TODO

  • layouts/_default/terms.html 用于渲染所有标签的列表页面(如 /tags/),而不是单个标签下的文章 terms.html:14-25
  • layouts/_default/list.html 是通用的列表模板,处理包括标签页面在内的各种列表类型

HOLD 留言框的优化 更加紧凑 验证码 确认提交

HOLD 暗色/亮色切换的图标 发光二极管

HOLD wiki 样式

嵌套的样式 使用 inbox 表示可以递归的词语. 或者下划线每个都是递归的引用 wiki的词条

HOLD 图片居中问题,文字环绕图片问题

使用#+attr_html设置class #+attr_html: :class foo /images/org-mode-unicorn-logo.png 然后通过CSS定义.foo类的样式来实现环绕效果

HOLD 对话样式, 大模型

HOLD 黄金分割线的hot图

HOLD 英文的文档建设

HOLD 特定语言的渲染样式

在性能受限的设备上通过api调用大模型

  • 火山引擎 豆包
  • 魔搭社区 deepseek
  • 魔搭社区 千问
  • 快速和思考模式

HOLD 对齐列表条目的缩进?

手写字体 xkcd

TODO Atria Snake EdgeOfSun

TODO 作者的链接 彩蛋弹窗

TODO chat 的样式

TODO Atria 的工作日记

TODO 主页的聊天气泡

新的主页样式

HOLD 主页以渐变的照片作为背景

全黑样式, 鼠标是光源

蒲公英的文字,风一吹就飘散掉文字

回声样式, 点击会扩散波纹,探明形状

线的主题

飘带的主题

星星瓶的主题

对联的主题

莫比乌斯带 无限之猫 猫的优雅

机械手臂完成字母的拼接

图标追逐鼠标光标,追到自动跳转

悬链线 Picatria

主页

hugo.yaml 配置

1. 基础站点配置

这里定义了网站的根 URL、语言设置和使用的主题

baseURL: "https://picatria.org.cn/"
defaultContentLanguage: zh
languageCode: "zh"
title: "Picatria"
theme: "PaperMod"
disablePathToLower: true

2. 内容处理与分页

设置中文统计逻辑(CJK)、分页大小以及摘要长度。

# 这会让 Hugo 把每一个中文字符都算作一个 word
# hasCJKLanguage: true

# 分页设置 - 新的配置方式
# pagination:
#  pagerSize: 10
# 摘要长度 - 顶层配置
# summaryLength: 120

3. 分类

定义标签(Tag)和分类(Category)的映射,以及 Robots.txt 和永久链接格式。

# 分类系统
taxonomy:
  tag: "tags"
  category: "categories"

# robots.txt
enableRobotsTXT: true

permalinks:
      categories: /categories/:slug/
      tags: /tags/:slug/

TODO 待添加SEO

# SEO 相关配置
params:
  description: "网站描述,用于搜索引擎和社交媒体分享"
  keywords: ["关键词1", "关键词2", "关键词3"]
  images: ["https://picatria.org.cn/images/og-image.jpg"]

TODO 4. 主题参数 (Params) - 核心与作者

从这里开始进入 params 层级。 注意 :为了保证 YAML 格式正确,后续属于 params 的代码块,所有内容都必须 保留 2 个空格的缩进

params:
  #时间格式
  disableDate: true
  disableLastmod: true
  #作者表
  authors:
    Atria:
      name: "Atria"
      url: "/info/update"
    EdgeOfSun:
      name: "EdgeOfSun"
      qrCode: "/qr/EdgeOfSun.webp"
    alice:
      name: "alice"
      url: "https://alice.example.com"
    bob:
      name: "bob"
      qrCode: "/qr/bob-qr.webp"
    picatria.org.cn:
      name: "picatria.org.cn"
      qrCode: "/qr/picatria.jpeg"
    snake:
      name: "snake"
      qrCode: "/qr/picatria.jpeg"

  mainSections: ["1_idea", "2_tool", "3_book", "4_math", "5_code", "6_nature"]

知识共享协议

 # 默认协议(数字或完整名)
  licenseCode: 2
  # 数字简写 → {名称, 通俗解读链接}
  licenseMap:
    a:
      name: "保留所有权利"
      url: ""
    1:
      name: "CC BY-NC-ND 4.0"
      url: "https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-Hans"
    2:
      name: "CC BY-NC-SA 4.0"
      url: "https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-Hans"
    4:
      name: "CC BY-NC 4.0"
      url: "https://creativecommons.org/licenses/by-nc/4.0/deed.zh-Hans"
    8:
      name: "CC BY 4.0"
      url: "https://creativecommons.org/licenses/by/4.0/deed.zh-Hans"

DONE 5. 主题参数 - 界面 UI

设置顶部标签、面包屑、页码显示和暗色模式开关。 (注意:此处代码保留了2空格缩进)

  # 顶部导航标签
  label:
    text: "Picatria"
    icon: "/favicon.ico"
    iconHeight: 35
  # 是否显示页码
  ShowPageNums: true
  ShowBreadCrumbs: true
  defaultTheme: "light"
  disableThemeToggle: false
  # 显示前一页/后一页
  #ShowPostNavLinks: true

6. 主题参数 - 首页 Profile 模式

配置首页的大图、标题、副标题以及导航按钮。

  # 首页 Profile 模式
  profileMode:
    enabled: true
    title:   "三角猫知识工坊"
    subtitle: "继续访问即视为您已充分阅读并同意本站[[法律声明]](/info/legal/)"
    imageUrl: "logo.svg"
    imageTitle: "logo"
    imageWidth: 200
    imageHeight: 200
    buttons:
      - name: "tool:生产力"
        url: "/2_tool/"
      - name: "math:是什么"
        url: "/4_math/"
      - name: "code:怎么做"
        url: "/5_code/"
      - name: "nature:自然"
        url: "/6_nature/"
      - name: "idea:芦苇荡"
        url: "/1_idea/"
      - name: "book:灵感来源"
        url: "/3_book/"
      - name: "about:关于我"
        url: "/info/aboutMe/"
      - name: "tele:联系我"
        url: "/info/tele/"

法律声明的css

.profile .profile_inner span a {
    color: var(--fold-h2);
    text-decoration: none;
    transition: box-shadow 0.2s ease-in-out;
}

.profile .profile_inner span a:hover {
    box-shadow: 0 1px 0;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
}

7. 主题参数 - 社交链接

  # 社交链接
  socialIcons:
    - name: "git"
      url: "/info/update/"
    - name: "key"
      url: "/info/key/"
    - name: "email"
      url: "mailto:contact@picatria.org.cn"
    - name: "wechat"
      url: "#"
      qrCode: "/qr/wechat-qr.webp"
      title: "微信公众号"
    - name: "keyoxide"
      url: "https://keyoxide.org/85F1E2C59766CB40FC878B1C457C726D13DB58CC"

8. 主题参数 - 资源与搜索

配置阅读时间、代码高亮、Favicon 图标以及 Fuse.js 搜索设置

  # 阅读与代码
  ShowCodeCopyButtons: true

  assets:
    # 代码高亮
    disableHLJS: true
    # 可选:禁用指纹识别以加快开发速度
    # disableFingerprinting: false
    # 图标配置
    favicon: "/favicon.ico"
    favicon16x16: "/favicon-16x16.png"
    favicon32x32: "/favicon-32x32.png"
    apple_touch_icon: "/apple-touch-icon.png"
    safari_pinned_tab: "/safari-pinned-tab.svg"
    # 主题颜色
    theme_color:  "#2f89fc"
    msapplication_TileColor:  "#2f89fc"

  # 搜索配置 (fuse.js)
  fuseOpts:
    isCaseSensitive: false
    shouldSort: true
    location: 0
    distance: 1000
    threshold: 0.4
    minMatchCharLength: 0
    limit: 10
    keys: ["title",'description', "summary"]

9. 输出格式与渲染

注意 :这里回到了 顶层配置 ,不再属于 params , 所以缩进恢复为 0. 定义 Hugo 的输出格式(JSON/RSS/HTML)和 Markdown 渲染引擎(Goldmark).

# 输出格式
outputs:
  home:
    - HTML
    - RSS
    - JSON
  section:
    - HTML
    - RSS
    - JSON
  taxonomy:
    - HTML
    - RSS
    - JSON
  # 添加搜索页面的输出
  term:
    - HTML
    - RSS

# 渲染选项
markup:
  goldmark:
    renderer:
      unsafe: true
  highlight:
    noClasses: false
    codeFences: true
    lineNos: false

404页面

{{/* Hugo 变量定义:放在文件最顶部 */}}
{{ $homeURL := "/" }}
{{ $chestURL := "https://lucky.picatria.org.cn" }}

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 - 页面未找到</title>

    <style>
    /*
      =================================================================
       ★★★ 全局基础与字体设置 ★★★
      =================================================================
    */
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700;900&display=swap');

    html, body {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
        overflow-x: hidden;
    }

    .scene-container {
        font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
        font-weight: 400;
    }

    :root {
        --primary: #007bff;
        --secondary: #6c757d;
        --entry: #ffffff;
        --border: #dee2e6;
        --text-color: #333333;
    }

    /*
      =================================================================
       ★★★ 模块一:PC端布局 (> 700px) ★★★
      =================================================================
    */

    /* 1.1: 主场景容器 */
    .scene-container {
        position: relative;
        width: 100%;
        height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
        background-color: var(--entry);
    }

    /*
       1.2: 所有文本内容的包裹器
       ★ 修改点:添加 Flex 布局以修复 PC 端对齐关系
       这会让 "旅行者..." 和 "404" 回到同一水平线上,恢复旧版的相对位置
    */
    .text-wrapper {
        text-align: center;
        position: relative;
        z-index: 10;

        /* 新增:模拟旧版嵌套结构的布局 */
        display: flex;
        flex-wrap: wrap;       /* 允许换行,确保下方文字能换行 */
        align-items: baseline; /* 文字基线对齐 */
        justify-content: center;
        column-gap: 16px;      /* 保持旧版的文字间距 */
    }

    /* 1.3: "旅行者..." 这行引导文字 */
    .line-intro {
        font-size: 1.2rem;
        color: var(--secondary);
        display: flex;
        align-items: baseline;
        justify-content: center;
        /* gap: 16px; 移到了父容器控制 */
    }

    /* 1.4: "404" 数字及其装饰的容器 */
    .inline-giant-404 {
        font-size: 5rem;
        font-weight: 900;
        line-height: 0.8;
        color: var(--text-color);
        position: relative;
        z-index: 1;
        display: inline-block;
    }
    .text-404-body { opacity: 0.12; }
    .inline-giant-404::before, .inline-giant-404::after { content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); border-radius: 50%; border-style: solid; border-color: currentColor; z-index: -1; pointer-events: none; opacity: 0.12; }
    .inline-giant-404::before { width: 1.9em; height: 1.9em; border-width: 0.02em; }
    .inline-giant-404::after  { width: 2.2em; height: 2.2em; border-width: 0.12em; }
    .deco-lines { position: absolute; left: 50%; top: 50%; transform: translate(-50%, 0); margin-top: 1.12em; width: 0.24em; height: 3.5em; background-color: currentColor; pointer-events: none; opacity: 0.12; }
    .deco-lines::before, .deco-lines::after { content: ""; position: absolute; top: 0; width: 0.02em; height: 100%; background-color: currentColor; }
    .deco-lines::before { left: -0.06em; }
    .deco-lines::after  { right: -0.06em; }

    /* 1.5: 地平线场景 */
    .horizon-group {
        position: absolute;
        left: 50%;
        top: 50%;
        /* 恢复相对位置后,这个偏移量应该能正常工作了 */
        transform: translateX(-70%);
        margin-top: calc(1.12em + 3.5em);
        width: clamp(280px, 90vw, 800px);
        display: flex;
        flex-direction: column;
        align-items: center;
        z-index: 5;
    }
    .horizon-line { width: 100%; height: 2px; background-color: var(--text-color); opacity: 0.3;}
    .horizon-ground { --g-height: 12px; --g-spacing: 10px; --g-angle: 153deg; --g-width: 1px; opacity: 0.3; width: 100%; height: var(--g-height); background-image: repeating-linear-gradient( var(--g-angle), transparent, transparent calc(var(--g-spacing) - var(--g-width)), var(--text-color) calc(var(--g-spacing) - var(--g-width)), var(--text-color) var(--g-spacing) ); border: none; }

    /* 1.6: 小车和按钮 */
    .car-container {
        position: absolute;
        width: clamp(180px, 50vw, 600px);
        height: auto;
        left: 50%;
        bottom: 0px;
        transform: translateX(-70%) translateY(13.6%);
        z-index: 6;
    }
    .car-animation-wrapper {
        transform-origin: 25% 75%;
        animation: engine-vibration 0.3s infinite linear;
    }
    .car-svg { position: relative; width: 100%; height: auto; display: block; }

    /* 1.7: 宝箱 */
    .chest-svg {
        position: absolute;
        width: 40px;
        height: auto;
        left: 60%;
        bottom: -50px;
        transform: translateX(450%);
        z-index: 6;
        opacity: 0.12;
        cursor: pointer;
        transition: transform 0.2s ease;
        color: var(--text-color);
    }
    .chest-svg:hover { transform: translateX(450%) translateY(-5px); }

    /* 1.8: 按钮 */
    .btn-get-on-board {
        position: absolute;
        top: 35%;
        left: 40%;
        transform: translate(-50%, -50%);
        width: 20%;
        aspect-ratio: 1 / 0.618;
        display: grid;
        place-content: center;
        font-size: clamp(0.75rem, 2.2vw, 1.5rem);
        font-weight: 700;
        letter-spacing: 0.05em;
        color: var(--text-color);
        background-color: transparent;
        border: 2px solid rgba(0, 0, 0, 0.2);
        border-radius: 8px;
        box-shadow: inset 0 0.1em 0.3em rgba(0, 0, 0, 0.25);
        text-decoration: none;
        white-space: nowrap;
        cursor: pointer;
        transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
    }
    .btn-get-on-board:hover {
        transform: translate(-50%, -50%) translateY(-3px) scale(1.03);
        box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
        background-color: rgba(0, 0, 0, 0.02);
    }
    .btn-get-on-board:active {
        transform: translate(-50%, -50%) translateY(1px) scale(0.99);
        box-shadow: inset 0 0.2em 0.5em rgba(0, 0, 0, 0.35);
        background-color: transparent;
    }

    /* 1.9: "没时间..." 文字 */
    .line-focus {
        margin-top: 20px;
        font-size: 2.5rem;
        font-weight: 900;
        color: var(--text-color);
        letter-spacing: 2px;

        /* 新增:确保它在Flex容器中强制独占一行,位于下方 */
        width: 100%;
    }

    /* 1.10: 动画 */
    @keyframes engine-vibration {
        0%   { transform: rotate(0deg); } 20%  { transform: rotate(-0.4deg); } 45%  { transform: rotate(0.5deg); } 70%  { transform: rotate(-0.2deg); } 85%  { transform: rotate(0.3deg); } 100% { transform: rotate(0deg); }
    }


    /*
      =================================================================
       ★★★ 模块二:移动端响应式布局 (<= 700px) ★★★
      =================================================================
    */
@media (max-width: 700px) {

    body { overflow-y: auto; }
    .scene-container { position: relative; }

    /*
       ★ 关键修复:重置 text-wrapper 为块级布局
       这取消了 PC 端的 Flex 设置,让移动端的绝对定位逻辑能正常生效
    */
    .text-wrapper { position: static; display: block; }

    /* ===============================
       核心锚点:404
       =============================== */
    .inline-giant-404 {
        position: absolute;
        left: 50%;
        top: 20vh;
        transform: translate(-50%, -50%);
        font-size: clamp(5rem, 22vw, 7rem);
    }

    /* ===============================
       上方文本
       =============================== */
    .line-intro {
        position: absolute;
        left: 50%;
        bottom: 83vh;
        transform: translateX(-50%) translateY(-1.1em);
        width: 100%;
        padding: 0 20px;
        box-sizing: border-box;
        text-align: center;
    }

    /* ===============================
       下方文本
       =============================== */
    .line-focus {
        position: absolute;
        left: 50%;
        top: 25vh;
        transform: translateX(-50%) translateY(1.2em);
        width: 100%;
        padding: 0 20px;
        box-sizing: border-box;
        text-align: center;
        font-size: clamp(1.2rem, 5vw, 1.5rem);
    }

    /* ===============================
       地平线 & 车
       =============================== */
    .horizon-group {
        left: 50%;
        transform: translateX(-50%);
        width: 100vw;
    }

    .car-container {
        left: 50%;
        transform: translateX(-50%) translateY(13.6%);
        width: 120vw;
    }

    .car-animation-wrapper { animation: none; }

    .chest-svg {
        left: 50%;
        transform: translateX(-50%);
        bottom: auto;
        top: 25vh;
        width: 50px;
    }
}

    </style>
</head>
<body>
    <div class="scene-container">
        <div class="text-wrapper">

            <div class="line-intro">
                <span>旅行者,前面的区域</span>
            </div>

            <div class="inline-giant-404">
                <span class="text-404-body">404</span>
                <span class="deco-lines"></span>
                <div class="horizon-group">
                    <div class="horizon-line"></div>
                    <div class="horizon-ground"></div>
                    <div class="car-container">
                        <div class="car-animation-wrapper">
                            <svg class="car-svg" viewBox="0 0 44.0625 24" xmlns="http://www.w3.org/2000/svg">
                              <path fill="currentColor" d="m 31.914657,13.710575 0.79,0.1 -0.02,0.23 0.47,0.17 0.15,-0.16 c 0.5,0.26 0.93,0.66 1.21,1.16 l -0.17,0.17 0.2,0.45 0.23,-0.02 c 0.09,0.29 0.14,0.59 0.14,0.9 l -0.1,0.78 -0.24,-0.01 -0.19,0.47 0.19,0.15 c -0.27,0.51 -0.66,0.93 -1.16,1.21 l -0.17,-0.18 -0.46,0.2 0.02,0.24 c -0.28,0.09 -0.58,0.14 -0.89,0.14 l -0.79,-0.1 0.01,-0.25 -0.46,-0.18 -0.16,0.19 c -0.5,-0.27 -0.93,-0.66 -1.21,-1.16 l 0.18,-0.18 -0.2,-0.46 -0.24,0.02 c -0.08,-0.28 -0.13,-0.57 -0.13,-0.88 l 0.11,-0.8 0.23,0.01 0.19,-0.46 -0.18,-0.15 c 0.27,-0.51 0.65,-0.92 1.15,-1.21 l 0.17,0.17 0.46,-0.2 -0.02,-0.22 c 0.28,-0.09 0.58,-0.14 0.89,-0.14 m 0,1.5 c -0.83,0 -1.5,0.67 -1.5,1.5 0,0.83 0.67,1.5 1.5,1.5 0.83,0 1.5,-0.67 1.5,-1.5 0,-0.83 -0.67,-1.5 -1.5,-1.5 m -12,-1.5 0.79,0.1 -0.02,0.23 0.47,0.17 0.15,-0.16 c 0.5,0.26 0.93,0.66 1.21,1.16 l -0.17,0.17 0.2,0.45 0.23,-0.02 c 0.09,0.29 0.14,0.59 0.14,0.9 l -0.1,0.78 -0.25,-0.01 -0.18,0.47 0.19,0.15 c -0.27,0.51 -0.66,0.93 -1.16,1.21 l -0.17,-0.18 -0.46,0.2 0.02,0.24 c -0.28,0.09 -0.58,0.14 -0.89,0.14 l -0.79,-0.1 0.01,-0.25 -0.46,-0.18 -0.16,0.19 c -0.5,-0.27 -0.93,-0.66 -1.21,-1.16 l 0.18,-0.18 -0.2,-0.46 -0.24,0.02 c -0.08,-0.28 -0.13,-0.57 -0.13,-0.88 l 0.11,-0.8 0.23,0.01 0.19,-0.46 -0.18,-0.15 c 0.27,-0.51 0.65,-0.92 1.15,-1.21 l 0.17,0.17 0.46,-0.2 -0.02,-0.22 c 0.28,-0.09 0.58,-0.14 0.89,-0.14 m 0,1.5 c -0.83,0 -1.5,0.67 -1.5,1.5 0,0.83 0.67,1.5 1.5,1.5 0.83,0 1.5,-0.67 1.5,-1.5 0,-0.83 -0.67,-1.5 -1.5,-1.5 m 10,-9.4999999 3,4 h 2 c 1.11,0 2,0.8899999 2,1.9999999 l -0.04453,4.090909 h -1.510204 c 0,-1.66 -1.941113,-3.06679 -3.601113,-3.06679 -1.66,0 -3.378479,1.384527 -3.378479,3.044527 h -5.176249 c 0,-1.66 -1.740743,-2.510204 -3.400743,-2.510204 -1.66,0 -2.977736,0.538515 -2.977736,2.198515 l -6.9647496,-0.04453 -0.044524,-3.686457 14.5760676,-0.02227 0.02226,-6.0037029 z m -4,1.5 v 2.5 h 5.46 l -1.96,-2.5 z" />
                            </svg>
                        </div>
                        <a href="{{ $homeURL }}" class="btn-get-on-board">上车</a>
                    </div>
                    <a href="{{ $chestURL }}" class="chest-svg">
                      <svg viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
                        <path fill="currentColor" d="m 146.857,20.842 c -12.535,-0.036 -24.268,2.86 -37.285,9.424 h 0.004 C 61.356,54.6 19.966,120.734 17.982,175.91 l 41.848,14.236 c 4.33,-61.89 47.057,-128.37 101.527,-155.86 h 0.002 c 4.28755,-2.168935 8.68894,-4.105079 13.185,-5.8 l -22.26,-7.45 c -1.80676,-0.12338 -3.61704,-0.188081 -5.428,-0.194 z m 59.34,20.19 c -10.478,-0.09 -22.832,3.093 -36.424,9.943 l 0.004,-0.004 c -48.23,24.34 -89.625,90.513 -91.548,145.436 l 156.485,53.24 c 3.865,-62.22 46.797,-129.372 101.613,-157.035 h 0.002 l 0.002,-0.003 c 4.17579,-2.110744 11.30868,-4.47694 15.68172,-6.140954 L 214.623,41.907 c -2.77429,-0.561972 -5.59643,-0.854332 -8.427,-0.873 z m 174.97,58.323 c -10.476,-0.09 -22.83,3.092 -36.42,9.94 l -0.005,0.002 c -48.577,24.518 -90.225,91.473 -91.586,146.623 l 46.205,15.72 c 3.914,-62.188 46.825,-129.274 101.607,-156.92 4.39309,-2.22751 8.91053,-4.20076 13.53,-5.91 l -26.544,-8.884 c -2.24393,-0.361889 -4.51215,-0.552439 -6.785,-0.57 z m 63.554,22.014 c -10.267,0.093 -22.094,3.353 -35.333,10.034 -47.158,23.8 -87.777,87.587 -91.362,141.75 l 174.55,-73.726 c -0.404,-39.01 -10.754,-61.304 -24.415,-71.082 -2.34242,-1.67558 -4.87715,-3.06447 -7.55,-4.137 l -0.01,0.034 -4.735,-1.584 c -3.48,-0.887 -7.195,-1.327 -11.144,-1.29 z M 17.9,195.622 17.865,383.106 59.46,397.58 V 209.764 L 17.9,195.624 Z m 60.25,20.498 v 187.962 l 156.282,54.37 V 269.288 l -29.053,-9.886 v 119.43 L 104.325,344.75 V 225.025 Z m 414.22,3.683 -173.937,73.467 v 189.236 l 173.935,-73.504 v -189.2 z m -369.354,11.582 v 99.947 l 63.675,21.477 v -99.763 l -63.674,-21.662 z m 31.306,28.797 c 9.705,0 17.573,7.867 17.573,17.572 0,6.34 -3.37,11.88 -8.407,14.97 v 28.53 h -18.69 v -28.746 c -4.838,-3.13 -8.048,-8.562 -8.048,-14.754 0,-9.705 7.867,-17.572 17.572,-17.572 z m 98.797,15.464 v 189.307 l 46.626,16.22 V 291.51 l -46.627,-15.864 z"/>
                      </svg>
                    </a>
                </div>
            </div>

            <div class="line-focus">
                没时间解释了,快上车。
            </div>

        </div>
    </div>
</body>
</html>

drop 和lucky 子域名

drop的 index.php

<?php
// =========================================================================
// Drop 站点 - 安全增强版 (v3.0: 策略分离 / Org日志 / Session验证)
// =========================================================================
session_start();


// --- 统一配置区 ---
$CONFIG_FILE = '/etc/picatria/hashes.txt';
$UPLOAD_DIR  = '/home/picatria_uploads/';
$USAGE_FILE  = '/var/www/drop/usage.cnt';
$MAX_DISK    = 2 * 1024 * 1024 * 1024; // 2GB

// 留言配置 (Org-mode 格式)
$COMMENT_LOG_FILE = '/home/picatria_uploads/comments.org'; // .log -> .org
$MAX_COMMENT_LENGTH = 500; // 稍微放宽字数,适应Org结构

// MIME 白名单
$ALLOWED_MIME = [
    'application/zip' => 'zip', 'application/x-zip-compressed' => 'zip',
    'application/x-rar-compressed' => 'rar', 'application/x-7z-compressed' => '7z',
    'application/octet-stream' => 'bin',
    'application/pdf' => 'pdf', 'text/plain' => 'txt',
    'application/msword' => 'doc',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
    'application/vnd.ms-excel' => 'xls',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
    'image/jpeg' => 'jpg', 'image/png'  => 'png', 'image/gif' => 'gif',
    'image/heic' => 'heic', 'image/heif' => 'heif',
    'video/mp4' => 'mp4', 'video/quicktime' => 'mov', 'video/x-msvideo' => 'avi',
    'audio/mpeg' => 'mp3', 'audio/wav' => 'wav'
];

// --- 🛡️ 安全策略配置 (策略分离) ---

// 策略 A: 文件上传 (Upload) - 更严格,防止磁盘滥用
$RL_UPLOAD_LOG = '/home/picatria_uploads/rl_upload.log';
$RL_UPLOAD_WINDOW = 600;      // 10分钟窗口
$RL_UPLOAD_MAX    = 5;        // 窗口内最多5次尝试
$RL_UPLOAD_DAILY  = 20;       // 单IP每日最多20次上传

// 策略 B: 留言 (Comment) - 较宽松,允许正常交流
$RL_COMMENT_LOG = '/home/picatria_uploads/rl_comment.log';
$RL_COMMENT_WINDOW = 300;     // 5分钟窗口
$RL_COMMENT_MAX    = 10;      // 窗口内最多10条
$RL_COMMENT_DAILY  = 50;      // 单IP每日最多50条


// --- 核心函数 ---

function checkAndAddQuota($bytesToAdd, $usageFile, $maxDisk) {
    $fp = fopen($usageFile, 'c+');
    if (!flock($fp, LOCK_EX)) return false;
    rewind($fp);
    $current = (int)fread($fp, 1024);
    if (($current + $bytesToAdd) > $maxDisk) {
        flock($fp, LOCK_UN); fclose($fp); return false;
    }
    flock($fp, LOCK_UN); fclose($fp); return true;
}

function updateQuota($bytesToAdd, $usageFile) {
    $fp = fopen($usageFile, 'c+');
    if (flock($fp, LOCK_EX)) {
        rewind($fp);
        $current = (int)fread($fp, 1024);
        ftruncate($fp, 0); rewind($fp);
        fwrite($fp, $current + $bytesToAdd);
        flock($fp, LOCK_UN);
    }
    fclose($fp);
}

/**
 * 通用频率限制检查器
 * @param string $logFile 日志文件路径
 * @param int $window 窗口时间()
 * @param int $maxRequests 窗口内最大请求数
 * @param int $dailyMax 每日最大请求数
 */
function check_rate_limit($logFile, $window, $maxRequests, $dailyMax) {
    $ip = $_SERVER['REMOTE_ADDR'];
    $now = time();
    $today = date('Y-m-d');
    $requestsInWindow = 0;
    $requestsToday = 0;

    // 确保日志文件存在并可写
    if (!file_exists($logFile)) {
        touch($logFile);
        chmod($logFile, 0666); // 确保读写权限
    }

    $fp = fopen($logFile, 'c+');
    if (!$fp || !flock($fp, LOCK_EX)) return ['allowed' => false, 'msg' => 'Server Busy'];

    $lines = [];
    while (($line = fgets($fp)) !== false) {
        $parts = explode('|', trim($line));
        if (count($parts) === 3) {
            list($entry_ip, $entry_time, $entry_date) = $parts;
            // 垃圾回收:只保留窗口期内或今天的日志
            if (($now - $entry_time) < $window || $entry_date === $today) {
                $lines[] = $line;
                if ($entry_ip === $ip) {
                    if (($now - $entry_time) < $window) $requestsInWindow++;
                    if ($entry_date === $today) $requestsToday++;
                }
            }
        }
    }

    if ($requestsInWindow >= $maxRequests) {
        flock($fp, LOCK_UN); fclose($fp);
        return ['allowed' => false, 'msg' => "操作过于频繁,请等待 " . ceil($window/60) . " 分钟"];
    }
    if ($requestsToday >= $dailyMax) {
        flock($fp, LOCK_UN); fclose($fp);
        return ['allowed' => false, 'msg' => '今日操作次数已达上限'];
    }

    // 记录本次请求
    $lines[] = "$ip|$now|$today\n";
    ftruncate($fp, 0); rewind($fp);
    foreach ($lines as $line) fwrite($fp, $line);

    flock($fp, LOCK_UN); fclose($fp);
    return ['allowed' => true];
}

// =========================================================================
// --- 路由与逻辑处理 ---
// =========================================================================

// CORS (生产环境请指定具体域名)
if (isset($_SERVER['HTTP_ORIGIN'])) {
    header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Headers: Content-Type');
    header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
}

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204); exit;
}

// [GET] 获取验证码
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_GET['action'] === 'get_captcha') {
    header('Content-Type: application/json');
    $num1 = rand(1, 10);
    $num2 = rand(1, 10);
    $_SESSION['captcha_answer'] = $num1 + $num2;
    $_SESSION['captcha_time'] = time();
    echo json_encode(['question' => "$num1 + $num2 = ?"]);
    exit;
}

// [POST] 业务处理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');

    // ====== 【关键修改 1】统一读取输入流 ======
    // 无论后面用不用,先读取并解析 JSON,避免重复操作流
    $rawBody   = file_get_contents('php://input');
    $jsonInput = json_decode($rawBody, true) ?: [];

    // ====== 【关键修改 2】统一获取 Action ======
    // 优先看 POST 表单 (上传文件),如果没有,再看 JSON (留言)
    $action = $_POST['action'] ?? $jsonInput['action'] ?? '';

    // --- 分支 1: 文件上传 (策略 A) ---
    if ($action === 'upload') {
        $jsonResponse = ['status' => 'error', 'msg' => '未知错误'];

        // 1. 频率检查
        $limit = check_rate_limit($RL_UPLOAD_LOG, $RL_UPLOAD_WINDOW, $RL_UPLOAD_MAX, $RL_UPLOAD_DAILY);
        if (!$limit['allowed']) {
            echo json_encode(['status'=>'error', 'msg'=>'⛔ ' . $limit['msg'], 'delay'=>10]);
            exit;
        }

        // 2. 原始上传逻辑 (保持不变,使用 $_POST$_FILES)
        $raw = $_POST['token'] ?? '';
        if (strlen($raw) > 1024) exit(json_encode(['status'=>'error','msg'=>'输入过长']));
        $poem = mb_substr(trim($raw), 0, 14, 'UTF-8');

        if (!preg_match('/^[\x{4e00}-\x{9fa5}]{14}$/u', $poem)) {
            sleep(2);
            $jsonResponse = ['status'=>'error', 'msg'=>'❌ 格式错误:必须是14个中文字符', 'delay'=>5];
        } else {
            $input_hash = hash('sha256', $poem);
            $lines = file_exists($CONFIG_FILE) ? file($CONFIG_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];
            $found = false;
            foreach ($lines as $line) {
                $parts = explode('|', $line);
                if (count($parts) < 2) continue;
                if (hash_equals(trim($parts[0]), $input_hash)) {
                    if (time() < (int)$parts[1]) $found = true;
                    break;
                }
            }
            if (!$found) {
                sleep(3);
                $jsonResponse = ['status'=>'error', 'msg'=>'❌ 密钥错误或已过期', 'delay'=>10];
            } else {
                if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
                    $jsonResponse['msg'] = '❌ 上传失败 (文件可能过大)';
                } elseif (!checkAndAddQuota($_FILES['file']['size'], $USAGE_FILE, $MAX_DISK)) {
                    $jsonResponse['msg'] = '⛔ 服务器空间已满';
                } else {
                    $finfo = finfo_open(FILEINFO_MIME_TYPE);
                    $mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
                    finfo_close($finfo);
                    if (!array_key_exists($mime, $ALLOWED_MIME)) {
                        $jsonResponse['msg'] = "❌ 文件类型不允许 ($mime)";
                    } else {
                        $safeExt = $ALLOWED_MIME[$mime];
                        $safeName = date('Ymd_His') . '_' . substr($input_hash, 0, 8) . '.' . $safeExt;
                        if (move_uploaded_file($_FILES['file']['tmp_name'], $UPLOAD_DIR . $safeName)) {
                            updateQuota($_FILES['file']['size'], $USAGE_FILE);
                            $jsonResponse = ['status'=>'success', 'msg'=>'✅ 上传成功!感谢投递。'];
                        } else {
                            $jsonResponse['msg'] = '❌ 保存失败 (权限不足)';
                        }
                    }
                }
            }
        }
        echo json_encode($jsonResponse);
        exit;
    }

    // --- 分支 2: 留言 (策略 B) ---
    elseif ($action === 'comment') {
        // 1. 频率检查
        $limit = check_rate_limit($RL_COMMENT_LOG, $RL_COMMENT_WINDOW, $RL_COMMENT_MAX, $RL_COMMENT_DAILY);
        if (!$limit['allowed']) {
            echo json_encode(['status' => 'error', 'msg' => '⛔ ' . $limit['msg'], 'delay' => 60]);
            exit;
        }

        // ====== 【关键修改 3】直接使用前面解析好的 $jsonInput ======
        $message     = $jsonInput['message'] ?? '';
        $userCaptcha = $jsonInput['captcha'] ?? null;
        $pageUrl     = $jsonInput['pageUrl'] ?? 'N/A';

        // 2. Session 验证 (销毁式)
        $expectedAnswer = $_SESSION['captcha_answer'] ?? null;
        $captchaTime    = $_SESSION['captcha_time'] ?? 0;
        unset($_SESSION['captcha_answer']);
        unset($_SESSION['captcha_time']);

        if (empty($message)) {
            $resp = ['msg' => '❌ 留言内容不能为空', 'delay' => 3];
        } elseif (mb_strlen($message, 'UTF-8') > $MAX_COMMENT_LENGTH) {
            $resp = ['msg' => '❌ 留言内容超过限制', 'delay' => 3];
        } elseif ($expectedAnswer === null || (time() - $captchaTime) > 600) {
            $resp = ['msg' => '❌ 验证码已过期', 'delay' => 3];
        } elseif ((int)$userCaptcha !== $expectedAnswer) {
            $resp = ['msg' => '❌ 计算错误', 'delay' => 5];
        } else {
            // 3. 构建 Org-mode 日志
            try {
                $date_str = date('Y-m-d D H:i');
                $ip_address = $_SERVER['REMOTE_ADDR'];
                $clean_message = trim($message);

                $org_entry  = "* [{$date_str}] 来自 {$ip_address} 的留言\n";
                $org_entry .= ":PROPERTIES:\n";
                $org_entry .= ":IP: {$ip_address}\n";
                $org_entry .= ":URL: {$pageUrl}\n";
                $org_entry .= ":END:\n";
                $org_entry .= "#+begin_quote\n";
                $org_entry .= "{$clean_message}\n";
                $org_entry .= "#+end_quote\n\n";

                if (file_put_contents($COMMENT_LOG_FILE, $org_entry, FILE_APPEND | LOCK_EX) !== false) {
                    $resp = ['status' => 'success', 'msg' => '✅ 留言成功'];
                } else {
                    $resp = ['msg' => '❌ 内部存储错误', 'delay' => 5];
                }
            } catch (Exception $e) {
                $resp = ['msg' => '❌ 服务器异常', 'delay' => 5];
            }
        }
        echo json_encode($resp ?? ['status'=>'error', 'msg'=>'未知错误']);
        exit;
    }

    // 无效 Action
    echo json_encode(['status' => 'error', 'msg' => 'Invalid Action']);
    exit;
}

?>
<!DOCTYPE html>
<!-- HTML 部分保持不变,无需修改 -->
<html>
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drop Box</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
/* ... CSS 保持不变 ... */
body{font-family:sans-serif;display:flex;justify-content:center;padding-top:30px;background:#f4f4f4}
.box{background:white;padding:25px;border-radius:10px;box-shadow:0 4px 10px rgba(0,0,0,0.1);text-align:center;width:380px}
.stepper-container { display: flex; justify-content: space-between; margin-bottom: 20px; position: relative; }
.stepper-container::before { content: ''; position: absolute; top: 14px; left: 0; right: 0; height: 2px; background: #eee; z-index: 0; }
.step { position: relative; z-index: 1; background: white; width: 25%; text-align: center; }
.step-circle { width: 30px; height: 30px; background: #eee; border-radius: 50%; margin: 0 auto 5px; line-height: 30px; color: #999; font-weight: bold; font-size: 14px; transition: 0.3s; }
.step-text { font-size: 12px; color: #999; }
.step.active .step-circle { background: #007bff; color: white; box-shadow: 0 0 5px rgba(0,123,255,0.5); }
.step.active .step-text { color: #007bff; font-weight: bold; }
.step.done .step-circle { background: #28a745; color: white; }
.step.done .step-text { color: #28a745; }
.step.error .step-circle { background: #dc3545; color: white; }
.step.error .step-text { color: #dc3545; }
input[type="text"]{width:100%;padding:10px;margin:10px 0;box-sizing:border-box;border:1px solid #ddd;border-radius:4px}
.upload-area {border: 2px dashed #ddd; padding: 15px; border-radius: 5px; margin-bottom: 10px; transition: 0.2s;}
.upload-area:hover {border-color: #007bff; background: #f8f9fa;}
.hidden-input {display: none;}
.btn {padding:10px; color:white; border:none; border-radius:5px; cursor:pointer; font-size:14px;}
.btn-primary {background:#007bff;}
.btn-success {background:#28a745;}
.btn:disabled {background:#ccc; cursor:not-allowed;}
#progress-section {display:none; margin-top:15px; text-align: left;}
progress {width: 100%; height: 20px;}
#progress-text {font-size: 12px; color: #666; margin-bottom: 4px; display: block; text-align: center;}
#status-msg {margin-top:15px; font-weight: bold;}
.text-error {color:#dc3545} .text-success {color:#28a745}
</style>
</head>
<body>
<div class="box">
    <h3>📦 文件投递箱</h3>
    <div class="stepper-container">
        <div class="step" id="step1"><div class="step-circle">1</div><div class="step-text">准备</div></div>
        <div class="step" id="step2"><div class="step-circle">2</div><div class="step-text">传输</div></div>
        <div class="step" id="step3"><div class="step-circle">3</div><div class="step-text">验证</div></div>
        <div class="step" id="step4"><div class="step-circle">4</div><div class="step-text">结果</div></div>
    </div>
    <div style="margin-bottom: 15px;">
        <input type="text" id="token" placeholder="输入14字中文密钥" maxlength="14" required autocomplete="off">
    </div>
    <div class="upload-area">
        <p style="margin:5px 0">📄 <b>单文件</b></p>
        <button class="btn btn-primary" onclick="selectFile()">选择</button>
        <div id="file-info" style="font-size:12px;margin-top:5px;color:#333"></div>
    </div>
    <div class="upload-area" id="area-folder">
        <p style="margin:5px 0">📂 <b>文件夹</b> (自动打包)</p>
        <button class="btn btn-success" onclick="selectFolder()">选择</button>
        <div id="folder-info" style="font-size:12px;margin-top:5px;color:#333"></div>
    </div>
    <input type="file" id="file-input" class="hidden-input">
    <input type="file" id="folder-input" class="hidden-input" webkitdirectory multiple>
    <button id="upload-btn" class="btn btn-primary" style="width:100%; margin-top:10px;" disabled>开始流程</button>
    <div id="progress-section">
        <span id="progress-text">准备中...</span>
        <progress id="progressBar" value="0" max="100"></progress>
    </div>
    <div id="status-msg"></div>
</div>

<script>
    // JS 保持完全不变,复用您已有的逻辑
    let selectedFiles = [];
    let uploadMode = 'none';
    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    const folderInputRef = document.getElementById('folder-input');
    const folderAreaText = document.querySelector('#area-folder p');
    const folderBtn = document.querySelector('#area-folder button');

    if (isMobile) {
        folderInputRef.removeAttribute('webkitdirectory');
        folderAreaText.innerHTML = '📸 <b>多图/批量上传</b> (自动打包)';
        folderBtn.innerText = "选择多张图片";
    }

    const fileInput = document.getElementById('file-input');
    const folderInput = document.getElementById('folder-input');
    const uploadBtn = document.getElementById('upload-btn');
    const progressSection = document.getElementById('progress-section');
    const progressBar = document.getElementById('progressBar');
    const progressText = document.getElementById('progress-text');
    const statusMsg = document.getElementById('status-msg');

    function selectFile() { document.getElementById('file-input').click(); }
    function selectFolder() { document.getElementById('folder-input').click(); }

    function updateStep(stepNum, status) {
        const step = document.getElementById('step' + stepNum);
        step.className = 'step ' + status;
        if (status === 'active') {
            for(let i=1; i<stepNum; i++) document.getElementById('step'+i).className = 'step done';
            for(let i=stepNum+1; i<=4; i++) document.getElementById('step'+i).className = 'step';
        }
        if (status === 'reset') for(let i=1; i<=4; i++) document.getElementById('step'+i).className = 'step';
    }

    fileInput.addEventListener('change', function() {
        if (this.files.length > 0) {
            selectedFiles = this.files;
            uploadMode = 'file';
            document.getElementById('file-info').innerText = this.files[0].name;
            document.getElementById('folder-info').innerText = "";
            folderInput.value = "";
            uploadBtn.disabled = false;
        }
    });

    folderInput.addEventListener('change', function() {
        if (this.files.length > 0) {
            let totalSize = 0;
            for(let f of this.files) totalSize += f.size;
            if (totalSize > 50 * 1024 * 1024) {
                alert("⚠️ 文件总大小超过 50MB,浏览器打包可能失败。\n建议使用单文件模式上传大视频。");
                this.value = ""; return;
            }
            selectedFiles = this.files;
            uploadMode = 'folder';
            document.getElementById('folder-info').innerText = `${this.files.length} 个文件`;
            document.getElementById('file-info').innerText = "";
            fileInput.value = "";
            uploadBtn.disabled = false;
        }
    });

    uploadBtn.addEventListener('click', async function() {
        const token = document.getElementById('token').value;
        if (token.length !== 14) {
            statusMsg.innerHTML = '<span class="text-error">请检查密钥长度</span>';
            return;
        }
        updateStep(1, 'active');
        uploadBtn.disabled = true;
        progressSection.style.display = 'block';
        statusMsg.innerText = '';

        let fileToUpload = null;
        try {
            if (uploadMode === 'file') {
                progressText.innerText = "读取文件...";
                fileToUpload = selectedFiles[0];
                updateStep(1, 'done');
                performUpload(fileToUpload, token);
            } else if (uploadMode === 'folder') {
                progressText.innerText = "正在打包 (浏览器端)...";
                const zip = new JSZip();
                for (let file of selectedFiles) {
                    const path = file.webkitRelativePath || file.name;
                    zip.file(path, file);
                }
                const content = await zip.generateAsync({
                    type: "blob",
                    compression: "DEFLATE",
                    compressionOptions: { level: 6 }
                }, (metadata) => {
                    progressBar.value = metadata.percent;
                    progressText.innerText = `打包中 ${metadata.percent.toFixed(0)}%`;
                });
                updateStep(1, 'done');
                const zipName = "archive_" + new Date().getTime() + ".zip";
                fileToUpload = new File([content], zipName, {type: "application/zip"});
                performUpload(fileToUpload, token);
            }
        } catch (err) {
            updateStep(1, 'error');
            statusMsg.innerHTML = '<span class="text-error">客户端错误: ' + err.message + '</span>';
            resetUI();
        }
    });

    function performUpload(file, token) {
        updateStep(2, 'active');
        const formData = new FormData();
        formData.append('token', token);
        formData.append('file', file);
        formData.append('action', 'upload');

        const xhr = new XMLHttpRequest();

        xhr.upload.addEventListener("progress", function(evt) {
            if (evt.lengthComputable) {
                const percent = Math.round((evt.loaded / evt.total) * 100);
                progressBar.value = percent;
                if (percent >= 100) {
                    progressText.innerText = "传输完成";
                    updateStep(2, 'done');
                    updateStep(3, 'active');
                    statusMsg.innerHTML = '<span>⏳ 正在验证与保存...</span>';
                } else {
                    progressText.innerText = `正在上传 ${percent}%`;
                }
            }
        }, false);

        xhr.onload = function() {
            if (xhr.status === 200) {
                try {
                    const res = JSON.parse(xhr.responseText);
                    if (res.status === 'success') {
                        updateStep(3, 'done');
                        updateStep(4, 'done');
                        statusMsg.innerHTML = `<span class="text-success">${res.msg}</span>`;
                        setTimeout(() => location.reload(), 3000);
                    } else {
                        updateStep(3, 'error');
                        statusMsg.innerHTML = `<span class="text-error">${res.msg}</span>`;
                        handleErrorDelay(res.delay);
                    }
                } catch (e) {
                    updateStep(4, 'error');
                    statusMsg.innerHTML = '<span class="text-error">解析失败</span>';
                    resetUI();
                }
            } else if (xhr.status === 413) {
                 updateStep(2, 'error');
                 statusMsg.innerHTML = '<span class="text-error">文件过大 (超过50MB)</span>';
                 resetUI();
            } else {
                updateStep(2, 'error');
                statusMsg.innerHTML = `<span class="text-error">请求失败 (${xhr.status})</span>`;
                resetUI();
            }
            progressSection.style.display = 'none';
        };

        xhr.onerror = function() {
            updateStep(2, 'error');
            statusMsg.innerHTML = '<span class="text-error">网络中断</span>';
            progressSection.style.display = 'none';
            resetUI();
        };

        xhr.open("POST", "", true);
        xhr.send(formData);
    }

    function resetUI() {
        uploadBtn.disabled = false;
        fileInput.disabled = false;
        folderInput.disabled = false;
    }

    function handleErrorDelay(delaySeconds) {
        if (!delaySeconds || delaySeconds <= 0) { resetUI(); return; }
        let failCount = parseInt(localStorage.getItem('drop_fail') || 0) + 1;
        localStorage.setItem('drop_fail', failCount);
        let wait = Math.min(Math.floor(delaySeconds * Math.pow(1.5, failCount - 1)), 60);
        let counter = wait;
        uploadBtn.innerText = `重试等待 ${counter}s...`;
        const timer = setInterval(() => {
            counter--;
            if (counter <= 0) {
                clearInterval(timer);
                resetUI();
                uploadBtn.innerText = "开始流程";
            } else {
                uploadBtn.innerText = `重试等待 ${counter}s...`;
            }
        }, 1000);
    }
</script>
</body>
</html>

lucky 的 index.php

<?php
header("Cache-Control: no-store, no-cache, must-revalidate");
$file = __DIR__ . '/list.txt';
$urls = file_exists($file) ? file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];

if (!empty($urls)) {
    $url = trim($urls[array_rand($urls)]);

    // 安全检查:只允许 http 或 https 开头的链接
    // 防止 javascript: 伪协议攻击或配置错误
    if (preg_match('#^https?://#i', $url)) {
        header("Location: " . $url, true, 302);
    } else {
        echo "Error: Invalid URL configuration.";
    }
} else {
    echo "No luck today.";
}
exit;

配置

亮色主题

/* assets/css/extended/custom.css */

/* ================= 1. 统一的变量定义 ================= */
:root,
body.light{
        /* PaperMod 核心变量 - 现代配色 */
    --gap: 24px !important;
    --content-gap: 20px !important;
    --nav-width: 1024px !important;
    --main-width: 720px !important;
    --header-height: 60px !important;
    --footer-height: 80px !important;
    --radius: 8px !important;

    /* 颜色变量是重灾区,必须加  */
    --theme: rgb(250, 251, 252) !important;
    --entry: rgb(255, 255, 255) !important;
    --primary: rgb(25, 28, 35) !important;
    --secondary: rgb(85, 95, 110) !important;
    --tertiary: rgb(200, 210, 225) !important;
    --content: rgb(40, 45, 55) ;
    --code-block-bg: rgb(245, 245, 245);
    --code-bg: rgb(235, 240, 250) ;
    --border: rgb(220, 225, 235) !important;
    --accent: rgb(65, 105, 225) !important;


   /* 自定义变量 */
    --custom-heading-h1: #2f89fc;
    --custom-heading-h2: #d95b1a;
    --custom-heading-h3: #565656;
    --custom-emphasis: #c01c28;
    --custom-link: #2f89fc;
    --custom-mark-bg: #60c0c5;
    --custom-mark-text: #000000;

    /* GitHub 风格变量 */
    --github-note-border: #0969da;
    --github-note-bg: #ddf4ff;
    --github-note-icon: #0969da;
    --github-tip-border: #1a7f37;
    --github-tip-bg: #dafbe1;
    --github-tip-icon: #1a7f37;
    --github-warn-border: #9a6700;
    --github-warn-bg: #fff8c5;
    --github-warn-icon: #9a6700;
    --github-alert-border: #d1242f;
    --github-alert-bg: #ffebe9;
    --github-alert-icon: #d1242f;
}

.post-content pre code {
    color: var(--primary)  !important;
}

暗色模式

/* assets/css/extended/custom.css */

/* ================= 1. 统一的变量定义 ================= */

/* 改进后的暗色主题 */
.dark {
    /* PaperMod 核心变量 - 改进的暗色模式 */
    --theme: #1e1e20;          /* 主背景色 - 更柔和的深灰黑 */
    --entry: #252529;          /* 内容区域背景 - 略浅于主背景 */
    --primary: #f0f0f2;        /* 主要文字 - 柔和白色 */
    --secondary: #a6a6b0;      /* 次要文字 - 柔和灰色 */
    --tertiary: #525263;       /* 第三级内容 - 中等深度灰色 */
    --content: #dcdce4;        /* 内容文字 - 略带蓝色调的白 */
    --code-block-bg: #1a1a1d;  /* 代码块背景 - 深色 */
    --code-bg: #33333b;        /* 代码背景 - 中等深度 */
    --border: #3a3a42;         /* 边框 - 中等深度灰色 */
    --accent: #769bbf;         /* 强调色 - 柔和蓝色 */

    /* 自定义变量 - 改进的暗色模式 */
    --custom-heading-h1: #8cb4f0;   /* H1标题 - 柔和蓝色 */
    --custom-heading-h2: #f4a97e;   /* H2标题 - 柔和橙色 */
    --custom-heading-h3: #b5c9e3;   /* H3标题 - 柔和蓝灰 */
    --custom-emphasis: #ff9e9e;

    --custom-link: #8cb4f0;         /* 链接颜色 - 蓝色 */
    --custom-mark-bg: #5d6b4d;      /* 标记背景 - 深橄榄绿 */
    --custom-mark-text: #f0f0f2;    /* 标记文字 - 白色 */

    /* GitHub 风格变量 - 改进的暗色模式 */
    --github-note-border: #5e81ac;           /* 注释边框 - 深蓝 */
    --github-note-bg: rgba(94, 129, 172, 0.15); /* 注释背景 */
    --github-note-icon: #81a1c1;            /* 注释图标 */
    --github-tip-border: #5e81ac;           /* 提示边框 - 蓝绿 */
    --github-tip-bg: rgba(94, 129, 172, 0.15);  /* 提示背景 */
    --github-tip-icon: #81a1c1;             /* 提示图标 */
    --github-warn-border: #ebcb8b;           /* 警告边框 - 柔和黄色 */
    --github-warn-bg: rgba(235, 203, 139, 0.15); /* 警告背景 */
    --github-warn-icon: #ebcb8b;             /* 警告图标 */
    --github-alert-border: #bf616a;          /* 警告边框 - 柔和红 */
    --github-alert-bg: rgba(191, 97, 106, 0.15);  /* 警告背景 */
    --github-alert-icon: #bf616a;            /* 警告图标 */
}

摘要区

.post-description {
    /* 使用代码块的背景色,通常比主背景稍微突出一点 */
    background: var(--code-bg);

    /* 左侧加一道竖线,使用你的强调色 */
    /* border-left: 4px solid var(--accent);  */

    /* 圆角处理,但左侧直角以贴合竖线(可选) */
    border-radius: 0 4px 4px 0;

    /* 内部间距 */
    padding: 1rem 1.2rem;

    /* 外部间距,与正文拉开距离 */
    margin-bottom: 20px;
    margin-top: 10px;

    /* 字体颜色使用次级文字颜色,比正文稍浅,形成层次 */
    color: var(--secondary);

    /* 字体大小调整(可选,略小于正文) */
    font-size: 1.1rem;

    /* 行高优化 */
    line-height: 1.6;
}

自动识别 org-wip 类,画出虚线,悬停时显示内容

/* =================================================================== */
/* Standalone Dashed Underline for Tooltips (using background-image)   */
/* =================================================================== */

.org-wip {
    /* 保持文字颜色与正文一致 */
    color: inherit;

    /* 移除旧的边框样式 */
    /* border-bottom: none; */

    /* 关键:使用背景图像画一条独立的虚线 */
    background-image: linear-gradient(to right, #bbb 50%, transparent 50%);
    background-position: 0 100%; /* 将“线”定位在底部 */
    background-size: 6px 1px;     /* 每个“虚线段+间隔”的总宽度为6px,线高1px */
    background-repeat: repeat-x;  /* 水平重复这个背景图像,形成一条完整的线 */

    /* 其他辅助样式 */
    cursor: help;
    position: relative;
    padding-bottom: 2px; /* 给线和文字留出一点呼吸空间 */
    transition: background-image 0.2s; /* 为悬停效果添加过渡动画 */
}

/* 鼠标悬停时的交互反馈 */
.org-wip:hover {
    /* 悬停时,只需要改变背景图像中的颜色即可 */
    background-image: linear-gradient(to right, #333 50%, transparent 50%);
}

/* ------------------------------------------------------------------- */
/*  悬浮框和三角箭头部分 (这部分不需要修改,保持原样即可)              */
/* ------------------------------------------------------------------- */

.org-wip::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    width: max-content;
    max-width: 250px;
    padding: 8px 12px;
    background: #333;
    color: #fff;
    border-radius: 4px;
    font-size: 13px;
    line-height: 1.5;
    text-align: center;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s, visibility 0.2s;
    z-index: 1000;
    pointer-events: none;
    margin-bottom: 8px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}

.org-wip::before {
    content: "";
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-top-color: #333;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s;
    margin-bottom: -4px;
}

.org-wip:hover::after,
.org-wip:hover::before {
    opacity: 1;
    visibility: visible;
}

h2,h3,h4,h5的样式

/* --- 3. 标题特定样式 --- (最终版:极限压缩连续标题间距) */

/* H2 (蓝色) */
.post-content h2.foldable-header {
    color: var(--fold-h2) ;
    font-size: 1.2em; /* 约 21.6px */
    font-weight: bold;
    border-bottom: 1px solid var(--border);
    margin-top: 1.2em;    /* 标题与上方【段落】的间距 */
    margin-bottom: 0.5em; /* 标题与下方【段落】的间距,略微减小 */
}

/* H3 (绿色) */
.post-content h3.foldable-header {
    color: var(--fold-h3) ;
    font-size: 1em; /* 18px */
    font-weight: bold;
    margin-top: 1.0em;
    margin-bottom: 0.4em;
}

/* H4 (黄绿色) */
.post-content h4.foldable-header {
    color: var(--fold-h4);
    font-size: 1em; /* 18px */
    font-weight: bold;
    margin-top: 0.9em;
    margin-bottom: 0.4em;
}

/* H5 (黄色) */
.post-content h5.foldable-header {
    color: var(--fold-h5) ;
    font-size: 1em; /* 18px */
    font-weight: bold;
    margin-top: 0.9em;
    margin-bottom: 0.4em;
}

/* --- 连续标题间距优化 (强化版) --- */
/*
 * 当一个标题紧跟着另一个标题时,
 * 大幅削减第二个标题的上边距,以消除叠加造成的过大间隙。
 */

/* H2 后面紧跟 H2 或 H3 */
.post-content h2.foldable-header + h2.foldable-header,
.post-content h2.foldable-header + h3.foldable-header {
    margin-top: 0.2em; /* 大幅减小,消除叠加间距 */
}

/* H3 后面紧跟 H3, H4 或 H5 */
.post-content h3.foldable-header + h3.foldable-header,
.post-content h3.foldable-header + h4.foldable-header,
.post-content h3.foldable-header + h5.foldable-header {
    margin-top: 0.2em; /* 大幅减小,保持统一的紧凑感 */
}

按钮的样式和颜色

/* 移除圆形样式,设置为长条状 */
.profile img {
  border-radius: 0;
  object-fit: cover;
}

.buttons {
  max-width: 600px;
}

/* 按钮样式 - 亮色模式 */
.button:nth-child(1),
.button:nth-child(2),
.button:nth-child(3),
.button:nth-child(4) {
  background: #45b7d1;
  color: #000000;
}

.button:nth-child(5),
.button:nth-child(6) {
  background: #4ecdc4;
  color: #000000;
}

.button:nth-child(7),
.button:nth-child(8) {
  background: #ff8c66;
  color: #000000;
}

/* 按钮样式 - 暗色模式 */
.dark .button:nth-child(1),
.dark .button:nth-child(2),
.dark .button:nth-child(3),
.dark .button:nth-child(4) {
  background: #5ac7e0;
  color: #000000;
}

.dark .button:nth-child(5),
.dark .button:nth-child(6) {
  background: #6eddd4;
  color: #000000;
}

.dark .button:nth-child(7),
.dark .button:nth-child(8) {
  background: #ff9c7a;
  color: #000000;
}

底部的信息

/* 底部样式变量定义 */

.custom-footer-info {
  margin-top: 10px;
  padding-top: 0;
  font-size: 12px;
  color: var(--secondary);
  text-align: center;
}

.footer-info-row {
  margin: 4px 0;
  line-height: 1.4;
  text-align: center;
}

.footer-info-row a {
  color: inherit;
  border-bottom: 1px solid var(--secondary);
  text-decoration: none;
}

.footer-info-row a:hover {
  color: var(--primary);
  border-bottom: 1px solid var(--primary);
}

跨文章引用块

/* ========== 跨文章引用卡片样式 ========== */
.cross-reference {
  display: flex;
  align-items: flex-start;
  background-color: var(--code-bg);
  border-left: 4px solid var(--secondary);
  border-radius: var(--radius);
  padding: 16px;
  margin: 2em 0;
  overflow: hidden;
}

.ref-icon {
  font-size: 24px;
  margin-right: 16px;
  opacity: 0.6;
  user-select: none;
}

.ref-content {
  flex: 1;
}

.ref-title {
  font-weight: bold;
  color: var(--primary);
  margin-bottom: 8px;
}

.ref-excerpt {
  font-size: 0.9em;
  color: var(--secondary);
  margin-bottom: 12px;
  line-height: 1.6;
}

.ref-source-link {
  font-size: 0.9em;
  font-weight: 500;
  color: var(--custom-link);
  text-decoration: none;
  border-bottom: 1px dotted var(--custom-link);
  transition: all 0.2s;
}

.ref-source-link:hover {
  color: var(--custom-emphasis);
  border-bottom: 1px solid var(--custom-emphasis);
}

.dark .cross-reference {
  border-left-color: var(--primary);
}

引用块和github 忠告块

/* ========== 基础优雅引用块 (默认blockquote样式) ========== */
.post-content blockquote,
.entry-content blockquote {
  position: relative;
  padding: 1.5rem 2rem;
  margin: 2rem 0;
  font-style: italic;
  border-left: 4px solid var(--secondary);
  background-color: var(--entry);
  color: var(--primary);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  border-radius: 0 4px 4px 0;
}

.dark .post-content blockquote,
.dark .entry-content blockquote {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

.post-content blockquote::before,
.entry-content blockquote::before {
  content: "“";
  position: absolute;
  top: -15px;
  left: -10px;
  font-size: 4rem;
  font-family: Georgia, serif;
  color: var(--secondary);
  opacity: 0.3;
}

.post-content blockquote p,
.entry-content blockquote p {
  margin: 0;
}

.post-content blockquote footer,
.entry-content blockquote footer {
  margin-top: 1rem;
  font-size: 0.9rem;
  font-style: normal;
  display: block;
  text-align: right;
  /* color: var(--secondary); */
}
/* ================= GitHub 风格忠告块 (Unicode 极简版) ================= */


/* 2. 容器样式 */
.blockquote-github {
    font-family: var(--font-sans);
    margin: 1.5em 0;
    padding: 16px;
    border-left: 4px solid;
    border-radius: 0 6px 6px 0;
    color: var(--primary);
    line-height: 1.6;
}

/* 3. 标题区 (图标 + 文字) */
.blockquote-github .github-title {
    font-weight: bold;
    color: inherit; /* 标题文字保持正文色,不变色 */
    margin-right: 6px;
    display: inline; /* 强制行内显示 */
}

/* 4. 图标样式 (Unicode 字符) */
.blockquote-github .github-icon {
    display: inline-block;
    margin-right: 6px;
    font-style: normal;
    font-weight: normal; /* 符号本身不需要加粗,防止变形 */
    font-size: 1.2em;   /* 图标稍微大一点点 */
    line-height: 1;
    vertical-align: -2px; /* 微调对齐 */
}

/* 5. 布局控制 */
.blockquote-github p {
    margin: 0;
    display: inline; /* 核心:段落行内显示,不换行 */
}
.blockquote-github p + p {
    display: block; /* 第二段开始换行 */
    margin-top: 8px;
}

/* 6. 颜色映射 (使用 color 控制文字颜色) */
.blockquote-github.note {
    border-color: var(--github-note-border);
    background-color: var(--github-note-bg);
}
.blockquote-github.note .github-icon { color: var(--github-note-icon); }

.blockquote-github.tip {
    border-color: var(--github-tip-border);
    background-color: var(--github-tip-bg);
}
.blockquote-github.tip .github-icon { color: var(--github-tip-icon); }

.blockquote-github.warning {
    border-color: var(--github-warn-border);
    background-color: var(--github-warn-bg);
}
.blockquote-github.warning .github-icon { color: var(--github-warn-icon); }

.blockquote-github.alert {
    border-color: var(--github-alert-border);
    background-color: var(--github-alert-bg);
}
.blockquote-github.alert .github-icon { color: var(--github-alert-icon); }

DONE svg 公式

/* ============================================================
   Hugo + Org-mode SVG 公式样式(完整版)
   ============================================================ */

/* ============================================================
   1. 全局:消除阴影
   ============================================================ */
img.org-svg {
    box-shadow: none !important;
}

/* ============================================================
   2. 行内公式(p 内)
   ============================================================ */
.post-content p img.org-svg,
.entry-content p img.org-svg {
    display: inline-block !important;
    height: 0.85em !important;       /* 略小于 1em,修正数学字体虚高 */
    max-height: 1.8em !important;    /* 给分数/根号留余量 */
    width: auto !important;
    vertical-align: -0.1em;          /* 贴近文字基线 */
    margin: 0 0.12em !important;
}

/* ============================================================
   3. 裸块级公式(不在 p,也不在 .equation-container)
   例如:段落外独立一行的 <img class="org-svg">
   ============================================================ */
.post-content img.org-svg:not(p img):not(.equation-container img),
.entry-content img.org-svg:not(p img):not(.equation-container img) {
    display: block !important;
    margin: 1.4rem auto !important;
    max-width: 100% !important;
    height: auto !important;
    width: auto !important;
}

/* ============================================================
   4. 编号公式容器
   用 flexbox 替代 grid,避免 overflow-x 导致列折叠
   ============================================================ */
.post-content .equation-container,
.entry-content .equation-container {
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    margin: 1.8rem 0;
    overflow-x: auto;
    /* 左侧等宽占位,让公式视觉居中 */
    padding-left: 0;
}

/* 公式区:占满剩余空间,内容居中 */
.post-content .equation-container .equation,
.entry-content .equation-container .equation {
    flex: 1 1 0%;
    display: flex !important;
    justify-content: center;
    align-items: center;
    min-width: 0;            /* 防止 flex 子项溢出 */
}

/* 容器内公式图片 */
.post-content .equation-container img.org-svg,
.entry-content .equation-container img.org-svg {
    display: block !important;
    margin: 0 !important;
    max-width: 100% !important;
    height: auto !important;
    width: auto !important;
}

/* 编号标签:紧贴右侧,不参与伸缩 */
.post-content .equation-container .equation-label,
.entry-content .equation-container .equation-label {
    flex: 0 0 auto;
    font-family: var(--font-body), serif;
    font-size: 0.95rem;
    opacity: 0.7;
    white-space: nowrap;
    padding-left: 1rem;
}

.post-content .equation-container .equation-label::before,
.entry-content .equation-container .equation-label::before {
    content: "(";
}

.post-content .equation-container .equation-label::after,
.entry-content .equation-container .equation-label::after {
    content: ")";
}

/* ============================================================
   5. 暗色模式
   ============================================================ */
.dark img.org-svg {
    filter: none;
}

.dark .equation-container .equation-label {
    color: #bbb;
}

/* ============================================================
   6. 段落对齐修正
   ============================================================ */
.post-content p,
.entry-content p {
    text-align: left;
}
/* ============================================================
   Hugo + Org-mode SVG 公式样式 (完整版)
   ============================================================ */

/* ============================================================
   1. 行内公式 —— 反向排除策略
   覆盖 p / li / td / 任何非容器父元素
   ============================================================ */
img.org-svg:not(.equation-container img) {
    display: inline-block !important;
    height: 1em !important;
    max-height: 2.2em !important;
    width: auto !important;
    vertical-align: -0.15em;
    box-shadow: none !important;
    margin: 0 0.1em !important;
}

/* ============================================================
   2. 行间公式容器(含编号)
   ============================================================ */

/* 2.1 外层容器 */
.post-content .equation-container,
.entry-content .equation-container {
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 100%;
    margin: 1.5rem 0;
    padding: 0.5rem 3rem;
    overflow-x: auto;
    box-sizing: border-box;
}

/* 2.2 公式 span:透明容器,不干扰 flex 居中 */
.post-content .equation-container .equation,
.entry-content .equation-container .equation {
    display: contents;
}

/* 2.3 容器内图片:不限制高度,保持原始比例 */
.post-content .equation-container img.org-svg,
.entry-content .equation-container img.org-svg {
    display: block !important;
    margin: 0 !important;
    max-width: 100% !important;
    height: auto !important;
    max-height: none !important;
    width: auto !important;
    box-shadow: none !important;
}

/* ============================================================
   3. 编号标签
   ============================================================ */
.post-content .equation-container .equation-label,
.entry-content .equation-container .equation-label {
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--font-body, "IBM Plex Serif", "Noto Serif SC", serif);
    font-size: 1rem;
    font-weight: bold;
    color: inherit;
    opacity: 0.75;
    white-space: nowrap;
}

/* 自动添加括号 */
.post-content .equation-container .equation-label::before,
.entry-content .equation-container .equation-label::before {
    content: "(";
    margin-right: 1px;
}

.post-content .equation-container .equation-label::after,
.entry-content .equation-container .equation-label::after {
    content: ")";
    margin-left: 1px;
}

/* ============================================================
   4. 暗色模式
   ============================================================ */
.dark img.org-svg {
    filter: invert(0.88) hue-rotate(180deg);
}

.dark .equation-container .equation-label {
    color: #bbb;
    opacity: 0.85;
}

/* ============================================================
   5. 段落对齐修正
   ============================================================ */
.post-content p,
.entry-content p {
    text-align: left;
}
:tangle ../assets/css/extended/custom.css
/* ============================================================
   Hugo + Org-mode SVG 公式样式 (完整版)
   ============================================================ */

/* ============================================================
   1. 行内公式 —— 反向排除策略
   覆盖 p / li / td / 任何非容器父元素
   ============================================================ */
img.org-svg:not(.equation-container img) {
    display: inline-block !important;
    height: 1em !important;
    max-height: 2.2em !important;
    width: auto !important;
    vertical-align: -0.15em;
    box-shadow: none !important;
    margin: 0 0.1em !important;
}

/* ============================================================
   2. 行间公式容器(含编号)
   ============================================================ */

/* 2.1 外层容器 */
.post-content .equation-container,
.entry-content .equation-container {
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 100%;
    margin: 1.5rem 0;
    padding: 0.5rem 3rem;
    overflow-x: auto;
    box-sizing: border-box;
}

/* 2.2 公式 span:透明容器,不干扰 flex 居中 */
.post-content .equation-container .equation,
.entry-content .equation-container .equation {
    display: contents;
}

/* 2.3 容器内图片:不限制高度,保持原始比例 */
.post-content .equation-container img.org-svg,
.entry-content .equation-container img.org-svg {
    display: block !important;
    margin: 0 !important;
    max-width: 100% !important;
    height: auto !important;
    max-height: none !important;
    width: auto !important;
    box-shadow: none !important;
}

/* ============================================================
   3. 编号标签
   ============================================================ */
.post-content .equation-container .equation-label,
.entry-content .equation-container .equation-label {
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--font-body, "IBM Plex Serif", "Noto Serif SC", serif);
    font-size: 1rem;
    font-weight: bold;
    color: inherit;
    opacity: 0.75;
    white-space: nowrap;
}

/* 自动添加括号 */
.post-content .equation-container .equation-label::before,
.entry-content .equation-container .equation-label::before {
    content: "(";
    margin-right: 1px;
}

.post-content .equation-container .equation-label::after,
.entry-content .equation-container .equation-label::after {
    content: ")";
    margin-left: 1px;
}

/* ============================================================
   4. 暗色模式
   ============================================================ */
.dark img.org-svg {
    filter: invert(0.88) hue-rotate(180deg);
}

.dark .equation-container .equation-label {
    color: #bbb;
    opacity: 0.85;
}

/* ============================================================
   5. 段落对齐修正
   ============================================================ */
.post-content p,
.entry-content p {
    text-align: left;
}
/* ============================================================
   1. 行内公式 Inline Math
   ============================================================ */

.post-content img.org-svg,
.entry-content img.org-svg {
    box-shadow: none !important;
}

/* 行内公式 —— 基线对齐是关键 */
.post-content p img.org-svg,
.entry-content p img.org-svg {
    display: inline-block;
    height: 1em;              /* 与文字高度一致 */
    width: auto;
    vertical-align: -0.15em;  /* 精准基线校正 */
    margin: 0 0.15em;
}


/* ============================================================
   2. 行间公式 Display Math
   ============================================================ */

.post-content img.org-svg[alt^="\\["],
.post-content img.org-svg[alt^="$$"],
.entry-content img.org-svg[alt^="\\["],
.entry-content img.org-svg[alt^="$$"] {
    display: block;
    margin: 1.6rem auto;
    height: auto;
    max-width: 100%;
}


/* ============================================================
   3. 编号公式 —— 用 grid 代替 absolute
   ============================================================ */

.post-content .equation-container,
.entry-content .equation-container {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    column-gap: 1rem;

    margin: 1.8rem 0;
    overflow-x: auto;
}

.post-content .equation-container .equation,
.entry-content .equation-container .equation {
    justify-self: center;
}

.post-content .equation-container img.org-svg,
.entry-content .equation-container img.org-svg {
    display: block;
    max-width: 100%;
    height: auto;
}

/* 编号 */
.post-content .equation-container .equation-label,
.entry-content .equation-container .equation-label {
    font-family: var(--font-body), serif;
    font-size: 0.95rem;
    opacity: 0.7;
    white-space: nowrap;
}

.post-content .equation-container .equation-label::before {
    content: "(";
}
.post-content .equation-container .equation-label::after {
    content: ")";
}


/* ============================================================
   4. 暗色模式 —— 不再使用 invert
   ============================================================ */

.dark img.org-svg {
    filter: none;
}

.dark .equation-container .equation-label {
    color: #bbb;
}
/* ============================================================
   数学公式 SVG 优化版(Hugo + Org-mode ltximg)
   核心改进:全部使用结构化选择器(不依赖不可靠的 alt),对齐/间距/防重叠更专业
   ============================================================ */

/* 1. 行内公式(Inline Math) - 结构化选择器,精准可靠 */
.post-content p img.org-svg,
.entry-content p img.org-svg {
    display: inline-block !important;
    vertical-align: -0.18em !important;   /* 远优于 middle,自然贴合基线 */
    height: 1.08em !important;            /* 比 max-height 更稳定,适配各种字体 */
    width: auto !important;
    margin: 0 0.15em !important;
    box-shadow: none !important;
}

/* 2. 所有行间公式容器(含带编号、无编号、TikZ 等) */
.post-content .equation-container,
.entry-content .equation-container {
    display: block;
    position: relative;
    text-align: center;
    margin: 2.2rem 0;                     /* 更大呼吸感,阅读更舒服 */
    padding: 0.6rem 4.8rem 0.6rem 0;      /* 右侧留足够空间,彻底防长公式重叠 */
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* 容器内公式包裹层(简化,无需 width:100%) */
.post-content .equation-container .equation,
.entry-content .equation-container .equation {
    display: inline-block;
    max-width: 100%;
}

/* 容器内公式图片(支持 TikZ 大图不被压扁) */
.post-content .equation-container img.org-svg,
.entry-content .equation-container img.org-svg {
    display: block !important;
    margin: 0 auto !important;
    max-height: none !important;
    max-width: none !important;
    width: auto !important;
    box-shadow: none !important;
}

/* 3. 公式编号美化 + 防重叠 */
.post-content .equation-container .equation-label,
.entry-content .equation-container .equation-label {
    position: absolute;
    right: 0.8rem;                        /* 比紧贴边缘更优雅 */
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;

    font-family: var(--font-body), "IBM Plex", "Songti SC", "Noto Serif SC", serif;
    font-size: 1.05rem;
    font-weight: 600;
    color: inherit;
    opacity: 0.85;
    white-space: nowrap;
    padding-left: 1.8em;                  /* 公式与编号之间的呼吸间距(关键) */
}

/* 自动加括号 */
.post-content .equation-container .equation-label::before,
.entry-content .equation-container .equation-label::before {
    content: "(";
}
.post-content .equation-container .equation-label::after,
.entry-content .equation-container .equation-label::after {
    content: ")";
}

/* ============================================================
   4. 通用 + 暗色模式(更自然)
   ============================================================ */
.post-content p,
.entry-content p {
    text-align: left;
}

/* 所有公式统一暗色(颜色还原度更高) */
.dark img.org-svg {
    filter: invert(0.92) hue-rotate(180deg) saturate(1.1);
}

/* 暗色模式编号微调 */
.dark .equation-container .equation-label {
    color: #ccc;
    opacity: 0.9;
}

/* 移动端友好(长公式不挤) */
@media (max-width: 768px) {
    .equation-container {
        padding-right: 3.5rem;
    }
    .equation-container .equation-label {
        font-size: 1rem;
        padding-left: 1.2em;
    }
}
/* ============================================================
   Hugo + Org-mode SVG 公式样式
   核心思路:用 DOM 结构区分行内 / 行间,而非 alt 文本
   ============================================================ */

/* ============================================================
   1. 行内公式
   匹配依据:段落 <p> 内直接包含的 .org-svg
   ============================================================ */
.post-content p img.org-svg,
.entry-content p img.org-svg {
    display: inline-block !important;
    vertical-align: -0.2em;       /* 比 middle 更贴近数学排版基线 */
    margin: 0 0.1em !important;
    height: 1.1em;                 /* 相对行高,自适应字号 */
    max-height: 2em;               /* 防止分数/上下标撑破行距 */
    width: auto !important;
    box-shadow: none !important;
}

/* ============================================================
   2. 行间公式(有编号容器)
   ============================================================ */

/* 2.1 外层容器 */
.post-content .equation-container,
.entry-content .equation-container {
    display: flex;
    align-items: center;
    justify-content: center;        /* 公式居中 */
    position: relative;
    width: 100%;
    margin: 1.5rem 0;
    padding: 0.5rem 3rem;           /* 两侧留出编号空间,防止压叠 */
    overflow-x: auto;
    box-sizing: border-box;
}

/* 2.2 公式 span:只包裹图片,不影响布局 */
.post-content .equation-container .equation,
.entry-content .equation-container .equation {
    display: contents;              /* 透明容器,不产生额外盒子 */
}

/* 2.3 容器内的图片 */
.post-content .equation-container img.org-svg,
.entry-content .equation-container img.org-svg {
    display: block !important;
    max-width: 100% !important;
    width: auto !important;
    height: auto !important;
    box-shadow: none !important;
    margin: 0 !important;
}

/* 2.4 编号标签 */
.post-content .equation-container .equation-label,
.entry-content .equation-container .equation-label {
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--font-body, "IBM Plex Serif", "Noto Serif SC", serif);
    font-size: 1rem;
    color: inherit;
    opacity: 0.75;
    white-space: nowrap;
}

.post-content .equation-container .equation-label::before { content: "("; }
.post-content .equation-container .equation-label::after  { content: ")"; }
.entry-content .equation-container .equation-label::before { content: "("; }
.entry-content .equation-container .equation-label::after  { content: ")"; }

/* ============================================================
   3. 无容器的独立行间公式(org 生成的裸 <img>,不在容器内)
   匹配依据:不在 <p> 内,也不在 .equation-container 内
   ============================================================ */
.post-content img.org-svg:not(p img):not(.equation-container img),
.entry-content img.org-svg:not(p img):not(.equation-container img) {
    display: block !important;
    margin: 1.2rem auto !important;
    max-width: 100% !important;
    width: auto !important;
    box-shadow: none !important;
}

/* ============================================================
   4. 暗色模式
   ============================================================ */
.dark img.org-svg {
    filter: invert(0.88) hue-rotate(180deg);
}

.dark .equation-container .equation-label {
    color: #bbb;
    opacity: 0.85;
}

/* ============================================================
   5. 段落对齐修正
   ============================================================ */
.post-content p,
.entry-content p {
    text-align: left;
}
/* ============================================================
   1. 行内公式 (Inline Math)
   策略:所有 .org-svg 默认为行内,除非被包含在特定容器中
   ============================================================ */
/* 增加 p 标签限定,提高权重并防止误伤其他地方的 svg */
.post-content p .org-svg,
.entry-content p .org-svg {
    display: inline-block !important;
    /* 核心修正:LaTeX 公式通常基线偏下,middle 会导致公式显得“飘”在空中 */
    vertical-align: -0.15em;
    margin: 0 0.1em !important;
    height: 1.1em; /* 限制高度而非 max-height,确保与文字大小一致 */
    width: auto !important;
    box-shadow: none !important;
    border: none !important;
}

/* ============================================================
   2. 块级公式容器 (Equation Container)
   策略:包含标准行间公式 和 编号公式
   ============================================================ */
.post-content .equation-container,
.entry-content .equation-container {
    display: block;
    width: 100%;
    margin: 1.5rem 0;
    text-align: center;
    position: relative; /* 为编号的绝对定位做参照 */
    overflow-x: auto;   /* 允许长公式在移动端横向滚动,不撑破页面 */
    overflow-y: hidden; /* 隐藏不必要的垂直滚动条 */
}

/* 容器内的图片样式 */
.post-content .equation-container img.org-svg,
.entry-content .equation-container img.org-svg {
    display: block !important;
    margin: 0 auto !important; /* 居中 */
    max-width: 100% !important; /* 也就是不超过屏幕宽度 */
    height: auto !important;
    box-shadow: none !important;
}

/* ============================================================
   3. 编号系统 (.equation-label)
   策略:保持你的绝对定位逻辑,但增加移动端适配
   ============================================================ */
.post-content .equation-container .equation-label,
.entry-content .equation-container .equation-label {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;

    font-family: var(--font-body), serif;
    font-size: 0.9rem;
    color: inherit;
    opacity: 0.7;
    white-space: nowrap;

    /* 加上一点背景色(适配你的背景),防止公式太长时文字重叠看不清 */
    background-color: var(--entry, #fff);
    padding: 0 0 0 5px;
}

/* 自动添加圆括号 */
.post-content .equation-container .equation-label::before { content: "("; }
.post-content .equation-container .equation-label::after  { content: ")"; }

/* --- 移动端优化 (关键) --- */
@media screen and (max-width: 600px) {
    /* 屏幕很窄时,如果公式很宽,编号可能会挡住公式。
       这里提供一种策略:让编号稍微靠下,或者允许遮挡(依靠 overflow 滚动)
       通常保持现状即可,但如果遇到重叠严重,可以取消绝对定位,改为由 Flexbox 放置到底部
    */
    /* 可选方案:极窄屏幕下编号移到下方 */
    /*
    .post-content .equation-container {
        display: flex;
        flex-direction: column;
        align-items: flex-end; /* 编号靠右 */
    }
    .post-content .equation-container .equation {
        width: 100%;
        text-align: center;
    }
    .post-content .equation-container .equation-label {
        position: static;
        transform: none;
        margin-top: 5px;
        font-size: 0.8rem;
    }
    */
}

/* ============================================================
   4. 暗色模式适配 (Dark Mode)
   ============================================================ */
/*
   invert(1) hue-rotate(180deg) 是处理黑白公式的最佳实践。
   它把黑色变成白色,但保留了颜色的色相(红色还是红色,只是亮度变了)。
   Added: mix-blend-mode 帮助去背景
*/
.dark img.org-svg {
    filter: invert(1) hue-rotate(180deg);
}

/*
   进阶:如果某个 SVG 是复杂的彩色图表(TikZ),不希望被反色?
   你需要在 Org-mode 中给它加特定属性,例如 #+ATTR_HTML: :class no-dark-invert
*/
.dark img.org-svg.no-dark-invert {
    filter: none !important;
}

.dark .equation-container .equation-label {
    background-color: var(--entry, #222); /* 暗色模式下的背景,防止透明重叠 */
    color: #aaa;
}
:tangle ../assets/css/extended/custom.css

/* ============================================================
   1. 行内公式 (Inline Math)
   ============================================================ */
.post-content img.org-svg[alt^="$"],
.entry-content img.org-svg[alt^="$"] {
    display: inline-block !important;
    vertical-align: middle;
    margin: 0 0.15em !important;
    max-height: 1.5em !important;
    width: auto !important;
    box-shadow: none !important;
}

/* ============================================================
   2. 标准行间公式 (Display Math: \[ ... \])
   ============================================================ */
.post-content img.org-svg[alt^="\\["],
.post-content img.org-svg[alt^="$$"],
.entry-content img.org-svg[alt^="\\["],
.entry-content img.org-svg[alt^="$$"] {
    display: block !important;
    margin: 1.2rem auto !important;
    max-height: none !important;
    max-width: 100% !important;
    width: auto !important;
    box-shadow: none !important;
}

/* ============================================================
   3. 复杂绘图与多行容器 (TikZ / Equation Container / Numbered Equations)
   此处增加了对编号 (.equation-label) 的布局支持
   ============================================================ */

/* 3.1 容器:设置为相对定位,作为编号绝对定位的参照物 */
.post-content .equation-container,
.entry-content .equation-container {
    display: block;           /* 保持块级 */
    position: relative;       /* 关键:让内部的 absolute 元素相对于此容器定位 */
    text-align: center;       /* 让内部内容居中 */
    width: 100%;
    margin: 1.5rem 0;
    padding: 0.5rem 0;        /* 增加一点内边距防止图片贴边 */
    overflow-x: auto;         /* 保持对超大 TikZ 图的滚动支持 */
}

/* 3.2 公式包裹层 (.equation span):确保它不阻挡居中 */
.post-content .equation-container .equation,
.entry-content .equation-container .equation {
    display: inline-block;    /* 改回 inline-block 或 block 均可,配合父元素 text-align: center */
    width: 100%;              /* 占满宽度以便内部图片居中 */
    vertical-align: middle;
}

/* 3.3 图片:强制居中 */
.post-content .equation-container img.org-svg,
.entry-content .equation-container img.org-svg {
    display: block !important;
    margin: 0 auto !important;
    max-height: none !important;
    max-width: none !important; /* 允许 TikZ 图保持原尺寸(配合容器滚动) */
    width: auto !important;
    box-shadow: none !important;
}

/* 3.4 编号 (.equation-label) 美化与定位 - 字体修正版 */
.post-content .equation-container .equation-label,
.entry-content .equation-container .equation-label {
    /* --- 定位逻辑 (保持不变) --- */
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;

    /* --- 样式美化 --- */
    /* 1. 字体核心修正:优先使用CSS变量,如果没有则使用你提供的字体栈 */
    font-family: var(--font-body), "IBM Plex", "Songti SC", "Noto Serif SC", "SimSun", serif;

    font-size: 1rem;          /* 大小与正文一致 */
    font-weight: bold;        /* 保持加粗以“醒目” */
    color: inherit;           /* 颜色跟随正文,或者使用 #555 */
    opacity: 0.8;             /* 稍微淡一点点,体现它是编号,不是正文内容 */

    padding-right: 15px;
    white-space: nowrap;
}

/* --- 自动添加括号 (保持不变) --- */
.post-content .equation-container .equation-label::before,
.entry-content .equation-container .equation-label::before {
    content: "(";
    margin-right: 1px;
}

.post-content .equation-container .equation-label::after,
.entry-content .equation-container .equation-label::after {
    content: ")";
    margin-left: 1px;
}



/* --- 暗色模式适配 --- */
.dark .equation-container .equation-label {
    color: #bbb; /* 暗色模式下字体变亮 */
}

/* ============================================================
   4. 通用修正
   ============================================================ */
.post-content p,
.entry-content p {
    text-align: left;
}

.dark img.org-svg {
    filter: invert(0.9) hue-rotate(180deg);
}
/* 暗色模式下编号颜色调整 */
.dark .equation-container .equation-label {
    color: #aaa;
}

留言

js

document.addEventListener('DOMContentLoaded', function () {
    const commentButton = document.getElementById('comment-button');
    const modal = document.getElementById('comment-modal');
    const closeModalBtn = document.getElementById('modal-close-btn');

    if (!commentButton || !modal) return;

    const formSection = document.getElementById('comment-form-section');
    const successMessage = document.getElementById('comment-success-message');
    const statusDiv = document.getElementById('comment-status');
    const submitBtn = document.getElementById('comment-submit-btn');
    const textarea = document.getElementById('comment-textarea');
    const captchaQuestion = document.getElementById('captcha-question');
    const captchaInput = document.getElementById('captcha-input');
    const authorContactLink = document.getElementById('author-contact-link');

    let submissionTimer = null;

    // [核心变更 1] 异步向服务器请求题目
    async function getCaptcha() {
        try {
            captchaQuestion.textContent = "...";
            // ⚠️ 如果是本地测试请改用 localhost,生产环境请用真实域名
            const baseUrl = 'https://drop.picatria.org.cn/';

            const response = await fetch(baseUrl + '?action=get_captcha', {
                method: 'GET',
                credentials: 'include' // [核心变更 2] 必须携带 Cookie 以维持 Session
            });

            if (!response.ok) throw new Error('Network error');
            const data = await response.json();
            captchaQuestion.textContent = data.question;
        } catch (error) {
            captchaQuestion.textContent = "Error";
            showStatus('验证码加载失败', 'error');
        }
    }

    function openModal() {
        if (submissionTimer) clearInterval(submissionTimer);
        textarea.value = '';
        captchaInput.value = '';
        statusDiv.textContent = '';
        statusDiv.className = '';
        formSection.style.display = 'block';
        successMessage.style.display = 'none';
        submitBtn.disabled = false;
        submitBtn.innerText = "确认提交";

        const contactUrl = commentButton.getAttribute('data-author-contact');
        if (contactUrl) {
            authorContactLink.href = contactUrl;
        }

        // 打开时获取题目
        getCaptcha();
        modal.style.display = 'flex';
    }

    function closeModal() {
        modal.style.display = 'none';
    }

    async function submitComment() {
        const message = textarea.value.trim();
        const userCaptcha = captchaInput.value;

        if (message.length === 0) { showStatus('❌ 留言内容不能为空', 'error'); return; }
        if (message.length > 200) { showStatus('❌ 留言内容超过200字', 'error'); return; }
        if (userCaptcha === '') { showStatus('❌ 请输入计算结果', 'error'); return; }

        submitBtn.disabled = true;
        showStatus('正在提交...', 'info');

        try {
            const baseUrl = 'https://drop.picatria.org.cn/';

            const response = await fetch(baseUrl, {
                method: 'POST',
                credentials: 'include', // [核心变更 2] 必须携带 Cookie
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    action: 'comment', // [核心变更 3] 明确告诉 PHP 我要留言
                    message: message,
                    captcha: userCaptcha,
                    pageUrl: window.location.href
                })
            });

            if (!response.ok) throw new Error(`服务器错误: ${response.status}`);

            const result = await response.json();

            if (result.status === 'success') {
                formSection.style.display = 'none';
                successMessage.style.display = 'block';
                statusDiv.textContent = '';
                setTimeout(closeModal, 8000);
            } else {
                showStatus(result.msg || '提交失败', 'error');
                getCaptcha(); // 失败后刷新验证码
                handleSubmissionDelay(result.delay || 3);
            }
        } catch (error) {
            showStatus('网络请求失败,请稍后再试', 'error');
            handleSubmissionDelay(5);
        }
    }

    function showStatus(message, type) {
        statusDiv.textContent = message;
        statusDiv.className = type === 'error' ? 'text-error' : 'text-success';
    }

    function handleSubmissionDelay(delaySeconds) {
        if (submissionTimer) clearInterval(submissionTimer);
        let counter = Math.max(Math.floor(delaySeconds), 3);
        submitBtn.disabled = true;
        submitBtn.innerText = `重试等待 ${counter}s...`;
        submissionTimer = setInterval(() => {
            counter--;
            if (counter <= 0) {
                clearInterval(submissionTimer);
                submitBtn.disabled = false;
                submitBtn.innerText = "确认提交";
            } else {
                submitBtn.innerText = `重试等待 ${counter}s...`;
            }
        }, 1000);
    }

    commentButton.addEventListener('click', function(e) { e.preventDefault(); openModal(); });
    closeModalBtn.addEventListener('click', closeModal);
    modal.addEventListener('click', function(e) { if (e.target === modal) { closeModal(); } });
    submitBtn.addEventListener('click', submitComment);
});

css

.post-tags li a.action-tag {
  /* 使用主题定义的边框和强调色 */
  border: 1px solid var(--custom-link);
  color: var(--custom-link);
  font-weight: bold;
  background-color: transparent;
  transition: all 0.2s ease-in-out;
}

.post-tags li a.action-tag:hover {
  /* 悬停时反转颜色 */
  background-color: var(--custom-link);
  color: var(--entry); /* 字体颜色变为卡片背景色(通常是白色或深灰),保证对比度 */
  border-color: var(--custom-link);
}

/* ==========================================================================
   留言弹窗样式
   ========================================================================== */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  /* 遮罩层保持半透明黑,不受暗色模式影响太大,或者可以用 rgba(0,0,0,0.8) */
  background-color: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(2px); /* 增加一点毛玻璃效果,更现代 */
  display: none;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-content {
  /* 核心适配:背景色和文字色跟随主题 */
  background: var(--entry);
  color: var(--primary);

  padding: 25px 30px;
  border-radius: var(--radius); /* 使用统一圆角 */
  border: 1px solid var(--border); /* 增加微弱边框,提升暗色模式下的层次感 */
  box-shadow: 0 10px 25px rgba(0,0,0,0.2);
  width: 90%;
  max-width: 500px;
  text-align: left;
  position: relative;
}

.modal-close {
  position: absolute;
  top: 10px;
  right: 15px;
  font-size: 28px;
  font-weight: bold;
  cursor: pointer;
  color: var(--secondary); /* 使用次要文字颜色 */
  transition: color 0.2s;
}
.modal-close:hover {
  color: var(--primary); /* 悬停变亮 */
}

.modal-content h3 {
  margin-top: 0;
  text-align: center;
  color: var(--custom-heading-h2); /* 使用你定义的 H2 标题色,或者 var(--primary) */
}

.modal-content textarea {
  width: 100%;
  min-height: 120px;
  padding: 10px;
  box-sizing: border-box;

  /* 输入框适配 */
  background-color: var(--theme); /* 使用主题背景色,与弹窗背景形成对比 */
  color: var(--primary);
  border: 1px solid var(--border);

  border-radius: 5px;
  font-size: 14px;
  margin-bottom: 15px;
  resize: vertical;
  font-family: inherit;
  outline: none;
}

.modal-content textarea:focus,
.captcha-section input:focus {
    border-color: var(--custom-link); /* 聚焦时使用强调色 */
}

/* --- 验证码区域 --- */
.captcha-section {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 20px;
  color: var(--primary);
}
.captcha-section label {
  font-weight: bold;
  white-space: nowrap;
}
.captcha-section input {
  width: 80px;
  padding: 8px;

  /* 输入框适配 */
  background-color: var(--theme);
  color: var(--primary);
  border: 1px solid var(--border);

  border-radius: 5px;
  text-align: center;
  outline: none;
}

/* --- 隐藏数字输入框的上下箭头 --- */
#captcha-input::-webkit-outer-spin-button,
#captcha-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
#captcha-input {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* --- 按钮与状态信息 --- */
.modal-btn {
  width: 100%;
  padding: 12px;
  font-size: 16px;
  border: none;
  border-radius: 5px;
  cursor: pointer;

  /* 按钮适配 */
  background-color: var(--custom-link);
  color: #fff; /* 按钮文字保持白色通常没问题,或者用 var(--theme) */
  font-weight: bold;

  transition: opacity 0.2s, transform 0.1s;
}
.modal-btn:hover {
  opacity: 0.9; /* 稍微变透明一点来表示悬停,比改颜色更通用 */
}
.modal-btn:active {
  transform: scale(0.98);
}
.modal-btn:disabled {
  background-color: var(--secondary);
  cursor: not-allowed;
  opacity: 0.6;
}

#comment-status {
  text-align: center;
  margin-top: 15px;
  font-weight: bold;
  min-height: 20px;
}

/* 使用 GitHub 风格的变量来显示错误和成功 */
.text-error {
    color: var(--github-alert-icon) !important;
}
.text-success {
    color: var(--github-tip-icon) !important;
}

/* 留言成功后的提示信息样式 - 完美适配你的 GitHub 变量 */
#comment-success-message {
    padding: 15px;
    margin-top: 15px;
    border-radius: var(--radius);

    /* 核心修改:使用 GitHub Tip 变量 */
    border-left: 5px solid var(--github-tip-border);
    background-color: var(--github-tip-bg);
    color: var(--primary); /* 确保文字在背景上可见 */

    line-height: 1.6;
    font-size: 14px;
}
#comment-success-message a {
    color: var(--custom-link);
    text-decoration: underline;
    font-weight: bold;
}

DONE 字体增强

custom.css

/* Crimson Pro - Regular */
@font-face {
  font-family: 'Crimson Pro';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/crimson-pro-v28-latin-regular.woff2') format('woff2');
}

/* Crimson Pro - Italic */
@font-face {
  font-family: 'Crimson Pro';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/crimson-pro-v28-latin-italic.woff2') format('woff2');
}

/* Crimson Pro - SemiBold (600) */
@font-face {
  font-family: 'Crimson Pro';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/fonts/crimson-pro-v28-latin-600.woff2') format('woff2');
}

/* Crimson Pro - Bold (700) */
@font-face {
  font-family: 'Crimson Pro';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/fonts/crimson-pro-v28-latin-700.woff2') format('woff2');
}

/* jetbrains-mono-regular - latin */
@font-face {
  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  src: url('/fonts/jetbrains-mono-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-italic - latin */
@font-face {
  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
  font-family: 'JetBrains Mono';
  font-style: italic;
  font-weight: 400;
  src: url('/fonts/jetbrains-mono-v24-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-600 - latin */
@font-face {
  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 600;
  src: url('/fonts/jetbrains-mono-v24-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-700 - latin */
@font-face {
  font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 700;
  src: url('/fonts/jetbrains-mono-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

/* ================= 0. 基础设置 ================= */
:root {
    /* 这里必须写 'Crimson Pro',因为你在 @font-face 里定义的就是这个名字 */
    --font-body: "Crimson Pro", "Songti SC", "SimSun", serif;

    /* 这里必须写 'JetBrains Mono' */
    --font-mono: "JetBrains Mono", "PingFang SC", "Microsoft YaHei", monospace;
    /* 明确设置字体大小 */
    --font-size-body: 18px;
    --font-size-code: 0.9em; /* 调整代码字体大小 */

}

/* ================= 2. 暗色模式专属优化 (核心修改) ================= */
@media (prefers-color-scheme: dark) {
    :root {
        /*
           暗色模式下重写 --font-body:
           1. 英文:继续用 Crimson Pro (它的可读性不错)。
           2. 中文:强制改为 "PingFang SC" (苹果黑体) 和 "Microsoft YaHei" (微软雅黑)。

           原因:黑体的笔画粗细均匀,彻底解决宋体在黑底下的“糊”和“晕”的问题。
        */
        --font-body: "Crimson Pro", "PingFang SC", "Microsoft YaHei", sans-serif;
    }

    /*
       macOS 专属优化:
       在暗色背景下开启 antialiased,会让字体变细一点点,
       减少“光渗”带来的糊感,让边缘更锐利。
    */
    body {
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }
}
/* ================= 2. 应用字体 (仅修改 Font-Family) ================= */

/* A. 全局正文应用 */
body {
    font-family: var(--font-body) !important;
    font-size: var(--font-size-body);
    -webkit-font-smoothing: antialiased; /* 让宋体在 Mac 上更清晰 */
}

/* B. 强制标题也使用这套字体 */
/* 注意:这里只改 font-family,绝对不会影响你下面定义的 color/margin/size */
h1, h2, h3, h4, h5, h6, .post-content h2.foldable-header {
    font-family: var(--font-body) !important;
}

/* C. 代码字体应用 */
code, pre, kbd, samp, .menu-prefix, .menu-suffix {
    font-family: var(--font-mono) !important;
}

/* D. 代码视觉平衡微调 (重要) */
/* 因为 JetBrains Mono 原生字形比宋体大,这里只针对“行内代码”微缩,保持行高和谐 */
/* 这里的 :not(pre) 确保不影响大代码块的排版 */
:not(pre) > code {
    font-size: 0.9em !important;
    /* 如果你觉得行内代码太素,可以保留下面这行淡淡的背景,不需要则删掉 */
    /* background: rgba(0, 0, 0, 0.05); */
}

/* 让代码高亮中的注释部分变为斜体 (Hugo Chroma 默认类名通常是 .c, .c1, .cm) */
.chroma .c, .chroma .c1, .chroma .cm {
    font-style: italic !important;
    opacity: 0.8; /* 稍微降低透明度,让注释不抢代码的风头 */
}

DONE 搜索样式

  • description:面向搜索引擎和社交媒体,建议150-160字符,包含关键词
  • summary:面向读者,可以是更长更详细的内容摘要
  • description 在单篇文章页面的头部显示
  • summary 在文章列表的每个条目中显示

DONE custom.css搜索框和搜索结果呈现

/* --- 搜索框样式 (保持不变) --- */
#searchbox {
  margin: 20px auto 40px auto;
  position: relative;
}

#searchbox input {
  padding: 12px 20px;
  width: 100%;
  color: var(--primary);
  background-color: var(--entry);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 16px;
  transition: border-color 0.2s, box-shadow 0.2s;
}

#searchbox input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--theme-hover);
  outline: none;
}

/* --- 搜索结果容器 --- */
#searchResults {
    display: none; /* 默认隐藏,由 JS 控制 */
}

/*
 * --- 核心交互优化 ---
 * 我们不再使用旧的 li:hover 样式。
 * 因为搜索结果现在是 <article class="post-entry">,它们会继承主列表的样式。
 * 我们在这里为搜索结果中的条目定义一个专属的、更优雅的悬停效果。
*/

/* 为搜索结果中的 .post-entry 添加平滑过渡效果 */
#searchResults .post-entry {
    transition: transform 0.2s ease-out, box-shadow 0.2s ease-out, background-color 0.2s;
    border: 1px solid transparent; /* 预留边框位置,防止悬停时跳动 */
}

/* 优雅的“卡片浮起”悬停效果 */
#searchResults .post-entry:hover {
    transform: translateY(-4px); /* 向上微移,产生浮起感 */
    box-shadow: 0 10px 20px rgba(0,0,0,0.08); /* 添加柔和的阴影 */
    background-color: var(--entry); /* 确保背景色不变,不覆盖内容 */
    border-color: var(--border); /* 添加一个微妙的边框,增强边界感 */
}


/* --- 关键词高亮 (保持不变) --- */
mark.search-highlight {
    background-color: transparent;
    color: var(--custom-emphasis);
    font-weight: bold;
    border-bottom: 2px solid var(--custom-emphasis);
    padding-bottom: 1px;
    box-decoration-break: clone;
}

DONE fastsearch.js

/**
 * fastsearch.js - 优化版 (全局搜索)
 *
 * - 修复了 `for...in` 循环导致的重复结果 Bug。
 * - 搜索时自动隐藏/显示原始文章列表。
 * - 搜索结果中包含封面图,样式与主列表完全一致。
 * - 专为全局搜索设计,始终使用 /index.json。
 */
import * as params from '@params';

// --- 工具函数:高亮关键词 ---
function highlightText(text, keyword) {
    if (!text || !keyword) return text;
    const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(`(${escapedKeyword})`, "gi");
    return text.replace(regex, '<mark class="search-highlight">$1</mark>');
}

// --- DOM 元素引用 ---
let fuse;
const sInput = document.getElementById('searchInput');
const searchResultsContainer = document.getElementById('searchResults');
const postListContainer = document.getElementById('post-list-container');
const infiniteScrollStatus = document.getElementById('infinite-scroll-status');

// 确保所有需要的 DOM 元素都存在,否则不执行任何操作
if (sInput && searchResultsContainer) {
    const indexPath = '/index.json'; // 全局搜索始终使用根索引

    // 异步加载搜索索引
    window.onload = function () {
        fetch(indexPath)
            .then(response => {
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                return response.json();
            })
            .then(data => {
                if (data && data.length > 0) {
                    fuse = new Fuse(data, {
                        minMatchCharLength: 2,
                        threshold: 0.4,
                        includeMatches: true,
                        ignoreLocation: true,
                        keys: [
                            { name: 'title', weight: 0.8 },
                            { name: 'summary', weight: 0.5 }
                        ]
                    });
                }
            })
            .catch(error => console.error("全局搜索索引加载失败:", error));
    };

    // --- 重置函数:清空搜索并恢复原始列表 ---
    const resetSearch = () => {
        sInput.value = '';
        searchResultsContainer.innerHTML = '';
        searchResultsContainer.style.display = 'none';

        // 只有在页面上存在这些元素时才操作它们
        if (postListContainer) postListContainer.style.display = 'block';
        if (infiniteScrollStatus) infiniteScrollStatus.style.display = 'flex';
    };

    // --- 核心:监听输入事件 ---
    sInput.onkeyup = function () {
        if (!fuse) return;

        const searchKey = this.value.trim();

        if (searchKey.length === 0) {
            resetSearch();
            return;
        }

        // 隐藏原始列表 (如果存在)
        if (postListContainer) postListContainer.style.display = 'none';
        if (infiniteScrollStatus) infiniteScrollStatus.style.display = 'none';

        searchResultsContainer.style.display = 'block';

        const results = fuse.search(searchKey, { limit: 20 });

        if (results.length > 0) {
            let resultSet = '';

            // BUG 修复: 使用 for...of 循环
            for (const result of results) {
                const data = result.item;

                const highlightedTitle = highlightText(data.title, searchKey);
                const highlightedSummary = highlightText(data.summary, searchKey);

                let coverHTML = '';
                if (data.cover_image) {
                    coverHTML = `
                    <figure class="entry-cover" style="margin-bottom: 10px;">
                        <img src="${data.cover_image}" alt="${data.title}" loading="lazy" style="width: 100%; max-height: 250px; object-fit: cover; border-radius: 5px;">
                    </figure>`;
                }

                resultSet += `
                <article class="post-entry">
                    ${coverHTML}
                    <header class="entry-header"><h2>${highlightedTitle}</h2></header>
                    <div class="entry-content"><p>${highlightedSummary}</p></div>
                    <footer class="entry-footer">
                        <span>${data.date || ''}</span>
                    </footer>
                    <a class="entry-link" aria-label="post link to ${data.title}" href="${data.permalink}"></a>
                </article>`;
            }
            searchResultsContainer.innerHTML = resultSet;
        } else {
            searchResultsContainer.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--secondary);">没有找到相关内容</div>';
        }
    };

    // --- 监听 Escape 键 ---
    document.onkeydown = function (e) {
        if (e.key === "Escape") {
            resetSearch();
        }
    };
}

DONE section-search.js

import * as params from '@params';

// --- 工具函数:高亮关键词 ---
function highlightText(text, keyword) {
    if (!text || !keyword) return text;
    const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(`(${escapedKeyword})`, "gi");
    return text.replace(regex, '<mark class="search-highlight">$1</mark>');
}

// --- DOM 元素引用 ---
let fuse;
const sInput = document.getElementById('searchInput');
const searchResultsContainer = document.getElementById('searchResults');
const postListContainer = document.getElementById('post-list-container'); // <-- 获取原始文章列表
const infiniteScrollStatus = document.getElementById('infinite-scroll-status'); // <-- 获取瀑布流状态栏

// --- 路径判断与索引加载 (与之前相同) ---
const currentPath = window.location.pathname;
const sectionMatch = currentPath.match(/^\/(1_idea|2_tool|3_book|4_math|5_code|6_nature)(?:\/|$)/);
let indexPath = sectionMatch ? `/${sectionMatch[1]}/index.json` : '/index.json';

window.onload = function () {
    if (!indexPath || !sInput) return;
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
            let data = JSON.parse(xhr.responseText);
            if (data && data.length > 0) {
                fuse = new Fuse(data, {
                    minMatchCharLength: 2,
                    threshold: 0.4,
                    includeMatches: true,
                    ignoreLocation: true,
                    keys: [
                        { name: 'title', weight: 0.8 },
                        { name: 'summary', weight: 0.5 }
                    ]
                });
            }
        }
    };
    xhr.open('GET', indexPath);
    xhr.send();
};

// --- 重置函数:清空搜索并恢复原始列表 ---
function resetSearch() {
    sInput.value = '';
    searchResultsContainer.innerHTML = '';
    searchResultsContainer.style.display = 'none';

    // 恢复原始列表的显示
    if(postListContainer) postListContainer.style.display = 'block';
    if(infiniteScrollStatus) infiniteScrollStatus.style.display = 'flex';
}

// --- 核心:监听输入事件 ---
sInput.onkeyup = function (e) {
    if (!fuse) return;

    const searchKey = this.value.trim();

    // 1. 如果搜索框为空,恢复原始状态
    if (searchKey.length === 0) {
        resetSearch();
        return;
    }

    // 2. 隐藏原始文章列表
    if(postListContainer) postListContainer.style.display = 'none';
    if(infiniteScrollStatus) infiniteScrollStatus.style.display = 'none';

    // 3. 执行搜索
    const results = fuse.search(searchKey, { limit: 20 });

    // 4. 显示搜索结果容器
    searchResultsContainer.style.display = 'block';

    if (results.length > 0) {
        let resultSet = '';

        // **BUG 修复**: 使用 for...of 循环,正确迭代结果数组
        for (const result of results) {
            let data = result.item;

            // 高亮处理
            let highlightedTitle = highlightText(data.title, searchKey);
            let highlightedSummary = highlightText(data.summary, searchKey);

            // 封面图 HTML (条件渲染)
            let coverHTML = '';
            if (data.cover_image) {
                coverHTML = `
                <figure class="entry-cover" style="margin-bottom: 10px;">
                    <img src="${data.cover_image}" alt="${data.title}" loading="lazy" style="width: 100%; max-height: 250px; object-fit: cover; border-radius: 5px;">
                </figure>`;
            }

            // 构建与 list.html 完全一致的 <article> 结构
            resultSet += `
            <article class="post-entry">
                ${coverHTML}
                <header class="entry-header"><h2>${highlightedTitle}</h2></header>
                <div class="entry-content"><p>${highlightedSummary}</p></div>
                <footer class="entry-footer">
                    <span title='${data.date}'>${data.date}</span>
                </footer>
                <a class="entry-link" aria-label="post link to ${data.title}" href="${data.permalink}"></a>
            </article>`;
        }
        searchResultsContainer.innerHTML = resultSet;
    } else {
        searchResultsContainer.innerHTML = '<div class="no-results" style="text-align: center; padding: 40px; color: var(--secondary);">没有找到相关内容</div>';
    }
};

// 按下 Escape 键清空
document.onkeydown = function(e) {
    if (e.key === "Escape") {
        resetSearch();
    }
};

DONE section-index.json

{{- $.Scratch.Add "index" slice -}}

{{- /* 只索引当前section的内容 */ -}}
{{- $pages := where .Site.RegularPages "Section" .Section -}}
{{- $pages = where $pages "Params.hiddenInList" "!=" true -}}

{{- range $pages -}}
    {{- $.Scratch.Add "index" (dict "title" .Title "permalink" .Permalink "summary" .Summary ) -}}
{{- end -}}

{{- $.Scratch.Get "index" | jsonify -}}

DONE 文章列表展示list.html

list.html:默认列表页面模板,显示文章摘要列表 list.json:JSON格式的文章列表输出

css


/* --- 瀑布流样式 --- */

/* 底部状态栏容器 */
.infinite-scroll-status {
    padding: 30px 0;
    text-align: center;
    width: 100%;
    min-height: 60px;
    display: flex;
    justify-content: center;
    align-items: center;
}

/* 加载中动画容器 */
.infinite-loader {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
    color: #666;
    font-size: 14px;
}

/* END 结束语 */
.infinite-end {
    /* 移除 border-top,避免出现双重横线 */
    /* border-top: 1px dashed #eee;  <-- 删掉了这一行 */

    color: #888; /* 使用中性灰,确保亮暗模式都看得清 */
    font-size: 14px;
    letter-spacing: 2px;
    font-weight: bold;

    width: 100%;
    padding-top: 20px;
    margin-top: 10px;

    /* 确保文字水平居中 */
    text-align: center;
}

/* 旋转动画 Spinner */
.spinner {
    width: 18px;
    height: 18px;
    border: 2px solid rgba(128, 128, 128, 0.2); /* 调整为中性色透明度,适配暗色 */
    border-radius: 50%;
    border-top-color: #888; /* 灰色主题,适配暗色模式 */
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* --- 性能优化核心 --- */
.post-entry {
    /* 1. 开启内容可见性优化 */
    content-visibility: auto;

    /* 2. 高度预估值 */
    contain-intrinsic-size: 150px;

    margin-bottom: 24px;

    /* 动画 */
    animation: fadeInUp 0.5s ease-out forwards;
    opacity: 0;
}

@keyframes fadeInUp {
    from { opacity: 0; transform: translateY(20px); }
    to   { opacity: 1; transform: translateY(0); }
}

list.html

{{- define "main" }}

<header class="page-header">
    <h1>{{ .Title }}</h1>
</header>

{{- /* --- 搜索框开始 --- */ -}}
<div id="searchbox" style="width: 100%; max-width: 600px; margin: 20px auto;">
    <input id="searchInput" placeholder="搜索 {{ .Title }} 文章..." aria-label="search" type="search" autocomplete="off" maxlength="64">
    <ul id="searchResults" aria-label="search results"></ul>
</div>

{{- /* 安全加载搜索脚本 (防止缺少文件报错) */ -}}
<link crossorigin="anonymous" rel="preload" as="fetch" href="index.json">
{{- $fusejs := resources.Get "js/fuse.basic.min.js" }}
{{- $searchScript := resources.Get "js/section-search.js" }}
{{- if and $fusejs $searchScript }}
    {{- $section_search := $searchScript | js.Build (dict "params" (dict "fuseOpts" site.Params.fuseOpts)) | resources.Minify }}
    {{- $search := (slice $fusejs $section_search) | resources.Concat "assets/js/main-search.js" | fingerprint }}
    <script defer crossorigin="anonymous" src="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}"></script>
{{- end }}
{{- /* --- 搜索框结束 --- */ -}}

{{- /* --- 准备文章数据 --- */ -}}
{{- $pages := .Pages }}
{{- $pages = where $pages "Params.hiddenInList" "!=" true }}
{{- if .IsHome }}
    {{- $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- end }}

{{- /* --- 分页器定义 (必须在所有 if 之外) --- */ -}}
{{- $paginator := .Paginate $pages }}

{{- /* --- 列表容器 --- */ -}}
<div id="post-list-container">
    {{- range $paginator.Pages }}
    <article class="post-entry">
        {{- /* 封面图逻辑 */ -}}
        {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
        {{- if not $isHidden }}
            {{- if .Params.cover.image }}
                {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" false) }}
            {{- else if .Params.images }}
                {{- with index .Params.images 0 }}
                <figure class="entry-cover">
                    <img src="{{ . | absURL }}" alt="{{ $.Title }}" loading="lazy">
                </figure>
                {{- end }}
            {{- end }}
        {{- end }}

        <header class="entry-header">
            <h2>{{- if .Params.recommend }}✦ {{ end -}}{{ .Title }}</h2>
        </header>

        {{- $scratch := newScratch }}


        {{- /* 作者 */ -}}
        {{- $author := (.Params.author | default site.Params.author) }}
        {{- $author_type := (printf "%T" $author) }}
        {{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
        {{- $author = (delimit $author ", ") }}
        {{- end }}
        {{- $scratch.Add "meta" (slice (printf "@ %s" $author)) }}

        {{- /* 阅读等级/时长 */ -}}
        {{- with .Params.level -}}
            {{- $levelStr := . | string -}}
            {{- if reflect.IsSlice . -}}
                {{- $levelStr = delimit . " " -}}
            {{- end -}}
            {{- $scratch.Add "meta" (slice (printf "[%s]" $levelStr)) -}}
        {{- end -}}

        {{- /* 更新时间 */ -}}
        {{- with .Lastmod }}
            {{- $scratch.Add "meta" (slice (printf "更新于 %s" (. | time.Format "2006年01月02日"))) }}
        {{- end }}

        <div class="entry-content" style="font-size: 1.2rem; margin-bottom: 0.8rem;">
            <p>{{ .Summary }}</p>
        </div>

        {{- /* 输出元信息行 */ -}}
        {{- with ($scratch.Get "meta") }}
            <div class="post-meta" style="font-size: 1.3rem; margin-bottom: 0.8rem; font-weight: 500;">
                {{ delimit . "&nbsp;·&nbsp;" | safeHTML }}
            </div>
        {{- end }}

        <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
    </article>
    {{- end }}
</div>

{{- /* --- 瀑布流底部状态栏 --- */ -}}
<div id="infinite-scroll-status" class="infinite-scroll-status">
    <div class="infinite-loader" style="display: none;">
        <span class="spinner"></span> 正在加载更多...
    </div>
    <div class="infinite-end" style="display: none;">
        ----------------- END -------------------
    </div>
</div>

{{- /* --- 隐藏的数据源:用于JS获取下一页链接 --- */ -}}
{{- /* 这里的 $paginator 必须能够被访问到 */ -}}
<div id="pagination-data" style="display: none;"
     data-has-next="{{ if $paginator.HasNext }}true{{ else }}false{{ end }}"
     data-next-url="{{ if $paginator.HasNext }}{{ $paginator.Next.URL }}{{ end }}">
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
    const container = document.getElementById('post-list-container');
    const statusDiv = document.getElementById('infinite-scroll-status');
    const loader = statusDiv.querySelector('.infinite-loader');
    const endMsg = statusDiv.querySelector('.infinite-end');
    const paginationData = document.getElementById('pagination-data');

    // 检查 paginationData 是否存在(防止页面无分页时报错)
    if (!paginationData) return;

    let nextUrl = paginationData.getAttribute('data-next-url');
    let hasNext = paginationData.getAttribute('data-has-next') === 'true';
    let isLoading = false;

    if (!hasNext) {
        endMsg.style.display = 'block';
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasNext && !isLoading) {
            loadNextPage();
        }
    }, {
        root: null,
        rootMargin: '200px',
        threshold: 0.1
    });

    observer.observe(statusDiv);

    async function loadNextPage() {
        if (isLoading || !nextUrl) return;

        isLoading = true;
        loader.style.display = 'flex';
        endMsg.style.display = 'none';

        try {
            const response = await fetch(nextUrl);
            if (response.ok) {
                const text = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');

                const newPosts = doc.querySelectorAll('#post-list-container .post-entry');

                newPosts.forEach(post => {
                    post.classList.add('fade-in-entry');
                    container.appendChild(post);
                });

                const newPaginationData = doc.getElementById('pagination-data');
                if (newPaginationData) {
                    hasNext = newPaginationData.getAttribute('data-has-next') === 'true';
                    nextUrl = newPaginationData.getAttribute('data-next-url');
                } else {
                    hasNext = false;
                }

                if (!hasNext) {
                    observer.disconnect();
                    loader.style.display = 'none';
                    endMsg.style.display = 'block';
                }
            }
        } catch (error) {
            console.error('Error loading next page:', error);
            loader.textContent = "加载失败,请刷新重试";
        } finally {
            isLoading = false;
            if (hasNext) {
                loader.style.display = 'none';
            }
        }
    }
});
</script>

{{- end }}

1_idea, 2_tool, 3_book, 4_math, 5_code, 6_nature

{{- define "main" }}

<header class="page-header">
    <h1>{{ .Title }}</h1>
</header>

{{- /* --- 搜索框开始 --- */ -}}
<div id="searchbox" style="width: 100%; max-width: 600px; margin: 20px auto;">
    <input id="searchInput" placeholder="搜索 {{ .Title }} 文章..." aria-label="search" type="search" autocomplete="off" maxlength="64">
    <ul id="searchResults" aria-label="search results"></ul>
</div>

{{- /* 安全加载搜索脚本 (防止缺少文件报错) */ -}}
<link crossorigin="anonymous" rel="preload" as="fetch" href="index.json">
{{- $fusejs := resources.Get "js/fuse.basic.min.js" }}
{{- $searchScript := resources.Get "js/section-search.js" }}
{{- if and $fusejs $searchScript }}
    {{- $section_search := $searchScript | js.Build (dict "params" (dict "fuseOpts" site.Params.fuseOpts)) | resources.Minify }}
    {{- $search := (slice $fusejs $section_search) | resources.Concat "assets/js/main-search.js" | fingerprint }}
    <script defer crossorigin="anonymous" src="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}"></script>
{{- end }}
{{- /* --- 搜索框结束 --- */ -}}

{{- /* --- 准备文章数据 --- */ -}}
{{- $pages := .Pages }}
{{- $pages = where $pages "Params.hiddenInList" "!=" true }}
{{- if .IsHome }}
    {{- $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- end }}

{{- /* --- 分页器定义 (必须在所有 if 之外) --- */ -}}
{{- $paginator := .Paginate $pages }}

{{- /* --- 列表容器 --- */ -}}
<div id="post-list-container">
    {{- range $paginator.Pages }}
    <article class="post-entry">
        {{- /* 封面图逻辑 */ -}}
        {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
        {{- if not $isHidden }}
            {{- if .Params.cover.image }}
                {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" false) }}
            {{- else if .Params.images }}
                {{- with index .Params.images 0 }}
                <figure class="entry-cover">
                    <img src="{{ . | absURL }}" alt="{{ $.Title }}" loading="lazy">
                </figure>
                {{- end }}
            {{- end }}
        {{- end }}

        <header class="entry-header">
            <h2>{{- if .Params.recommend }}✦ {{ end -}}{{ .Title }}</h2>
        </header>

        {{- $scratch := newScratch }}


        {{- /* 作者 */ -}}
        {{- $author := (.Params.author | default site.Params.author) }}
        {{- $author_type := (printf "%T" $author) }}
        {{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
        {{- $author = (delimit $author ", ") }}
        {{- end }}
        {{- $scratch.Add "meta" (slice (printf "@ %s" $author)) }}

        {{- /* 阅读等级/时长 */ -}}
        {{- with .Params.level -}}
            {{- $levelStr := . | string -}}
            {{- if reflect.IsSlice . -}}
                {{- $levelStr = delimit . " " -}}
            {{- end -}}
            {{- $scratch.Add "meta" (slice (printf "[%s]" $levelStr)) -}}
        {{- end -}}

        {{- /* 更新时间 */ -}}
        {{- with .Lastmod }}
            {{- $scratch.Add "meta" (slice (printf "更新于 %s" (. | time.Format "2006年01月02日"))) }}
        {{- end }}

        <div class="entry-content" style="font-size: 1.2rem; margin-bottom: 0.8rem;">
            <p>{{ .Summary }}</p>
        </div>

        {{- /* 输出元信息行 */ -}}
        {{- with ($scratch.Get "meta") }}
            <div class="post-meta" style="font-size: 1.3rem; margin-bottom: 0.8rem; font-weight: 500;">
                {{ delimit . "&nbsp;·&nbsp;" | safeHTML }}
            </div>
        {{- end }}

        <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
    </article>
    {{- end }}
</div>

{{- /* --- 瀑布流底部状态栏 --- */ -}}
<div id="infinite-scroll-status" class="infinite-scroll-status">
    <div class="infinite-loader" style="display: none;">
        <span class="spinner"></span> 正在加载更多...
    </div>
    <div class="infinite-end" style="display: none;">
        ----------------- END -------------------
    </div>
</div>

{{- /* --- 隐藏的数据源:用于JS获取下一页链接 --- */ -}}
{{- /* 这里的 $paginator 必须能够被访问到 */ -}}
<div id="pagination-data" style="display: none;"
     data-has-next="{{ if $paginator.HasNext }}true{{ else }}false{{ end }}"
     data-next-url="{{ if $paginator.HasNext }}{{ $paginator.Next.URL }}{{ end }}">
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
    const container = document.getElementById('post-list-container');
    const statusDiv = document.getElementById('infinite-scroll-status');
    const loader = statusDiv.querySelector('.infinite-loader');
    const endMsg = statusDiv.querySelector('.infinite-end');
    const paginationData = document.getElementById('pagination-data');

    // 检查 paginationData 是否存在(防止页面无分页时报错)
    if (!paginationData) return;

    let nextUrl = paginationData.getAttribute('data-next-url');
    let hasNext = paginationData.getAttribute('data-has-next') === 'true';
    let isLoading = false;

    if (!hasNext) {
        endMsg.style.display = 'block';
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasNext && !isLoading) {
            loadNextPage();
        }
    }, {
        root: null,
        rootMargin: '200px',
        threshold: 0.1
    });

    observer.observe(statusDiv);

    async function loadNextPage() {
        if (isLoading || !nextUrl) return;

        isLoading = true;
        loader.style.display = 'flex';
        endMsg.style.display = 'none';

        try {
            const response = await fetch(nextUrl);
            if (response.ok) {
                const text = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');

                const newPosts = doc.querySelectorAll('#post-list-container .post-entry');

                newPosts.forEach(post => {
                    post.classList.add('fade-in-entry');
                    container.appendChild(post);
                });

                const newPaginationData = doc.getElementById('pagination-data');
                if (newPaginationData) {
                    hasNext = newPaginationData.getAttribute('data-has-next') === 'true';
                    nextUrl = newPaginationData.getAttribute('data-next-url');
                } else {
                    hasNext = false;
                }

                if (!hasNext) {
                    observer.disconnect();
                    loader.style.display = 'none';
                    endMsg.style.display = 'block';
                }
            }
        } catch (error) {
            console.error('Error loading next page:', error);
            loader.textContent = "加载失败请刷新重试";
        } finally {
            isLoading = false;
            if (hasNext) {
                loader.style.display = 'none';
            }
        }
    }
});
</script>

{{- end }}
{{- define "main" }}

<header class="page-header">
    <h1>{{ .Title }}</h1>
</header>

{{- /* --- 搜索框开始 --- */ -}}
<div id="searchbox" style="width: 100%; max-width: 600px; margin: 20px auto;">
    <input id="searchInput" placeholder="搜索 {{ .Title }} 文章..." aria-label="search" type="search" autocomplete="off" maxlength="64">
    <ul id="searchResults" aria-label="search results"></ul>
</div>

{{- /* 安全加载搜索脚本 (防止缺少文件报错) */ -}}
<link crossorigin="anonymous" rel="preload" as="fetch" href="index.json">
{{- $fusejs := resources.Get "js/fuse.basic.min.js" }}
{{- $searchScript := resources.Get "js/section-search.js" }}
{{- if and $fusejs $searchScript }}
    {{- $section_search := $searchScript | js.Build (dict "params" (dict "fuseOpts" site.Params.fuseOpts)) | resources.Minify }}
    {{- $search := (slice $fusejs $section_search) | resources.Concat "assets/js/main-search.js" | fingerprint }}
    <script defer crossorigin="anonymous" src="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}"></script>
{{- end }}
{{- /* --- 搜索框结束 --- */ -}}

{{- /* --- 准备文章数据 --- */ -}}
{{- $pages := .Pages }}
{{- $pages = where $pages "Params.hiddenInList" "!=" true }}
{{- if .IsHome }}
    {{- $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- end }}

{{- /* --- 分页器定义 (必须在所有 if 之外) --- */ -}}
{{- $paginator := .Paginate $pages }}

{{- /* --- 列表容器 --- */ -}}
<div id="post-list-container">
    {{- range $paginator.Pages }}
    <article class="post-entry">
        {{- /* 封面图逻辑 */ -}}
        {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
        {{- if not $isHidden }}
            {{- if .Params.cover.image }}
                {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" false) }}
            {{- else if .Params.images }}
                {{- with index .Params.images 0 }}
                <figure class="entry-cover">
                    <img src="{{ . | absURL }}" alt="{{ $.Title }}" loading="lazy">
                </figure>
                {{- end }}
            {{- end }}
        {{- end }}

        <header class="entry-header">
            <h2>{{- if .Params.recommend }}✦ {{ end -}}{{ .Title }}</h2>
        </header>

        {{- $scratch := newScratch }}


        {{- /* 作者 */ -}}
        {{- $author := (.Params.author | default site.Params.author) }}
        {{- $author_type := (printf "%T" $author) }}
        {{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
        {{- $author = (delimit $author ", ") }}
        {{- end }}
        {{- $scratch.Add "meta" (slice (printf "@ %s" $author)) }}

        {{- /* 阅读等级/时长 */ -}}
        {{- with .Params.level -}}
            {{- $levelStr := . | string -}}
            {{- if reflect.IsSlice . -}}
                {{- $levelStr = delimit . " " -}}
            {{- end -}}
            {{- $scratch.Add "meta" (slice (printf "[%s]" $levelStr)) -}}
        {{- end -}}

        {{- /* 更新时间 */ -}}
        {{- with .Lastmod }}
            {{- $scratch.Add "meta" (slice (printf "更新于 %s" (. | time.Format "2006年01月02日"))) }}
        {{- end }}

        <div class="entry-content" style="font-size: 1.2rem; margin-bottom: 0.8rem;">
            <p>{{ .Summary }}</p>
        </div>

        {{- /* 输出元信息行 */ -}}
        {{- with ($scratch.Get "meta") }}
            <div class="post-meta" style="font-size: 1.3rem; margin-bottom: 0.8rem; font-weight: 500;">
                {{ delimit . "&nbsp;·&nbsp;" | safeHTML }}
            </div>
        {{- end }}

        <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
    </article>
    {{- end }}
</div>

{{- /* --- 瀑布流底部状态栏 --- */ -}}
<div id="infinite-scroll-status" class="infinite-scroll-status">
    <div class="infinite-loader" style="display: none;">
        <span class="spinner"></span> 正在加载更多...
    </div>
    <div class="infinite-end" style="display: none;">
        ----------------- END -------------------
    </div>
</div>

{{- /* --- 隐藏的数据源:用于JS获取下一页链接 --- */ -}}
{{- /* 这里的 $paginator 必须能够被访问到 */ -}}
<div id="pagination-data" style="display: none;"
     data-has-next="{{ if $paginator.HasNext }}true{{ else }}false{{ end }}"
     data-next-url="{{ if $paginator.HasNext }}{{ $paginator.Next.URL }}{{ end }}">
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
    const container = document.getElementById('post-list-container');
    const statusDiv = document.getElementById('infinite-scroll-status');
    const loader = statusDiv.querySelector('.infinite-loader');
    const endMsg = statusDiv.querySelector('.infinite-end');
    const paginationData = document.getElementById('pagination-data');

    // 检查 paginationData 是否存在(防止页面无分页时报错)
    if (!paginationData) return;

    let nextUrl = paginationData.getAttribute('data-next-url');
    let hasNext = paginationData.getAttribute('data-has-next') === 'true';
    let isLoading = false;

    if (!hasNext) {
        endMsg.style.display = 'block';
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasNext && !isLoading) {
            loadNextPage();
        }
    }, {
        root: null,
        rootMargin: '200px',
        threshold: 0.1
    });

    observer.observe(statusDiv);

    async function loadNextPage() {
        if (isLoading || !nextUrl) return;

        isLoading = true;
        loader.style.display = 'flex';
        endMsg.style.display = 'none';

        try {
            const response = await fetch(nextUrl);
            if (response.ok) {
                const text = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');

                const newPosts = doc.querySelectorAll('#post-list-container .post-entry');

                newPosts.forEach(post => {
                    post.classList.add('fade-in-entry');
                    container.appendChild(post);
                });

                const newPaginationData = doc.getElementById('pagination-data');
                if (newPaginationData) {
                    hasNext = newPaginationData.getAttribute('data-has-next') === 'true';
                    nextUrl = newPaginationData.getAttribute('data-next-url');
                } else {
                    hasNext = false;
                }

                if (!hasNext) {
                    observer.disconnect();
                    loader.style.display = 'none';
                    endMsg.style.display = 'block';
                }
            }
        } catch (error) {
            console.error('Error loading next page:', error);
            loader.textContent = "加载失败请刷新重试";
        } finally {
            isLoading = false;
            if (hasNext) {
                loader.style.display = 'none';
            }
        }
    }
});
</script>

{{- end }}
{{- define "main" }}

<header class="page-header">
    <h1>{{ .Title }}</h1>
</header>

{{- /* --- 搜索框开始 --- */ -}}
<div id="searchbox" style="width: 100%; max-width: 600px; margin: 20px auto;">
    <input id="searchInput" placeholder="搜索 {{ .Title }} 文章..." aria-label="search" type="search" autocomplete="off" maxlength="64">
    <ul id="searchResults" aria-label="search results"></ul>
</div>

{{- /* 安全加载搜索脚本 (防止缺少文件报错) */ -}}
<link crossorigin="anonymous" rel="preload" as="fetch" href="index.json">
{{- $fusejs := resources.Get "js/fuse.basic.min.js" }}
{{- $searchScript := resources.Get "js/section-search.js" }}
{{- if and $fusejs $searchScript }}
    {{- $section_search := $searchScript | js.Build (dict "params" (dict "fuseOpts" site.Params.fuseOpts)) | resources.Minify }}
    {{- $search := (slice $fusejs $section_search) | resources.Concat "assets/js/main-search.js" | fingerprint }}
    <script defer crossorigin="anonymous" src="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}"></script>
{{- end }}
{{- /* --- 搜索框结束 --- */ -}}

{{- /* --- 准备文章数据 --- */ -}}
{{- $pages := .Pages }}
{{- $pages = where $pages "Params.hiddenInList" "!=" true }}
{{- if .IsHome }}
    {{- $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- end }}

{{- /* --- 分页器定义 (必须在所有 if 之外) --- */ -}}
{{- $paginator := .Paginate $pages }}

{{- /* --- 列表容器 --- */ -}}
<div id="post-list-container">
    {{- range $paginator.Pages }}
    <article class="post-entry">
        {{- /* 封面图逻辑 */ -}}
        {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
        {{- if not $isHidden }}
            {{- if .Params.cover.image }}
                {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" false) }}
            {{- else if .Params.images }}
                {{- with index .Params.images 0 }}
                <figure class="entry-cover">
                    <img src="{{ . | absURL }}" alt="{{ $.Title }}" loading="lazy">
                </figure>
                {{- end }}
            {{- end }}
        {{- end }}

        <header class="entry-header">
            <h2>{{- if .Params.recommend }}✦ {{ end -}}{{ .Title }}</h2>
        </header>

        {{- $scratch := newScratch }}


        {{- /* 作者 */ -}}
        {{- $author := (.Params.author | default site.Params.author) }}
        {{- $author_type := (printf "%T" $author) }}
        {{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
        {{- $author = (delimit $author ", ") }}
        {{- end }}
        {{- $scratch.Add "meta" (slice (printf "@ %s" $author)) }}

        {{- /* 阅读等级/时长 */ -}}
        {{- with .Params.level -}}
            {{- $levelStr := . | string -}}
            {{- if reflect.IsSlice . -}}
                {{- $levelStr = delimit . " " -}}
            {{- end -}}
            {{- $scratch.Add "meta" (slice (printf "[%s]" $levelStr)) -}}
        {{- end -}}

        {{- /* 更新时间 */ -}}
        {{- with .Lastmod }}
            {{- $scratch.Add "meta" (slice (printf "更新于 %s" (. | time.Format "2006年01月02日"))) }}
        {{- end }}

        <div class="entry-content" style="font-size: 1.2rem; margin-bottom: 0.8rem;">
            <p>{{ .Summary }}</p>
        </div>

        {{- /* 输出元信息行 */ -}}
        {{- with ($scratch.Get "meta") }}
            <div class="post-meta" style="font-size: 1.3rem; margin-bottom: 0.8rem; font-weight: 500;">
                {{ delimit . "&nbsp;·&nbsp;" | safeHTML }}
            </div>
        {{- end }}

        <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
    </article>
    {{- end }}
</div>

{{- /* --- 瀑布流底部状态栏 --- */ -}}
<div id="infinite-scroll-status" class="infinite-scroll-status">
    <div class="infinite-loader" style="display: none;">
        <span class="spinner"></span> 正在加载更多...
    </div>
    <div class="infinite-end" style="display: none;">
        ----------------- END -------------------
    </div>
</div>

{{- /* --- 隐藏的数据源:用于JS获取下一页链接 --- */ -}}
{{- /* 这里的 $paginator 必须能够被访问到 */ -}}
<div id="pagination-data" style="display: none;"
     data-has-next="{{ if $paginator.HasNext }}true{{ else }}false{{ end }}"
     data-next-url="{{ if $paginator.HasNext }}{{ $paginator.Next.URL }}{{ end }}">
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
    const container = document.getElementById('post-list-container');
    const statusDiv = document.getElementById('infinite-scroll-status');
    const loader = statusDiv.querySelector('.infinite-loader');
    const endMsg = statusDiv.querySelector('.infinite-end');
    const paginationData = document.getElementById('pagination-data');

    // 检查 paginationData 是否存在(防止页面无分页时报错)
    if (!paginationData) return;

    let nextUrl = paginationData.getAttribute('data-next-url');
    let hasNext = paginationData.getAttribute('data-has-next') === 'true';
    let isLoading = false;

    if (!hasNext) {
        endMsg.style.display = 'block';
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasNext && !isLoading) {
            loadNextPage();
        }
    }, {
        root: null,
        rootMargin: '200px',
        threshold: 0.1
    });

    observer.observe(statusDiv);

    async function loadNextPage() {
        if (isLoading || !nextUrl) return;

        isLoading = true;
        loader.style.display = 'flex';
        endMsg.style.display = 'none';

        try {
            const response = await fetch(nextUrl);
            if (response.ok) {
                const text = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');

                const newPosts = doc.querySelectorAll('#post-list-container .post-entry');

                newPosts.forEach(post => {
                    post.classList.add('fade-in-entry');
                    container.appendChild(post);
                });

                const newPaginationData = doc.getElementById('pagination-data');
                if (newPaginationData) {
                    hasNext = newPaginationData.getAttribute('data-has-next') === 'true';
                    nextUrl = newPaginationData.getAttribute('data-next-url');
                } else {
                    hasNext = false;
                }

                if (!hasNext) {
                    observer.disconnect();
                    loader.style.display = 'none';
                    endMsg.style.display = 'block';
                }
            }
        } catch (error) {
            console.error('Error loading next page:', error);
            loader.textContent = "加载失败请刷新重试";
        } finally {
            isLoading = false;
            if (hasNext) {
                loader.style.display = 'none';
            }
        }
    }
});
</script>

{{- end }}
{{- define "main" }}

<header class="page-header">
    <h1>{{ .Title }}</h1>
</header>

{{- /* --- 搜索框开始 --- */ -}}
<div id="searchbox" style="width: 100%; max-width: 600px; margin: 20px auto;">
    <input id="searchInput" placeholder="搜索 {{ .Title }} 文章..." aria-label="search" type="search" autocomplete="off" maxlength="64">
    <ul id="searchResults" aria-label="search results"></ul>
</div>

{{- /* 安全加载搜索脚本 (防止缺少文件报错) */ -}}
<link crossorigin="anonymous" rel="preload" as="fetch" href="index.json">
{{- $fusejs := resources.Get "js/fuse.basic.min.js" }}
{{- $searchScript := resources.Get "js/section-search.js" }}
{{- if and $fusejs $searchScript }}
    {{- $section_search := $searchScript | js.Build (dict "params" (dict "fuseOpts" site.Params.fuseOpts)) | resources.Minify }}
    {{- $search := (slice $fusejs $section_search) | resources.Concat "assets/js/main-search.js" | fingerprint }}
    <script defer crossorigin="anonymous" src="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}"></script>
{{- end }}
{{- /* --- 搜索框结束 --- */ -}}

{{- /* --- 准备文章数据 --- */ -}}
{{- $pages := .Pages }}
{{- $pages = where $pages "Params.hiddenInList" "!=" true }}
{{- if .IsHome }}
    {{- $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- end }}

{{- /* --- 分页器定义 (必须在所有 if 之外) --- */ -}}
{{- $paginator := .Paginate $pages }}

{{- /* --- 列表容器 --- */ -}}
<div id="post-list-container">
    {{- range $paginator.Pages }}
    <article class="post-entry">
        {{- /* 封面图逻辑 */ -}}
        {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
        {{- if not $isHidden }}
            {{- if .Params.cover.image }}
                {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" false) }}
            {{- else if .Params.images }}
                {{- with index .Params.images 0 }}
                <figure class="entry-cover">
                    <img src="{{ . | absURL }}" alt="{{ $.Title }}" loading="lazy">
                </figure>
                {{- end }}
            {{- end }}
        {{- end }}

        <header class="entry-header">
            <h2>{{- if .Params.recommend }}✦ {{ end -}}{{ .Title }}</h2>
        </header>

        {{- $scratch := newScratch }}


        {{- /* 作者 */ -}}
        {{- $author := (.Params.author | default site.Params.author) }}
        {{- $author_type := (printf "%T" $author) }}
        {{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
        {{- $author = (delimit $author ", ") }}
        {{- end }}
        {{- $scratch.Add "meta" (slice (printf "@ %s" $author)) }}

        {{- /* 阅读等级/时长 */ -}}
        {{- with .Params.level -}}
            {{- $levelStr := . | string -}}
            {{- if reflect.IsSlice . -}}
                {{- $levelStr = delimit . " " -}}
            {{- end -}}
            {{- $scratch.Add "meta" (slice (printf "[%s]" $levelStr)) -}}
        {{- end -}}

        {{- /* 更新时间 */ -}}
        {{- with .Lastmod }}
            {{- $scratch.Add "meta" (slice (printf "更新于 %s" (. | time.Format "2006年01月02日"))) }}
        {{- end }}

        <div class="entry-content" style="font-size: 1.2rem; margin-bottom: 0.8rem;">
            <p>{{ .Summary }}</p>
        </div>

        {{- /* 输出元信息行 */ -}}
        {{- with ($scratch.Get "meta") }}
            <div class="post-meta" style="font-size: 1.3rem; margin-bottom: 0.8rem; font-weight: 500;">
                {{ delimit . "&nbsp;·&nbsp;" | safeHTML }}
            </div>
        {{- end }}

        <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
    </article>
    {{- end }}
</div>

{{- /* --- 瀑布流底部状态栏 --- */ -}}
<div id="infinite-scroll-status" class="infinite-scroll-status">
    <div class="infinite-loader" style="display: none;">
        <span class="spinner"></span> 正在加载更多...
    </div>
    <div class="infinite-end" style="display: none;">
        ----------------- END -------------------
    </div>
</div>

{{- /* --- 隐藏的数据源:用于JS获取下一页链接 --- */ -}}
{{- /* 这里的 $paginator 必须能够被访问到 */ -}}
<div id="pagination-data" style="display: none;"
     data-has-next="{{ if $paginator.HasNext }}true{{ else }}false{{ end }}"
     data-next-url="{{ if $paginator.HasNext }}{{ $paginator.Next.URL }}{{ end }}">
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
    const container = document.getElementById('post-list-container');
    const statusDiv = document.getElementById('infinite-scroll-status');
    const loader = statusDiv.querySelector('.infinite-loader');
    const endMsg = statusDiv.querySelector('.infinite-end');
    const paginationData = document.getElementById('pagination-data');

    // 检查 paginationData 是否存在(防止页面无分页时报错)
    if (!paginationData) return;

    let nextUrl = paginationData.getAttribute('data-next-url');
    let hasNext = paginationData.getAttribute('data-has-next') === 'true';
    let isLoading = false;

    if (!hasNext) {
        endMsg.style.display = 'block';
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasNext && !isLoading) {
            loadNextPage();
        }
    }, {
        root: null,
        rootMargin: '200px',
        threshold: 0.1
    });

    observer.observe(statusDiv);

    async function loadNextPage() {
        if (isLoading || !nextUrl) return;

        isLoading = true;
        loader.style.display = 'flex';
        endMsg.style.display = 'none';

        try {
            const response = await fetch(nextUrl);
            if (response.ok) {
                const text = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');

                const newPosts = doc.querySelectorAll('#post-list-container .post-entry');

                newPosts.forEach(post => {
                    post.classList.add('fade-in-entry');
                    container.appendChild(post);
                });

                const newPaginationData = doc.getElementById('pagination-data');
                if (newPaginationData) {
                    hasNext = newPaginationData.getAttribute('data-has-next') === 'true';
                    nextUrl = newPaginationData.getAttribute('data-next-url');
                } else {
                    hasNext = false;
                }

                if (!hasNext) {
                    observer.disconnect();
                    loader.style.display = 'none';
                    endMsg.style.display = 'block';
                }
            }
        } catch (error) {
            console.error('Error loading next page:', error);
            loader.textContent = "加载失败请刷新重试";
        } finally {
            isLoading = false;
            if (hasNext) {
                loader.style.display = 'none';
            }
        }
    }
});
</script>

{{- end }}
{{- define "main" }}

<header class="page-header">
    <h1>{{ .Title }}</h1>
</header>

{{- /* --- 搜索框开始 --- */ -}}
<div id="searchbox" style="width: 100%; max-width: 600px; margin: 20px auto;">
    <input id="searchInput" placeholder="搜索 {{ .Title }} 文章..." aria-label="search" type="search" autocomplete="off" maxlength="64">
    <ul id="searchResults" aria-label="search results"></ul>
</div>

{{- /* 安全加载搜索脚本 (防止缺少文件报错) */ -}}
<link crossorigin="anonymous" rel="preload" as="fetch" href="index.json">
{{- $fusejs := resources.Get "js/fuse.basic.min.js" }}
{{- $searchScript := resources.Get "js/section-search.js" }}
{{- if and $fusejs $searchScript }}
    {{- $section_search := $searchScript | js.Build (dict "params" (dict "fuseOpts" site.Params.fuseOpts)) | resources.Minify }}
    {{- $search := (slice $fusejs $section_search) | resources.Concat "assets/js/main-search.js" | fingerprint }}
    <script defer crossorigin="anonymous" src="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}"></script>
{{- end }}
{{- /* --- 搜索框结束 --- */ -}}

{{- /* --- 准备文章数据 --- */ -}}
{{- $pages := .Pages }}
{{- $pages = where $pages "Params.hiddenInList" "!=" true }}
{{- if .IsHome }}
    {{- $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- end }}

{{- /* --- 分页器定义 (必须在所有 if 之外) --- */ -}}
{{- $paginator := .Paginate $pages }}

{{- /* --- 列表容器 --- */ -}}
<div id="post-list-container">
    {{- range $paginator.Pages }}
    <article class="post-entry">
        {{- /* 封面图逻辑 */ -}}
        {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
        {{- if not $isHidden }}
            {{- if .Params.cover.image }}
                {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" false) }}
            {{- else if .Params.images }}
                {{- with index .Params.images 0 }}
                <figure class="entry-cover">
                    <img src="{{ . | absURL }}" alt="{{ $.Title }}" loading="lazy">
                </figure>
                {{- end }}
            {{- end }}
        {{- end }}

        <header class="entry-header">
            <h2>{{- if .Params.recommend }}✦ {{ end -}}{{ .Title }}</h2>
        </header>

        {{- $scratch := newScratch }}


        {{- /* 作者 */ -}}
        {{- $author := (.Params.author | default site.Params.author) }}
        {{- $author_type := (printf "%T" $author) }}
        {{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
        {{- $author = (delimit $author ", ") }}
        {{- end }}
        {{- $scratch.Add "meta" (slice (printf "@ %s" $author)) }}

        {{- /* 阅读等级/时长 */ -}}
        {{- with .Params.level -}}
            {{- $levelStr := . | string -}}
            {{- if reflect.IsSlice . -}}
                {{- $levelStr = delimit . " " -}}
            {{- end -}}
            {{- $scratch.Add "meta" (slice (printf "[%s]" $levelStr)) -}}
        {{- end -}}

        {{- /* 更新时间 */ -}}
        {{- with .Lastmod }}
            {{- $scratch.Add "meta" (slice (printf "更新于 %s" (. | time.Format "2006年01月02日"))) }}
        {{- end }}

        <div class="entry-content" style="font-size: 1.2rem; margin-bottom: 0.8rem;">
            <p>{{ .Summary }}</p>
        </div>

        {{- /* 输出元信息行 */ -}}
        {{- with ($scratch.Get "meta") }}
            <div class="post-meta" style="font-size: 1.3rem; margin-bottom: 0.8rem; font-weight: 500;">
                {{ delimit . "&nbsp;·&nbsp;" | safeHTML }}
            </div>
        {{- end }}

        <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
    </article>
    {{- end }}
</div>

{{- /* --- 瀑布流底部状态栏 --- */ -}}
<div id="infinite-scroll-status" class="infinite-scroll-status">
    <div class="infinite-loader" style="display: none;">
        <span class="spinner"></span> 正在加载更多...
    </div>
    <div class="infinite-end" style="display: none;">
        ----------------- END -------------------
    </div>
</div>

{{- /* --- 隐藏的数据源:用于JS获取下一页链接 --- */ -}}
{{- /* 这里的 $paginator 必须能够被访问到 */ -}}
<div id="pagination-data" style="display: none;"
     data-has-next="{{ if $paginator.HasNext }}true{{ else }}false{{ end }}"
     data-next-url="{{ if $paginator.HasNext }}{{ $paginator.Next.URL }}{{ end }}">
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
    const container = document.getElementById('post-list-container');
    const statusDiv = document.getElementById('infinite-scroll-status');
    const loader = statusDiv.querySelector('.infinite-loader');
    const endMsg = statusDiv.querySelector('.infinite-end');
    const paginationData = document.getElementById('pagination-data');

    // 检查 paginationData 是否存在(防止页面无分页时报错)
    if (!paginationData) return;

    let nextUrl = paginationData.getAttribute('data-next-url');
    let hasNext = paginationData.getAttribute('data-has-next') === 'true';
    let isLoading = false;

    if (!hasNext) {
        endMsg.style.display = 'block';
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasNext && !isLoading) {
            loadNextPage();
        }
    }, {
        root: null,
        rootMargin: '200px',
        threshold: 0.1
    });

    observer.observe(statusDiv);

    async function loadNextPage() {
        if (isLoading || !nextUrl) return;

        isLoading = true;
        loader.style.display = 'flex';
        endMsg.style.display = 'none';

        try {
            const response = await fetch(nextUrl);
            if (response.ok) {
                const text = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');

                const newPosts = doc.querySelectorAll('#post-list-container .post-entry');

                newPosts.forEach(post => {
                    post.classList.add('fade-in-entry');
                    container.appendChild(post);
                });

                const newPaginationData = doc.getElementById('pagination-data');
                if (newPaginationData) {
                    hasNext = newPaginationData.getAttribute('data-has-next') === 'true';
                    nextUrl = newPaginationData.getAttribute('data-next-url');
                } else {
                    hasNext = false;
                }

                if (!hasNext) {
                    observer.disconnect();
                    loader.style.display = 'none';
                    endMsg.style.display = 'block';
                }
            }
        } catch (error) {
            console.error('Error loading next page:', error);
            loader.textContent = "加载失败请刷新重试";
        } finally {
            isLoading = false;
            if (hasNext) {
                loader.style.display = 'none';
            }
        }
    }
});
</script>

{{- end }}
{{- define "main" }}

<header class="page-header">
    <h1>{{ .Title }}</h1>
</header>

{{- /* --- 搜索框开始 --- */ -}}
<div id="searchbox" style="width: 100%; max-width: 600px; margin: 20px auto;">
    <input id="searchInput" placeholder="搜索 {{ .Title }} 文章..." aria-label="search" type="search" autocomplete="off" maxlength="64">
    <ul id="searchResults" aria-label="search results"></ul>
</div>

{{- /* 安全加载搜索脚本 (防止缺少文件报错) */ -}}
<link crossorigin="anonymous" rel="preload" as="fetch" href="index.json">
{{- $fusejs := resources.Get "js/fuse.basic.min.js" }}
{{- $searchScript := resources.Get "js/section-search.js" }}
{{- if and $fusejs $searchScript }}
    {{- $section_search := $searchScript | js.Build (dict "params" (dict "fuseOpts" site.Params.fuseOpts)) | resources.Minify }}
    {{- $search := (slice $fusejs $section_search) | resources.Concat "assets/js/main-search.js" | fingerprint }}
    <script defer crossorigin="anonymous" src="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}"></script>
{{- end }}
{{- /* --- 搜索框结束 --- */ -}}

{{- /* --- 准备文章数据 --- */ -}}
{{- $pages := .Pages }}
{{- $pages = where $pages "Params.hiddenInList" "!=" true }}
{{- if .IsHome }}
    {{- $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- end }}

{{- /* --- 分页器定义 (必须在所有 if 之外) --- */ -}}
{{- $paginator := .Paginate $pages }}

{{- /* --- 列表容器 --- */ -}}
<div id="post-list-container">
    {{- range $paginator.Pages }}
    <article class="post-entry">
        {{- /* 封面图逻辑 */ -}}
        {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
        {{- if not $isHidden }}
            {{- if .Params.cover.image }}
                {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" false) }}
            {{- else if .Params.images }}
                {{- with index .Params.images 0 }}
                <figure class="entry-cover">
                    <img src="{{ . | absURL }}" alt="{{ $.Title }}" loading="lazy">
                </figure>
                {{- end }}
            {{- end }}
        {{- end }}

        <header class="entry-header">
            <h2>{{- if .Params.recommend }}✦ {{ end -}}{{ .Title }}</h2>
        </header>

        {{- $scratch := newScratch }}


        {{- /* 作者 */ -}}
        {{- $author := (.Params.author | default site.Params.author) }}
        {{- $author_type := (printf "%T" $author) }}
        {{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
        {{- $author = (delimit $author ", ") }}
        {{- end }}
        {{- $scratch.Add "meta" (slice (printf "@ %s" $author)) }}

        {{- /* 阅读等级/时长 */ -}}
        {{- with .Params.level -}}
            {{- $levelStr := . | string -}}
            {{- if reflect.IsSlice . -}}
                {{- $levelStr = delimit . " " -}}
            {{- end -}}
            {{- $scratch.Add "meta" (slice (printf "[%s]" $levelStr)) -}}
        {{- end -}}

        {{- /* 更新时间 */ -}}
        {{- with .Lastmod }}
            {{- $scratch.Add "meta" (slice (printf "更新于 %s" (. | time.Format "2006年01月02日"))) }}
        {{- end }}

        <div class="entry-content" style="font-size: 1.2rem; margin-bottom: 0.8rem;">
            <p>{{ .Summary }}</p>
        </div>

        {{- /* 输出元信息行 */ -}}
        {{- with ($scratch.Get "meta") }}
            <div class="post-meta" style="font-size: 1.3rem; margin-bottom: 0.8rem; font-weight: 500;">
                {{ delimit . "&nbsp;·&nbsp;" | safeHTML }}
            </div>
        {{- end }}

        <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
    </article>
    {{- end }}
</div>

{{- /* --- 瀑布流底部状态栏 --- */ -}}
<div id="infinite-scroll-status" class="infinite-scroll-status">
    <div class="infinite-loader" style="display: none;">
        <span class="spinner"></span> 正在加载更多...
    </div>
    <div class="infinite-end" style="display: none;">
        ----------------- END -------------------
    </div>
</div>

{{- /* --- 隐藏的数据源:用于JS获取下一页链接 --- */ -}}
{{- /* 这里的 $paginator 必须能够被访问到 */ -}}
<div id="pagination-data" style="display: none;"
     data-has-next="{{ if $paginator.HasNext }}true{{ else }}false{{ end }}"
     data-next-url="{{ if $paginator.HasNext }}{{ $paginator.Next.URL }}{{ end }}">
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
    const container = document.getElementById('post-list-container');
    const statusDiv = document.getElementById('infinite-scroll-status');
    const loader = statusDiv.querySelector('.infinite-loader');
    const endMsg = statusDiv.querySelector('.infinite-end');
    const paginationData = document.getElementById('pagination-data');

    // 检查 paginationData 是否存在(防止页面无分页时报错)
    if (!paginationData) return;

    let nextUrl = paginationData.getAttribute('data-next-url');
    let hasNext = paginationData.getAttribute('data-has-next') === 'true';
    let isLoading = false;

    if (!hasNext) {
        endMsg.style.display = 'block';
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasNext && !isLoading) {
            loadNextPage();
        }
    }, {
        root: null,
        rootMargin: '200px',
        threshold: 0.1
    });

    observer.observe(statusDiv);

    async function loadNextPage() {
        if (isLoading || !nextUrl) return;

        isLoading = true;
        loader.style.display = 'flex';
        endMsg.style.display = 'none';

        try {
            const response = await fetch(nextUrl);
            if (response.ok) {
                const text = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');

                const newPosts = doc.querySelectorAll('#post-list-container .post-entry');

                newPosts.forEach(post => {
                    post.classList.add('fade-in-entry');
                    container.appendChild(post);
                });

                const newPaginationData = doc.getElementById('pagination-data');
                if (newPaginationData) {
                    hasNext = newPaginationData.getAttribute('data-has-next') === 'true';
                    nextUrl = newPaginationData.getAttribute('data-next-url');
                } else {
                    hasNext = false;
                }

                if (!hasNext) {
                    observer.disconnect();
                    loader.style.display = 'none';
                    endMsg.style.display = 'block';
                }
            }
        } catch (error) {
            console.error('Error loading next page:', error);
            loader.textContent = "加载失败请刷新重试";
        } finally {
            isLoading = false;
            if (hasNext) {
                loader.style.display = 'none';
            }
        }
    }
});
</script>

{{- end }}

DONE list.json

{{/* layouts/_default/list.json */}}
{{- $.Scratch.Add "index" slice -}}

{{- $sectionPages := where site.RegularPages "Section" .Section -}}
{{- $pagesToIndex := where $sectionPages "Params.hiddenInList" "!=" true -}}

{{- range $pagesToIndex -}}

{{- $cover_image := "" -}}
{{- with .Params.cover.image -}}
    {{- $cover_image = . | absURL -}}
{{- else with .Params.images -}}
    {{- if gt (len .) 0 -}}
        {{- $cover_image = (index . 0) | absURL -}}
    {{- end -}}
{{- end -}}

{{- $.Scratch.Add "index" (dict
        "title" .Title
        "permalink" .Permalink
        "date" (.Date.Format "2006-01-02")
        "summary" .Summary
        "cover_image" $cover_image
    ) -}}
{{- end -}}

{{- $.Scratch.Get "index" | jsonify -}}

DONE index.json

{{- $.Scratch.Add "index" slice -}}

{{- /* 1. 定义要被索引的页面范围:主内容区的文章 */ -}}
{{- $pages := where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- $pagesToIndex := where $pages "Params.hiddenInList" "!=" true }}

{{- /* 2. 遍历每一个页面,提取所需数据 */ -}}
{{- range $pagesToIndex -}}

{{- $cover_image := "" -}}
{{- with .Params.cover.image -}}
    {{- $cover_image = . | absURL -}}
{{- else with .Params.images -}}
    {{- if gt (len .) 0 -}}
        {{- $cover_image = (index . 0) | absURL -}}
    {{- end -}}
{{- end -}}



{{- $.Scratch.Add "index" (dict
    "title" .Title
    "permalink" .Permalink
    "date" (.Date.Format "2006-01-02")
    "summary" .Summary
    "cover_image" $cover_image
) -}}
{{- end -}}

{{- /* 4. 将最终的索引数据以 JSON 格式输出 */ -}}
{{- $.Scratch.Get "index" | jsonify -}}

DONE info/ 拒绝访问

{{- define "main" }}
<div class="post-content">
  <h1>Access Denied</h1>
  <p>You don't have permission to view this content.</p>
  <p>你没有访问权限</p>
</div>
{{- end }}

DONE 文章元数据

{{- $scratch := newScratch }}
<!-- 作者 - 单独一行 -->
{{- if not (.Param "hideAuthor") -}}
{{- with (partial "author.html" .) }}
<div class="post-author" style="font-size: 1.3rem; margin-bottom: 0.8rem; font-weight: 500;">
  <span>@ {{- . -}}</span>
</div>
{{- end }}
{{- end }}
<!-- 阅读时间 -->
{{- /* 替换原有的 ShowReadingTime 逻辑 */ -}}
{{- with .Params.level -}}
  {{- $levelStr := . | string -}}
  {{- if reflect.IsSlice . -}}
    {{- $levelStr = delimit . " " -}}
  {{- end -}}
  {{- $scratch.Add "meta" (slice (printf "[%s]" $levelStr)) -}}
{{- end -}}

<!-- 发布日期 -->
{{- if not .Date.IsZero -}}
{{- $scratch.Add "meta" (slice (printf "发布于 %s" (.Date | time.Format (default ":date_long" site.Params.DateFormat)))) }}
{{- end }}

<!-- 更新日期 -->
{{- with .Lastmod }}
{{- $scratch.Add "meta" (slice (printf "更新于 %s" (. | time.Format "2006年01月02日"))) }}
{{- end }}

<!-- 知识许可 -->


{{- $licenseCode := .Params.licenseCode | default site.Params.licenseCode }}
{{- $license := .Params.license }}
{{- $licenseInfo := dict }}
{{- if $license }}
  {{- $licenseInfo = dict "name" $license "url" "" }}
{{- else }}
  {{- $licenseInfo = index site.Params.licenseMap $licenseCode | default (index site.Params.licenseMap (string site.Params.licenseCode)) }}
{{- end }}
{{- if $licenseInfo.name }}
  {{- if $licenseInfo.url }}
    {{- $scratch.Add "meta" (slice (printf "<a href=\"%s\" rel=\"license\" target=\"_blank\" rel=\"noopener\">%s</a>" $licenseInfo.url $licenseInfo.name)) }}
  {{- else }}
    {{- $scratch.Add "meta" (slice $licenseInfo.name) }}
  {{- end }}
{{- end }}

<!-- 元信息 - 单独一行 -->
{{- with ($scratch.Get "meta") }}
<div class="post-meta">
  {{- delimit . " · " | safeHTML -}}
</div>
{{- end -}}

DONE 文章页面 singlehtml

{{- define "main" }}

<article class="post-single">
  <!-- ... 这里的 header, content, footer 等内容保持不变 ... -->
  <header class="post-header">
    {{ partial "breadcrumbs.html" . }}
    <h1 class="post-title entry-hint-parent">
      {{- if .Params.recommend }}✦ {{ end -}} {{ .Title }}

      {{- if .Draft }}
      <span class="entry-hint" title="Draft">
        <svg xmlns="http://www.w3.org/2000/svg" height="35" viewBox="0 -960 960 960" fill="currentColor">
          <path
            d="M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z" />
        </svg>
      </span>
      {{- end }}
    </h1>
{{- if not (.Param "hideMeta") }}
<div class="post-meta">
  {{- partial "post_meta.html" . -}}
  {{- partial "translation_list.html" . -}}
  {{- partial "post_canonical.html" . -}}
</div>
{{- end }}
    {{- if .Description }}
    <div class="post-description">
      {{ .Description }}
    </div>
    {{- end }}

  </header>
  {{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }}
  {{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }}

  {{- if .Content }}
  <div class="post-content">
    {{- if not (.Param "disableAnchoredHeadings") }}
    {{- partial "anchored_headings.html" .Content -}}
    {{- else }}{{ .Content }}{{ end }}
  </div>
  {{- end }}

  <footer class="post-footer">

    {{- $categories := .Language.Params.Taxonomies.category | default "categories" }}
    {{- with ($.GetTerms $categories) }}
    <ul class="post-tags">
      {{- range . }}
      <li><a href="{{ .Permalink }}"><span style="font-weight:bold;font-size:1.3em;">{</span>  {{ .LinkTitle }}  <span style="font-weight:bold;font-size:1.3em;">}</span></a></li>
      {{- end }}
    </ul>
    {{- end }}

    {{- $tags := .Language.Params.Taxonomies.tag | default "tags" }}
    <ul class="post-tags">
      {{- range ($.GetTerms $tags) }}
      <li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
      {{- end }}

      {{- /* 获取作者信息 */ -}}
      {{- $authorId := .Params.author | default "picatria" -}}
      {{- $authorData := index .Site.Data.authors $authorId -}}
      {{- $authorContact := "#" -}}
      {{- if $authorData -}}
        {{- $authorContact = $authorData.contact | default "#" -}}
      {{- end -}}

      {{- /* <li><a href="#" id="share-button" class="action-tag" title="生成图片">分享</a></li>*/ -}}
      <li><a href="#" id="comment-button" class="action-tag" title="给我留言" data-author-contact="{{ $authorContact }}">留言</a></li>
    </ul>
    {{- if (.Param "ShowPostNavLinks") }}
    {{- partial "post_nav_links.html" . }}
    {{- end }}
    {{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }}
    {{- partial "share_icons.html" . -}}
    {{- end }}
  </footer>

  {{- if (.Param "comments") }}
  {{- partial "comments.html" . }}
  {{- end }}
</article>

{{- /* 留言弹窗 HTML 结构 */ -}}
<div id="comment-modal" class="modal-overlay">
    <div class="modal-content">
        <span class="modal-close" id="modal-close-btn">&times;</span>
        <h3>📝 匿名留言</h3>
        <div id="comment-form-section">
            <textarea id="comment-textarea" placeholder="请在此输入留言内容 (200字以内)" maxlength="200"></textarea>
            <div class="captcha-section">
                <label for="captcha-input">请计算:</label>
                <span id="captcha-question">加载中...</span>
                <input type="number" id="captcha-input" placeholder="结果">
            </div>
            <button id="comment-submit-btn" class="modal-btn">确认提交</button>
        </div>
        <div id="comment-status"></div>
        <div id="comment-success-message" style="display: none;">
            <p>留言已收到,感谢您的关注与留言。</p>
            <p>请注意:本留言通道为匿名投递,无法直接回复。</p>
            <p>若您的留言包含问题或需与作者进一步交流,请通过作者预留的联系方式 <a id="author-contact-link" href="#">[点击此处]</a> 与其联系。</p>
        </div>
    </div>
</div>

{{- /* ================================================================= */ -}}
{{- /* ⚠️ 核心修复:引入 JavaScript 文件 */ -}}
{{- /* 这一步至关重要,它将 assets/js/comment.js 编译并插入页面 */ -}}
{{- /* ================================================================= */ -}}
{{- $commentJS := resources.Get "js/comment.js" | resources.Minify | resources.Fingerprint }}
{{- if $commentJS }}
    <script src="{{ $commentJS.Permalink }}" defer></script>
{{- else }}
    {{- /* 如果文件找不到,在构建时报错提示 */ -}}
    {{- errorf "❌ 错误: 未找到 assets/js/comment.js,请确认文件路径正确" -}}
{{- end }}

{{- end }}{{/* end main */}}

DONE 3_book 不显示标题

{{- define "main" }}

<article class="post-single">
  <!-- ... 这里的 header, content, footer 等内容保持不变 ... -->
  <header class="post-header">
    {{ partial "breadcrumbs.html" . }}

    {{- if not (.Param "hideMeta") }}
    <div class="post-meta">
      {{- partial "post_meta.html" . -}}
      {{- partial "translation_list.html" . -}}
      {{- partial "post_canonical.html" . -}}
    </div>
    {{- end }}

    {{- if .Description }}
    <div class="post-description">
      {{ .Description }}
    </div>
    {{- end }}
  </header>
  {{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }}
  {{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }}

  {{- if .Content }}
  <div class="post-content">
    {{- if not (.Param "disableAnchoredHeadings") }}
    {{- partial "anchored_headings.html" .Content -}}
    {{- else }}{{ .Content }}{{ end }}
  </div>
  {{- end }}

  <footer class="post-footer">
    {{- $tags := .Language.Params.Taxonomies.tag | default "tags" }}
    <ul class="post-tags">
      {{- range ($.GetTerms $tags) }}
      <li><a href="{{ .Permalink }}">{{ .LinkTitle | lower }}</a></li>
      {{- end }}

      {{- /* 获取作者信息 */ -}}
      {{- $authorId := .Params.author | default "picatria" -}}
      {{- $authorData := index .Site.Data.authors $authorId -}}
      {{- $authorContact := "#" -}}
      {{- if $authorData -}}
      {{- $authorContact = $authorData.contact | default "#" -}}
      {{- end -}}

       {{- /*<li><a href="#" id="share-button" class="action-tag" title="生成图片">分享</a></li>*/ -}}

      <li><a href="#" id="comment-button" class="action-tag" title="给我留言" data-author-contact="{{ $authorContact }}">留言</a></li>
    </ul>
    {{- if (.Param "ShowPostNavLinks") }}
    {{- partial "post_nav_links.html" . }}
    {{- end }}
    {{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }}
    {{- partial "share_icons.html" . -}}
    {{- end }}
  </footer>

  {{- if (.Param "comments") }}
  {{- partial "comments.html" . }}
  {{- end }}
</article>

{{- /* 留言弹窗 HTML 结构 */ -}}
<div id="comment-modal" class="modal-overlay">
  <div class="modal-content">
    <span class="modal-close" id="modal-close-btn">&times;</span>
    <h3>📝 匿名留言</h3>
    <div id="comment-form-section">
      <textarea id="comment-textarea" placeholder="请在此输入留言内容 (200字以内)" maxlength="200"></textarea>
      <div class="captcha-section">
        <label for="captcha-input">请计算:</label>
        <span id="captcha-question">加载中...</span>
        <input type="number" id="captcha-input" placeholder="结果">
      </div>
      <button id="comment-submit-btn" class="modal-btn">确认提交</button>
    </div>
    <div id="comment-status"></div>
    <div id="comment-success-message" style="display: none;">
      <p>留言已收到,感谢您的关注与留言。</p>
      <p>请注意:本留言通道为匿名投递,无法直接回复。</p>
      <p>若您的留言包含问题或需与作者进一步交流,请通过作者预留的联系方式 <a id="author-contact-link" href="#">[点击此处]</a> 与其联系。</p>
    </div>
  </div>
</div>

{{- /* ================================================================= */ -}}
{{- /* ⚠️ 核心修复:引入 JavaScript 文件 */ -}}
{{- /* 这一步至关重要,它将 assets/js/comment.js 编译并插入页面 */ -}}
{{- /* ================================================================= */ -}}
{{- $commentJS := resources.Get "js/comment.js" | resources.Minify | resources.Fingerprint }}
{{- if $commentJS }}
<script src="{{ $commentJS.Permalink }}" defer></script>
{{- else }}
{{- /* 如果文件找不到,在构建时报错提示 */ -}}
{{- errorf "❌ 错误: 未找到 assets/js/comment.js,请确认文件路径正确" -}}
{{- end }}

{{- end }}{{/* end main */}}

{{- define "main" }}

<article class="post-single">
  <!-- ... 这里的 header, content, footer 等内容保持不变 ... -->
  <header class="post-header">
    {{ partial "breadcrumbs.html" . }}

    {{- if not (.Param "hideMeta") }}
    <div class="post-meta">
      {{- partial "post_meta.html" . -}}
      {{- partial "translation_list.html" . -}}
      {{- partial "post_canonical.html" . -}}
    </div>
    {{- end }}

    {{- if .Description }}
    <div class="post-description">
      {{ .Description }}
    </div>
    {{- end }}
  </header>
  {{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }}
  {{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }}

  {{- if .Content }}
  <div class="post-content">
    {{- if not (.Param "disableAnchoredHeadings") }}
    {{- partial "anchored_headings.html" .Content -}}
    {{- else }}{{ .Content }}{{ end }}
  </div>
  {{- end }}

  <footer class="post-footer">
    {{- $tags := .Language.Params.Taxonomies.tag | default "tags" }}
    <ul class="post-tags">
      {{- range ($.GetTerms $tags) }}
      <li><a href="{{ .Permalink }}">{{ .LinkTitle | lower }}</a></li>
      {{- end }}

      {{- /* 获取作者信息 */ -}}
      {{- $authorId := .Params.author | default "picatria" -}}
      {{- $authorData := index .Site.Data.authors $authorId -}}
      {{- $authorContact := "#" -}}
      {{- if $authorData -}}
      {{- $authorContact = $authorData.contact | default "#" -}}
      {{- end -}}

       {{- /*<li><a href="#" id="share-button" class="action-tag" title="生成图片">分享</a></li>*/ -}}

      <li><a href="#" id="comment-button" class="action-tag" title="给我留言" data-author-contact="{{ $authorContact }}">留言</a></li>
    </ul>
    {{- if (.Param "ShowPostNavLinks") }}
    {{- partial "post_nav_links.html" . }}
    {{- end }}
    {{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }}
    {{- partial "share_icons.html" . -}}
    {{- end }}
  </footer>

  {{- if (.Param "comments") }}
  {{- partial "comments.html" . }}
  {{- end }}
</article>

{{- /* 留言弹窗 HTML 结构 */ -}}
<div id="comment-modal" class="modal-overlay">
  <div class="modal-content">
    <span class="modal-close" id="modal-close-btn">&times;</span>
    <h3>📝 匿名留言</h3>
    <div id="comment-form-section">
      <textarea id="comment-textarea" placeholder="请在此输入留言内容 (200字以内)" maxlength="200"></textarea>
      <div class="captcha-section">
        <label for="captcha-input">请计算:</label>
        <span id="captcha-question">加载中...</span>
        <input type="number" id="captcha-input" placeholder="结果">
      </div>
      <button id="comment-submit-btn" class="modal-btn">确认提交</button>
    </div>
    <div id="comment-status"></div>
    <div id="comment-success-message" style="display: none;">
      <p>留言已收到,感谢您的关注与留言。</p>
      <p>请注意:本留言通道为匿名投递,无法直接回复。</p>
      <p>若您的留言包含问题或需与作者进一步交流,请通过作者预留的联系方式 <a id="author-contact-link" href="#">[点击此处]</a> 与其联系。</p>
    </div>
  </div>
</div>

{{- /* ================================================================= */ -}}
{{- /* ⚠️ 核心修复:引入 JavaScript 文件 */ -}}
{{- /* 这一步至关重要,它将 assets/js/comment.js 编译并插入页面 */ -}}
{{- /* ================================================================= */ -}}
{{- $commentJS := resources.Get "js/comment.js" | resources.Minify | resources.Fingerprint }}
{{- if $commentJS }}
<script src="{{ $commentJS.Permalink }}" defer></script>
{{- else }}
{{- /* 如果文件找不到,在构建时报错提示 */ -}}
{{- errorf "❌ 错误: 未找到 assets/js/comment.js,请确认文件路径正确" -}}
{{- end }}

{{- end }}{{/* end main */}}

DONE 书籍信息的呈现

bookcard

{{ $color := .Get "color" }}
{{ if $color }}
<div class="card-container" style="--book-color: {{ $color }};">
{{ else }}
<div class="card-container">
  {{end}}
    <div class="title-area">
        <h1 class="book-title">{{ .Get "title" }}</h1>
    </div>
    <div class="separator-line"></div>
    <div class="main-body">
        <div class="left-gutter">

<div class="bookmark-strip">
    <div class="bookmark-lines"></div>
    <div class="bookmark-tail">
        <div class="w-line left"></div>
        <div class="w-line right"></div>
    </div>
</div>

        </div>
        <div class="right-content">
            <div class="book-subtitle">{{ .Get "subtitle" }}</div>
            <div class="info-list">
                <div class="info-row"><span class="label">作者</span><span class="value">{{ .Get "author" }}</span></div>

                <div class="info-row"><span class="label">ISBN</span><span class="value">{{ .Get "isbn" }}</span></div>

                <div class="info-row"><span class="label">ISBN</span><span class="value">{{ .Get "date" }}</span></div>

                <div class="info-row highlight-rating"><span class="label">可信度</span><span class="value">{{ .Get "trust" }}</span></div>

                <div class="info-row highlight-trust"><span class="label">评分</span><span class="value">{{ .Get "rating" }}/10</span></div>

                <div class="info-row indent-for-btn sub-info"><span clas="label">评价</span><span class="value">{{ .Get "reviews" }}人 </span></div>

                <div class="info-row indent-for-btn sub-info"><span clas="label">书评</span><span class="value"> {{
                .Get "comments" }}条</span></div>


            </div>
            {{ if .Get "douban_url" }}<a href="{{ .Get "douban_url" }}" class="douban-btn">豆瓣</a>{{ end }}
        </div>
    </div>
</div>

extend_head

<style>
/* ==========================================================================
   Book Card Styles (Updated and Theme-Aware)
   ========================================================================== */
:root {
    --bookmark-height: 256px;
    --btn-bottom-offset: 9px;
    /* 默认颜色变量, 会被 JS 的随机颜色覆盖 */
    --bookmark-color: var(--fold-h3);
}

.card-container {
    width: 100%;
    max-width: 700px;
    padding: 20px;
    margin: 20px auto;
    /* 5. 亮色/暗色主题适配: 使用主题变量 */
    background: var(--theme);
    /* border: 1px solid var(--border); */
    /* box-shadow: 0 4px 12px rgba(0,0,0,0.05); */
    /* 为变量变化添加过渡效果 */
    /* transition: background 0.3s, border-color 0.3s; */
}

/* 4. 书名和横线间距缩写 */
.title-area {
    text-align: right;
    /* 从 5px 减小到 2px */
    padding-bottom: 2px;
}

.book-title {
    font-size: 2.2rem;
    font-weight: bold;
    /* 5. 亮色/暗色主题适配: 使用主题变量 */
    color: var(--primary);
    line-height: 0;
    margin: 0;
}

.separator-line {
    width: 100%;
    height: 3px;
    background-color: var(--content);
    margin: 0;
    /* display: block; */
}

.main-body {
    display: flex;
    position: relative;
    margin-top: 0;
}

/*左侧信息栏的位置 */
.left-gutter {
    width: 20%;
    flex-shrink: 0;
    position: relative;
}

/*书签的位置*/
.bookmark-strip {
    position: absolute;
    top: 0;
    left: 45%;
    width: 16px;
    height: var(--bookmark-height);
    display: flex;
    flex-direction: column;
}

.bookmark-lines {
    flex-grow: 1;
    width: 100%;
    /* 1. 随机颜色: 使用 --bookmark-color 变量 */
    border-left: 2px solid var(--book-color) !important;
    border-right: 2px solid var(--book-color) !important;
    /* 5. 亮色/暗色主题适配: 背景与卡片一致 */
    background: var(--theme);
}

.bookmark-tail {
    width: 100%;
    height: 12px;
    display: block;
    flex-shrink: 0;
    margin-top: -2px;
}

.bookmark-tail path {
    /* 1. 随机颜色: 使用 --bookmark-color 变量 */
    stroke: var(--book-color) !important;
}

.right-content {
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    position: relative;
    min-height: 200px;
}

.book-subtitle {
    text-align: right;
    font-size: 0.9rem;
    /* 5. 亮色/暗色主题适配: 使用主题变量 */
    color: var(--secondary);
    margin-top: 8px;
    margin-bottom: 20px;
    font-family: sans-serif;
    font-weight: normal;
}

.book-subtitle:empty {
    display: none;
}

/* 3. 作者和横线(分隔线)在没有副标题的情况下, 间距要增大 */
.book-subtitle:empty + .info-list {
    /* 当副标题为空时, 为信息列表添加上边距, 弥补副标题消失的间距 */
    /* 8px (原 subtitle margin-top) + 20px (原 subtitle margin-bottom) */
    margin-top: 16px;
}

.info-list {
    width: 90%;
}

.info-row {
    display: flex;
    justify-content: space-between;
    margin-bottom: 12px;
    font-size: 15px;
    line-height: 1.6;
}

.indent-for-btn {
    padding-right: 55px;
}

.label {
    font-weight: bold;
    /* 5. 亮色/暗色主题适配: 使用主题变量 */
    color: var(--secondary);
    flex-shrink: 0;
}

.value {
    text-align: right;
    /* 5. 亮色/暗色主题适配: 使用主题变量 */
    color: var(--content);
}


/*可信度和评分交换了位置, 偷懒没改*/
.highlight-rating .value {
    color: var(--book-color);
    font-weight: 800; }
.highlight-trust .value { color: #2a9d8f; }


/* 2. 豆瓣的颜色要跟主要字体的颜色一样 (此处统一为随机主题色) */

.douban-btn {
    position: absolute;
    right: 10%;
    bottom: var(--btn-bottom-offset);

    display: block;
    width: 40px;
    padding: 10px 5px;

    border: 3px solid var(--bookmark-color);
    background: var(--entry);
    color: var(--bookmark-color);

    writing-mode: vertical-rl;
    text-orientation: upright;

    font-weight: bold;
    font-size: 14px;
    letter-spacing: 4px;
    text-decoration: none;

    z-index: 2;
}

/* L 形阴影(连续、直角) */
.douban-btn::after {
    content: "";
    position: absolute;

    /* 从右下角“伸出去” */
    left: 0px;
    top: 0px;

    width: calc(100% + 7px);
    height: calc(100% + 7px);

    /* 只画右边和下边 */
    border-right: 6px solid color-mix(in srgb, var(--primary) 18%, transparent);
    border-bottom: 6px solid color-mix(in srgb, var(--primary) 18%, transparent);



    pointer-events: none;
    z-index: -1;
}


/* hover:像被抬起 */
.douban-btn:hover {
    transform: translate(-3px, -4px);
}


.douban-btn:hover::after {
    filter: blur(1px);

    border-right-width: 9px;
    border-bottom-width: 9px;

    width: calc(100% + 12px);
    height: calc(100% + 12px);
}

.douban-btn,
.douban-btn:link,
.douban-btn:visited,
.douban-btn:active {
    text-decoration: none;
}


/* 书签燕尾 W 形 */
.bookmark-tail {
    position: relative;
    width: 100%;        /* 和 bookmark-lines 宽度一致 */
    height: 20px;       /* 燕尾总高度,可调整 */
    margin-top: -2px;
}

/* 左竖线 */
.bookmark-tail::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 2px;               /* 与 bookmark-lines 宽度一致 */
    height: 100%;
    background-color: var(--book-color);
}

/* 右竖线 */
.bookmark-tail::after {
    content: "";
    position: absolute;
    right: 0;
    top: 0;
    width: 2px;
    height: 100%;
    background-color: var(--book-color);
}

/* W 的两个尖角线条 */
.bookmark-tail .w-line {
    position: absolute;
    bottom: 0;
    width: 2px;               /* 和 bookmark-lines 线宽一致 */
    background-color: var(--book-color);
}

/* 左 V 斜线 */
.bookmark-tail .w-line.left {
    left: 0;
    height: 12px;             /* 尖顶高度,可调 */
    transform: rotate(45deg);
    transform-origin: bottom left;
}

/* 右 V 斜线 */
.bookmark-tail .w-line.right {
    right: 0;
    height: 12px;             /* 尖顶高度,可调 */
    transform: rotate(-45deg);
    transform-origin: bottom right;
}


</style>

DONE 链接 / 强调 / 删除线/下划线/居中/语义增强

/* 应用自定义颜色到具体元素 */
/*加粗*/
/* strong, */
/* b { */
/*     color: var(--custom-emphasis) !important; */
/* } */

/*链接*/
.post-content a,
.page-header a,
.post-single a {
    color: var(--custom-link) ;
}

.post-content a:hover,
.page-header a:hover,
.post-single a:hover {
    color: var(--custom-emphasis) !important;
}

.post-content a {
    /* 1. 关掉 PaperMod 默认的“阴影下划线” */
    box-shadow: none !important;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
    transition: color 0.2s;
}

/* 删除线样式 - 使用主题变量 */
del,
s {
    text-decoration: none !important;
    background-color: #2c2c2c; /* 深色遮盖块 */
    color: #2c2c2c ; /* 文字同色,实现隐藏 */
    padding: 0 4px;
    border-radius: 2px;
    transition: color 0.2s;
    cursor: help;
}

/* 鼠标放上去时显示文字 */
del:hover,
s:hover {
    color: #ffffff; /* 文字变白显示 */
}

/* --- 居中样式优化 --- */
.post-content .org-center p {
    text-align: center !important;
    display: block;
    width: 100%;
    font-size: 1.1em;
    color: var(--primary);
    margin: 0.8em 0;
    font-weight: bold;

}

/* 如果居中里面是图片,去掉多余的边距 */
.org-center img {
    margin: 0 auto;
    display: block;
}

/*下划线*/
.underline {
    /* 抬高文本基准线 */
    position: relative;
    top: -3px; /* 可根据需求调整抬高幅度 */

    /* 下划线与其他文本底部一致(替代原生underline) */
    text-decoration: none; /* 取消原生下划线 */
    border-bottom: 3px solid var(--fold-h2); /* #c01c28; /* 自定义下划线 */
    padding-bottom: 0px; /* 下划线与文本底部间距 */

    /* 下划线前后增大字间距 */
    letter-spacing: 0px; /* 整体字间距 */
    margin: 0 0px; /* 下划线文本与前后内容的间距 */
}

DONE 表格居中并可以左右滑动

/* --- LaTeX booktabs Style for Tables (Ultra-Compact Version) --- */

/* 整体表格容器 */
.post-content table:not(.highlighttable, .highlight table, .gist .highlight) {
    border-collapse: collapse;
    font-size: 0.9em;
    text-align: left; /* 内部文字左对齐 */
    border: none;

    /* --- 核心修改开始 --- */
    display: block;

    /* 1. 改为 fit-content:让表格宽度跟随内容,而不是强行撑满一行 */
    width: fit-content;

    /* 2. 限制最大宽度:防止表格超出屏幕,超出时由 overflow 处理 */
    max-width: 100%;

    /* 3. 居中:因为宽度现在是 fit-content,margin: auto 就能生效了 */
    margin: 1.5em auto;

    /* 4. 滑动设置 */
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    /* --- 核心修改结束 --- */
}

/* 顶线(\toprule) 和 中线(\midrule) */
.post-content table:not(.highlighttable, .highlight table, .gist .highlight) thead {
    border-top: 2px solid var(--primary);
    border-bottom: 1px solid var(--primary);

    /* 配合 display: block,确保表头宽度跟随父元素 */
    width: 100%;
}

/* 底线(\bottomrule) */
.post-content table:not(.highlighttable, .highlight table, .gist .highlight) tbody {
    border-bottom: 2px solid var(--primary);

    /* 配合 display: block,确保表体宽度跟随父元素 */
    width: 100%;
}

/* 单元格样式 */
.post-content table:not(.highlighttable, .highlight table, .gist .highlight) th,
.post-content table:not(.highlighttable, .highlight table, .gist .highlight) td {
    border: none !important;
    padding: 0.25em 1em;
    vertical-align: top;

    /* 建议打开 nowrap,这样可以更早触发滑动,而不是挤压文字换行 */
    /* white-space: nowrap; */
}

/* 表头单元格加粗 */
.post-content table:not(.highlighttable, .highlight table, .gist .highlight) th {
    font-weight: bold;
    text-align: left;
}

DONE 内容折叠功能 延迟加载

DONE 折叠 css

/* --- 你原有折叠样式(完全保留) --- */
:root, body:not(.dark) {
    --fold-h2: #3a81c3;
    --fold-h3: #2d9574;
    --fold-h4: #67b11d;
    --fold-h5: #b1951d;
}
.dark {
    --fold-h2: #4f97d7;
    --fold-h3: #2d9574;
    --fold-h4: #67b11d;
    --fold-h5: #b1951d;
}
.post-content .foldable-header {
    cursor: pointer;
    position: relative;
    user-select: none;
    -webkit-user-select: none;
    display: flex;
    align-items: center;
    margin-left: 0 !important;
    line-height: 1.5 !important; /* 固定行高,避免CLS */
    border-radius: 4px;
    padding: 4px 0;
    margin: 0.8em 0 0.4em 0 !important; /* 固定边距,避免CLS */
    transition: background-color 0.2s;
}
.post-content .foldable-header:hover {
    background-color: rgba(0, 0, 0, 0.03);
}
.dark .post-content .foldable-header:hover {
    background-color: rgba(255, 255, 255, 0.05);
}

/* 箭头图标(伪元素) */
.post-content .foldable-header::before {
    content: '▶';
    display: inline-flex;
    justify-content: center;
    align-items: center;
    width: 1.5em;
    height: 1.5em;
    margin-right: 0.2em;
    font-size: 0.7em;
    flex-shrink: 0;
    transition: transform 0.2s ease;
    color: inherit;
    opacity: 0.8;
}
.post-content .foldable-header.expanded::before {
    transform: rotate(90deg);
    opacity: 1;
}
.post-content .foldable-header.collapsed::before {
    transform: rotate(0deg);
}

/* --- 核心CLS修复:隐藏折叠内容 --- */
/* 方式1:使用包裹容器(推荐,如果你能改HTML) */
/*
.post-content .foldable-section .foldable-content {
    display: none;
}
.post-content .foldable-section .foldable-header.expanded + .foldable-content {
    display: block;
}
*/

/* 方式2:不修改HTML,用兄弟选择器隐藏从折叠标题到下一个标题之间的所有内容 */
.post-content h2.foldable-header.collapsed ~ *:not(h2):not(h3):not(h4):not(h5),
.post-content h3.foldable-header.collapsed ~ *:not(h2):not(h3):not(h4):not(h5),
.post-content h4.foldable-header.collapsed ~ *:not(h2):not(h3):not(h4):not(h5),
.post-content h5.foldable-header.collapsed ~ *:not(h2):not(h3):not(h4):not(h5) {
    display: none;
}
/* 确保非折叠标题后的内容正常显示(覆盖上一条规则) */
.post-content h2.foldable-header:not(.collapsed) ~ *:not(h2):not(h3):not(h4):not(h5),
.post-content h3.foldable-header:not(.collapsed) ~ *:not(h2):not(h3):not(h4):not(h5),
.post-content h4.foldable-header:not(.collapsed) ~ *:not(h2):not(h3):not(h4):not(h5),
.post-content h5.foldable-header:not(.collapsed) ~ *:not(h2):not(h3):not(h4):not(h5) {
    display: block; /* 恢复默认显示,可根据实际需要调整 */
}

/* --- 移除或精简 content-visibility,避免额外偏移 --- */
/* 不需要对非标题元素设置 content-visibility,注释掉或删除 */
/* .post-content h2:first-of-type ~ *:not(h2):not(h3):not(h4):not(h5) { ... } */

/* 图片:保留宽高比或使用原生懒加载,避免设置固定预留高度 */
.post-content img {
    display: block;
    max-width: 100%;
    height: auto;
    /* 如果图片有明确宽高比,可以加上 aspect-ratio */
    /* aspect-ratio: attr(width) / attr(height); 但需要HTML有width/height */
}
/* 字体优化保留 */
body {
    font-display: swap;
}

/* 兼容Org-mode类(保留,但确保它们不受影响) */
.post-content .org-center,
.post-content .org-src-container,
.post-content .figure,
.post-content .table {
    display: block !important;
    /* 移除 contain 相关 */
}

/* --- 增强:键盘聚焦样式 (Tab键选中时显示) --- */
.post-content .foldable-header:focus-visible {
    outline: 2px solid var(--fold-h2);
    outline-offset: 2px;
    background-color: rgba(0, 0, 0, 0.05) !important;
}

.dark .post-content .foldable-header:focus-visible {
    background-color: rgba(255, 255, 255, 0.1) !important;
}

DONE content-fold.js

document.addEventListener("DOMContentLoaded", function () {
    const content = document.querySelector('.post-content');
    if (!content) return;

    const allHeaders = Array.from(content.querySelectorAll('h2, h3, h4, h5'));
    const allElements = Array.from(content.querySelectorAll('*'));

    // ==========================================
    // 1. 状态管理
    // ==========================================
    let leaderKeyActive = false;
    let leaderTimeout = null;
    let lastStateSnapshot = null;

    // ==========================================
    // 2. 原有逻辑:优先渲染 & 初始化标题
    // ==========================================
    function prioritizeFirstContent() {
        const firstH2 = content.querySelector('h2:first-of-type');
        if (firstH2) {
            const firstH2Index = allElements.indexOf(firstH2);
            for (let i = 0; i < firstH2Index; i++) {
                allElements[i].style.display = '';
            }
            allHeaders.forEach(header => {
                header.style.display = '';
            });
        }
    }

    allHeaders.forEach(header => {
        header.classList.add('foldable-header');
        header.classList.add('collapsed');
        header.setAttribute('role', 'button');
        header.setAttribute('tabindex', '0');
        header.setAttribute('aria-expanded', 'false');

        header.addEventListener('click', (e) => {
            if (window.getSelection().toString().length > 0) return;
            saveState();
            handleHeaderClick(header);
        });

        header.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                saveState();
                handleHeaderClick(header);
            }
        });
    });

    // ==========================================
    // 3. 原有逻辑:点击控制中心 & 渲染器
    // ==========================================
    function handleHeaderClick(clickedHeader) {
        const tagName = clickedHeader.tagName.toLowerCase();
        const isCurrentlyExpanded = clickedHeader.classList.contains('expanded');

        if (tagName === 'h2') {
            if (!isCurrentlyExpanded) {
                setExpanded(clickedHeader);
            } else {
                content.querySelectorAll('h2').forEach(h2 => setCollapsed(h2));
            }
        } else if (tagName === 'h3') {
            if (!isCurrentlyExpanded) {
                setExpanded(clickedHeader);
            } else {
                const parent = findParentByLevel(clickedHeader, 2);
                content.querySelectorAll('h3').forEach(h => {
                    if (findParentByLevel(h, 2) === parent) setCollapsed(h);
                });
            }
        } else if (tagName === 'h4') {
            if (!isCurrentlyExpanded) {
                setExpanded(clickedHeader);
            } else {
                const parent = findParentByLevel(clickedHeader, 3);
                content.querySelectorAll('h4').forEach(h => {
                    if (findParentByLevel(h, 3) === parent) setCollapsed(h);
                });
            }
        }
        renderVisibility();
    }

    function renderVisibility() {
        const changes = [];
        allHeaders.forEach((header, idx) => {
            const level = getLevel(header);
            const isExpanded = header.classList.contains('expanded');

            let nextSameLevelIdx = allHeaders.length;
            for (let i = idx + 1; i < allHeaders.length; i++) {
                if (getLevel(allHeaders[i]) <= level) {
                    nextSameLevelIdx = i;
                    break;
                }
            }

            const headerPosInAll = allElements.indexOf(header);
            const nextHeaderPosInAll = nextSameLevelIdx < allHeaders.length
                ? allElements.indexOf(allHeaders[nextSameLevelIdx])
                : allElements.length;

            for (let i = headerPosInAll + 1; i < nextHeaderPosInAll; i++) {
                const el = allElements[i];
                const elTag = el.tagName ? el.tagName.toLowerCase() : '';

                if (elTag.match(/^h[2-5]$/)) {
                    const elLevel = getLevel(el);
                    if (elLevel === level + 1) {
                        changes.push({ el, display: isExpanded ? '' : 'none' });
                    }
                    continue;
                }

                let shouldHide = !isExpanded;
                if (!shouldHide) {
                    for (let j = 0; j < idx; j++) {
                        const ancestorLevel = getLevel(allHeaders[j]);
                        const ancestorPosInAll = allElements.indexOf(allHeaders[j]);

                        if (ancestorPosInAll < i && ancestorLevel < level) {
                            let ancestorEndIdx = allHeaders.length;
                            for (let k = j + 1; k < allHeaders.length; k++) {
                                if (getLevel(allHeaders[k]) <= ancestorLevel) {
                                    ancestorEndIdx = k;
                                    break;
                                }
                            }
                            const ancestorEndPosInAll = ancestorEndIdx < allHeaders.length
                                ? allElements.indexOf(allHeaders[ancestorEndIdx])
                                : allElements.length;

                            if (i < ancestorEndPosInAll && !allHeaders[j].classList.contains('expanded')) {
                                shouldHide = true;
                                break;
                            }
                        }
                    }
                }
                changes.push({ el, display: shouldHide ? 'none' : '' });
            }
        });
        changes.forEach(({ el, display }) => {
            el.style.display = display;
        });
    }

    // ==========================================
    // 4. 辅助函数
    // ==========================================
    function getLevel(header) { return parseInt(header.tagName.substring(1)); }
    function findParentByLevel(element, parentLevel) {
        const idx = allHeaders.indexOf(element);
        for (let i = idx - 1; i >= 0; i--) {
            if (getLevel(allHeaders[i]) === parentLevel) return allHeaders[i];
        }
        return null;
    }
    function setExpanded(el) {
        el.classList.remove('collapsed');
        el.classList.add('expanded');
        el.setAttribute('aria-expanded', 'true');
    }
    function setCollapsed(el) {
        el.classList.remove('expanded');
        el.classList.add('collapsed');
        el.setAttribute('aria-expanded', 'false');
    }

    // ==========================================
    // 5. 状态快照系统
    // ==========================================
    function saveState() {
        lastStateSnapshot = allHeaders.map(h => ({
            element: h,
            isExpanded: h.classList.contains('expanded')
        }));
    }

    function restoreState() {
        if (!lastStateSnapshot) return;
        lastStateSnapshot.forEach(({ element, isExpanded }) => {
            if (isExpanded) {
                element.classList.remove('collapsed');
                element.classList.add('expanded');
                element.setAttribute('aria-expanded', 'true');
            } else {
                element.classList.remove('expanded');
                element.classList.add('collapsed');
                element.setAttribute('aria-expanded', 'false');
            }
        });
        renderVisibility();
    }

    // ==========================================
    // 6. 布局功能函数
    // ==========================================
    function modeAll() {
        saveState();
        allHeaders.forEach(h => setExpanded(h));
        renderVisibility();
    }

    function modeSemi() {
        saveState();
        allHeaders.forEach(h => {
            const level = getLevel(h);
            if (level === 2 || level === 3) setExpanded(h);
            else setCollapsed(h);
        });
        renderVisibility();
    }

    function modeDirectory() {
        saveState();
        allHeaders.forEach(h => {
            const level = getLevel(h);
            if (level === 2) setExpanded(h);
            else setCollapsed(h);
        });
        renderVisibility();
    }

    // ==========================================
    // 7. 主快捷键监听
    // ==========================================
    document.addEventListener('keydown', (e) => {
        const activeEl = document.activeElement;
        const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(activeEl.tagName) || activeEl.isContentEditable;
        if (isTyping) return;

        if (e.key === 'z' && !leaderKeyActive) {
            leaderKeyActive = true;
            clearTimeout(leaderTimeout);
            leaderTimeout = setTimeout(() => { leaderKeyActive = false; }, 500);
        } else if (leaderKeyActive) {
            if (e.key === 'a') modeAll();
            else if (e.key === 's') modeSemi();
            else if (e.key === 'd') modeDirectory();
            else if (e.key === 'z') restoreState();

            leaderKeyActive = false;
            clearTimeout(leaderTimeout);
        }
    });

    // ==========================================
    // 🌟 🌟 🌟 修复:Tab 优化逻辑移到这里 (内部)
    // ==========================================
    const firstContentHeader = content.querySelector('h2');
    if (firstContentHeader) {
        let hasEnteredContent = false;

        document.addEventListener('keydown', function(e) {
            // 如果是 Shift+Tab,或者正在打字,不管
            if (e.key !== 'Tab' || e.shiftKey) return;

            const activeEl = document.activeElement;
            const isTyping = ['INPUT', 'TEXTAREA'].includes(activeEl.tagName) || activeEl.isContentEditable;
            if (isTyping) return;

            const isOutsideContent = !content.contains(activeEl);

            if (isOutsideContent && !hasEnteredContent) {
                e.preventDefault();
                firstContentHeader.focus();
                hasEnteredContent = true;
            }
        });

        content.addEventListener('mousedown', function() {
            hasEnteredContent = true;
        });
    }

    // 初始化
    prioritizeFirstContent();
    renderVisibility();
});

index.html

{{- define "main" }}

{{- if site.Params.profileMode.enabled }}
{{- partial "index_profile.html" . }}
{{- else if site.Params.homeInfoParams }}
{{- partial "home_info.html" . }}
{{- end }}

{{- /* 加载搜索脚本 */ -}}
<link crossorigin="anonymous" rel="preload" as="fetch" href="index.json">
{{- $section_search := resources.Get "js/section-search.js" | js.Build (dict "params" (dict "fuseOpts" site.Params.fuseOpts)) | resources.Minify }}
{{- $fusejs := resources.Get "js/fuse.basic.min.js" }}
{{- $search := (slice $fusejs $section_search) | resources.Concat "assets/js/main-search.js" | fingerprint }}
<script defer crossorigin="anonymous" src="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}"></script>

{{- end }}

DONE 渲染/音频/引用/图片/链接/ shortcodes

audio.html

{{/* 使用方法: 
*/
}}
<div class="audio-wrapper" style="margin: 1rem 0;"> <audio controls preload="none" style="width: 100%;"> <source src="{{ .Get "src" }}" type="audio/mpeg"> 您的浏览器不支持 audio 标签 </audio> </div>

bilibili.html


{{/* 使用方法: 
*/
}}
<div class="video-wrapper" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; margin: 1.5rem 0;"> <iframe src="//player.bilibili.com/player.html?bvid={{ .Get "id" }}&page=1&autoplay=0&high_quality=1" loading="lazy" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"> </iframe> </div>

youtube.html


{{/* 使用方法: 
*/
}}
<div class="video-wrapper" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; margin: 1.5rem 0; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> <iframe src="https://www.youtube-nocookie.com/embed/{{ .Get "id" }}" title="YouTube video player" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"> </iframe> </div>

render-blockquote.html

{{- /* layouts/_default/_markup/render-blockquote.html (Unicode 图标 + 不换行版) */ -}}
{{- $raw := .Text -}}
{{- $content := $raw -}}
{{- $type := "" -}}
{{- $title := "" -}}
{{- $icon := "" -}}

{{- /* 定义 Unicode 图标 (添加 &#xFE0E; 强制以文本显示,以便 CSS 染色) */ -}}
{{- $iconNote := "ⓘ" -}}
{{- $iconTip := "✓" -}}
{{- $iconWarn := "⚠&#xFE0E;" -}}
{{- $iconAlert := "❗&#xFE0E;" -}}

{{- /* 1. Note / 注意 */ -}}
{{- if findRE `(?i)^\s*(<p>)?\s*(<strong>|<b>)?\s*(\[!NOTE\]|Note|注意)(:|:)?\s*(</strong>|</b>)?` $raw -}}
    {{- $type = "note" -}}
    {{- $title = "注意" -}}
    {{- $icon = $iconNote -}}
    {{- $content = replaceRE `(?i)^\s*(<p>)?\s*(<strong>|<b>)?\s*(\[!NOTE\]|Note|注意)(:|:)?\s*(</strong>|</b>)?\s*` "$1" $raw -}}

{{- /* 2. Tip / 提示 */ -}}
{{- else if findRE `(?i)^\s*(<p>)?\s*(<strong>|<b>)?\s*(\[!TIP\]|Tip|提示)(:|:)?\s*(</strong>|</b>)?` $raw -}}
    {{- $type = "tip" -}}
    {{- $title = "提示" -}}
    {{- $icon = $iconTip -}}
    {{- $content = replaceRE `(?i)^\s*(<p>)?\s*(<strong>|<b>)?\s*(\[!TIP\]|Tip|提示)(:|:)?\s*(</strong>|</b>)?\s*` "$1" $raw -}}

{{- /* 3. Warning / 警告 */ -}}
{{- else if findRE `(?i)^\s*(<p>)?\s*(<strong>|<b>)?\s*(\[!WARNING\]|Warning|警告)(:|:)?\s*(</strong>|</b>)?` $raw -}}
    {{- $type = "warning" -}}
    {{- $title = "警告" -}}
    {{- $icon = $iconWarn -}}
    {{- $content = replaceRE `(?i)^\s*(<p>)?\s*(<strong>|<b>)?\s*(\[!WARNING\]|Warning|警告)(:|:)?\s*(</strong>|</b>)?\s*` "$1" $raw -}}

{{- /* 4. Alert / 错误 */ -}}
{{- else if findRE `(?i)^\s*(<p>)?\s*(<strong>|<b>)?\s*(\[!IMPORTANT\]|Important|错误|重要)(:|:)?\s*(</strong>|</b>)?` $raw -}}
    {{- $type = "alert" -}}
    {{- $title = "重要" -}}
    {{- $icon = $iconAlert -}}
    {{- $content = replaceRE `(?i)^\s*(<p>)?\s*(<strong>|<b>)?\s*(\[!IMPORTANT\]|Important|错误|重要)(:|:)?\s*(</strong>|</b>)?\s*` "$1" $raw -}}
{{- end -}}

{{- /* 5. 渲染逻辑 */ -}}
{{- if ne $type "" -}}
    {{- /* 构造 HTML:图标 + 标题 */ -}}
    {{- $titleHtml := printf `<span class="github-title"><span class="github-icon">%s</span>%s:</span>` $icon $title -}}

    {{- /* 注入到第一个 P 标签,实现不换行 */ -}}
    {{- if strings.Contains $content "<p>" -}}
        {{- $content = replaceRE "^\\s*<p>" (printf "<p>%s" $titleHtml) $content -}}
    {{- else -}}
        {{- $content = printf "%s%s" $titleHtml $content -}}
    {{- end -}}

    <div class="blockquote-github {{ $type }}">
        {{ $content | safeHTML }}
    </div>
{{- else -}}
    <blockquote>
        {{ $raw | safeHTML }}
    </blockquote>
{{- end -}}

TODO figure.html


{{- /* layouts/shortcodes/figure.html */ -}}
{{- $src := .Get "src" -}}

{{- /* 新增:清洗路径逻辑 */ -}}
{{- /* 如果路径包含 /static/,则只保留 static 之后的部分 */ -}}
{{- if in $src "/static/" -}}
  {{- $src = index (split $src "/static/") 1 -}}
  {{- /* 补回开头的 / 以便作为 URL 使用 */ -}}
  {{- $src = printf "/%s" $src -}}
{{- end -}}

{{- $u := urls.Parse $src -}}
{{- $img := "" -}}

{{- /* 1. 查找资源 */ -}}
{{- if not $u.IsAbs -}}
  {{- $path := strings.TrimPrefix "./" $u.Path -}}
  {{- $cleanPath := strings.TrimPrefix "/" $path -}}
  {{- $img = or ($.Page.Resources.Get $cleanPath) (resources.Get $cleanPath) -}}
{{- end -}}

{{- /* 2. 生产环境检查 */ -}}
{{- $prod := (hugo.IsProduction | or (eq site.Params.env "production")) }}

{{- /* 3. 响应式图片配置 */ -}}
{{- $responsiveImages := (site.Params.responsiveImages | default true) }}

{{- /* 4. 定义可处理格式 */ -}}
{{- $processableFormats := (slice "jpg" "jpeg" "png" "tif" "bmp") -}}
{{- if hugo.IsExtended -}}
  {{- $processableFormats = $processableFormats | append "webp" -}}
{{- end -}}

{{- /* 5. 定义响应式尺寸 */ -}}
{{- $sizes := (slice "360" "480" "720" "1080" "1500") }}

<figure{{ if or (.Get "class") (eq (.Get "align") "center") }} class="
           {{- if eq (.Get "align") "center" }}align-center {{ end }}
           {{- with .Get "class" }}{{ . }}{{- end }}"
{{- end -}}>
    {{- if .Get "link" -}}
        <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
    {{- end }}

    {{- /* 6. 渲染图片 */ -}}
    {{- if and $img (in $processableFormats $img.MediaType.SubType) (eq $prod true) $responsiveImages -}}
        {{- /* A. 生产环境下的响应式图片 */ -}}
        <img
            loading="lazy"
            src="{{ $img.Permalink }}"
            {{- if or (.Get "alt") (.Get "caption") }}
            alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify | plainify }}{{ end }}"
            {{- end }}
            {{- with .Get "width" }} width="{{ . }}"{{ else }} width="{{ $img.Width }}"{{ end }}
            {{- with .Get "height" }} height="{{ . }}"{{ else }} height="{{ $img.Height }}"{{ end }}
            {{- if eq (.Get "align") "center" }}#center{{ end }}
            decoding="async"
            srcset="
              {{- range $size := $sizes -}}
                {{- if ge $img.Width $size -}}
                  {{ ($img.Resize (printf "%sx webp q75" $size)).RelPermalink }} {{ $size }}w,
                {{- end -}}
              {{- end -}}
              {{ ($img.Resize (printf "%dx webp q75" $img.Width)).RelPermalink }} {{ $img.Width }}w"
            sizes="(min-width: 768px) 720px, 100vw"
            style="max-width: 100%; height: auto;">

    {{- else if $img -}}
        {{- /* B. 开发环境或禁用响应式时的简单输出 */ -}}
        <img
            loading="lazy"
            src="{{ $img.Permalink }}"
            {{- if or (.Get "alt") (.Get "caption") }}
            alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify | plainify }}{{ end }}"
            {{- end }}
            {{- with .Get "width" }} width="{{ . }}"{{ end }}
            {{- with .Get "height" }} height="{{ . }}"{{ end }}
            {{- if eq (.Get "align") "center" }}#center{{ end }}
            style="max-width: 100%; height: auto;">

    {{- else -}}
        {{- /* C. 外部链接或无法处理的图片 */ -}}
        <img
            loading="lazy"
            src="{{ $src | safeURL }}"
            {{- if or (.Get "alt") (.Get "caption") }}
            alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify | plainify }}{{ end }}"
            {{- end }}
            {{- with .Get "width" }} width="{{ . }}"{{ end }}
            {{- with .Get "height" }} height="{{ . }}"{{ end }}
            {{- if eq (.Get "align") "center" }}#center{{ end }}
            style="max-width: 100%; height: auto;">
    {{- end }}

    {{- if .Get "link" }}</a>{{ end -}}

    {{- /* 7. 渲染 figcaption */ -}}
    {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
        <figcaption>
            {{ with (.Get "title") -}}
                {{ . }}
            {{- end -}}
            {{- if or (.Get "caption") (.Get "attr") -}}<p>
                {{- .Get "caption" | markdownify -}}
                {{- with .Get "attrlink" }}
                    <a href="{{ . }}">
                {{- end -}}
                {{- .Get "attr" | markdownify -}}
                {{- if .Get "attrlink" }}</a>{{ end }}</p>
            {{- end }}
        </figcaption>
    {{- end }}
</figure>

TODO render-image.html

{{- /* layouts/_default/_markup/render-image.html (已修复作用域问题) */ -}}
{{- $u := urls.Parse .Destination -}}
{{- $src := $u.String -}}
{{- $img := "" -}}

{{- /* 1. 查找资源 */ -}}
{{- if not $u.IsAbs -}}
  {{- $path := strings.TrimPrefix "./" $u.Path -}}
  {{- $cleanPath := strings.TrimPrefix "/" $path -}}
  {{- /* 注意:这里用 = 赋值,而不是 := */ -}}
  {{- $img = or (.PageInner.Resources.Get $cleanPath) (resources.Get $cleanPath) -}}
{{- end -}}

{{- /* 2. 定义可处理格式 */ -}}
{{- $processableFormats := (slice "jpg" "jpeg" "png" "tif" "bmp") -}}
{{- if hugo.IsExtended -}}
  {{- $processableFormats = $processableFormats | append "webp" -}}
{{- end -}}

{{- /* 3. 渲染逻辑 (现在这里的 $img 是可见的了) */ -}}
{{- if and $img (in $processableFormats $img.MediaType.SubType) -}}

  {{- /* A. 智能 WebP 转换 */ -}}
  <img
    src="{{ ($img.Resize "1200x").RelPermalink }}"
    alt="{{ .Text }}"
    {{- with .Title }} title="{{ . }}"{{ end }}
    width="{{ $img.Width }}"
    height="{{ $img.Height }}"
    loading="lazy"
    decoding="async"
    srcset="
      {{- if ge $img.Width 480 }}{{ ($img.Resize "480x webp q75").RelPermalink }} 480w,{{ end }}
      {{- if ge $img.Width 720 }}{{ ($img.Resize "720x webp q75").RelPermalink }} 720w,{{ end }}
      {{- if ge $img.Width 1080 }}{{ ($img.Resize "1080x webp q75").RelPermalink }} 1080w,{{ end }}
      {{ ($img.Resize (printf "%dx webp q75" $img.Width)).RelPermalink }} {{ $img.Width }}w"
    sizes="(max-width: 768px) 100vw, 720px"
    style="max-width: 100%; height: auto;">

{{- else -}}

{{- /* B. 原样输出 (SVG/GIF/外链) */ -}}
<img
  src="{{ $src | safeURL }}"
  alt="{{ .Text }}"
  {{- with .Title }} title="{{ . }}"{{ end }}
  loading="lazy"
  decoding="async"
  /* 修改重点在这里:添加 vertical-align 和 display */
  style="display: inline-block; vertical-align: middle; max-width: 100%; height: auto; margin: 0 0.2em;">

{{- end -}}
{{- /* layouts/_default/_markup/render-link.html */ -}}
{{- $destination := .Destination -}}
{{- $isRemote := strings.HasPrefix $destination "http" -}}
{{- $isInternal := strings.HasPrefix $destination site.BaseURL -}}

<a href="{{ $destination | safeURL }}"
  {{- with .Title }} title="{{ . }}"{{ end -}}
  {{- /* 逻辑:如果是 http 开头(远程) 且 不是本站域名(非内部) -> 则是站外链接 */ -}}
  {{- if and $isRemote (not $isInternal) -}}
    target="_blank" rel="noopener external noreferrer" class="external-link"
  {{- end -}}
>
  {{- .Text | safeHTML -}}
  {{- /* 可选:给站外链接加一个小图标 */ -}}
  {{- if and $isRemote (not $isInternal) -}}
    <span class="external-link-icon" style="font-size: 0.8em; opacity: 0.6; margin-left: 2px;"></span>
  {{- end -}}
</a>

DONE 备案号等

{{- /* layouts/partials/footer.html */ -}}

{{- if not (.Param "hideFooter") }}
<footer class="footer">
    {{- if .IsHome }}
    <div class="custom-footer-info">

        <!-- 第一行:版权年份 -->
        <div class="footer-row-copyright">
            &copy; 2025{{ if ne now.Year 2025 }}-{{ now.Year }}{{ end }} {{ site.Title }}
        </div>

        <!-- 第二行:备案信息 -->
        <div class="footer-row-beian">
            <!-- 苏公网安备 -->
            <span style="display:inline-flex; align-items:center; gap:3px;">
                <img src="beian.png" alt="公安备案" style="height:15px; width:auto; vertical-align:middle;">
                <a href="https://beian.mps.gov.cn/#/query/webSearch?code=32010502011305" target="_blank" rel="noreferrer">
                    苏公网安备32010502011305号
                </a>
            </span>

            <span class="beian-separator">·</span>

            <!-- ICP -->
            <a href="https://beian.miit.gov.cn" target="_blank" rel="nofollow noopener noreferrer">
                苏ICP备2025219176号-1
            </a>
        </div>

        <!-- 第三行:制作工具 -->
        <div class="footer-row-tools">
          本站由
          <a href="https://gohugo.io/" target="_blank" rel="noopener">Hugo</a> ·
          <a href="https://github.com/adityatelange/hugo-PaperMod/" target="_blank" rel="noopener">PaperMod</a> ·
          <a href="https://github.com/doomemacs/doomemacs" target="_blank" rel="noopener">Doom Emacs</a> +
          <a href="https://ox-hugo.scripter.co/" target="_blank" rel="noopener">ox-hugo</a> 创作
        </div>

    </div>
    {{- end }}
</footer>
{{- end }}

css

/* --- custom_footer.css --- */

/* 容器整体样式 */
.custom-footer-info {
    text-align: center;
    line-height: 1.5;
    padding: 10px 0;
    width: 100%;
}

/* 链接通用设置 */
.custom-footer-info a {
    text-decoration: none;         /* 默认无下划线 */
    color: inherit;                /* 默认继承文字颜色 */
    transition: color 0.3s ease;   /* 颜色变化添加 0.3秒 平滑过渡 */
}

/* 鼠标悬停时变色 */
.custom-footer-info a:hover {
    text-decoration: none;         /* 确保悬停也没下划线 */
    color: var(--custom-emphasis); /* 使用主题的高亮色变量 */
}

/* --- 第一行:版权信息 (字号正常,颜色最深) --- */
.footer-row-copyright {
    font-size: 14px;
    margin-bottom: 6px;
    opacity: 1.0;
}

/* --- 第二行:备案信息 (字号稍小,颜色稍淡) --- */
.footer-row-beian {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    font-size: 13px;
    margin-bottom: 6px;
    opacity: 0.85;
}

/* --- 第三行:制作工具 (字号最小,颜色最淡) --- */
.footer-row-tools {
    font-size: 12px;
    opacity: 0.6;
}

/* --- 手机端适配 (屏幕宽度小于 700px) --- */
@media screen and (max-width: 700px) {
    .footer-row-copyright {
        font-size: 12px;
    }
    .footer-row-beian {
        font-size: 12px;
        flex-direction: column; /* 垂直排列 */
        gap: 4px;
    }
    .beian-separator {
        display: none; /* 隐藏分隔点 */
    }
    .footer-row-tools {
        font-size: 10px;
        margin-top: 8px;
    }
}

TODO 三联排按钮 /主页/返回/顶部

css

/* assets/css/extended/custom.css (完整替换版) */

/* 1. 按钮组容器样式 (保持不变) */
.scroll-buttons-container {
    position: fixed;
    bottom: 30px;
    margin-left: calc(var(--main-width) / 2 + var(--gap) + 30px);
    left: 50%;
    display: flex;
    flex-direction: column;
    gap: 10px;
    visibility: hidden;
    opacity: 0;
    transition: visibility 0.3s, opacity 0.3s ease;
    z-index: 9999;
    pointer-events: none;
}
.scroll-buttons-container.visible {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
}

/* 2. 所有滚动按钮的统一基础样式 (亮色模式) */
.scroll-btn {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 45px;
    height: 45px;
    border-radius: 50%;

    /* 亮色模式样式 */
    background: #fff;
    color: #333; /* 文本/符号颜色 (例如 ↩ 和 ∧) */
    border: 1px solid #e0e0e0;

    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    transition: all 0.3s ease;
    cursor: pointer;
    text-decoration: none;
    font-size: 18px;
    font-weight: bold;
    text-align: center;
}

.scroll-btn:hover {
    transform: scale(1.1);
    background: #f0f0f0; /* 亮色模式悬浮背景 */
}

/* 3. 按钮内图标/文本的容器 */
.scroll-btn .btn-icon {
    display: inline-block;
    line-height: 1;
    width: 100%;
    text-align: center;
}

/* 4. 控制按钮内的 SVG 图标样式 (亮色模式) */
.scroll-btn svg {
    width: 24px;
    height: 24px;
    vertical-align: middle;
    transition: all 0.3s ease;

    /* 适用于通过 stroke 绘制的图标 (太阳/月亮) */
    stroke: #333;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;

    /* 适用于通过 fill 填充的图标 (你的 Logo) */
    fill: #333;
}


/* 5. == 核心:暗色模式下的样式覆盖 == */
body.dark .scroll-btn {
    background: #333;
    color: #fff; /* 文本/符号颜色变为白色 */
    border-color: #555;
    box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}

body.dark .scroll-btn:hover {
    background: #555;
}

body.dark .scroll-btn svg {
    stroke: #fff; /* stroke 绘制的图标变为白色 */
    fill: #fff;   /* fill 填充的图标变为白色 */
}


/* 6. 主题切换按钮的图标显示/隐藏逻辑 (保持不变) */
body:not(.dark) .theme-toggle-vertical #sun-icon-vertical {
    display: none;
}

body.dark .theme-toggle-vertical #moon-icon-vertical {
    display: none;
}

/* 7. 移动端优化 (保持不变) */
@media screen and (max-width: 960px) {
    .scroll-buttons-container {
        display: none !important;
    }
}
{{- /* layouts/partials/footer.html (已修正) */ -}}

{{- /* ==========================================
     2. 滚动按钮组 (HTML 结构)
   ========================================== */ -}}
{{- if (and (not site.Params.disableScrollButtons) (or (eq .Kind "page") (eq .Kind "section")))}}
<div class="scroll-buttons-container" id="scroll-buttons">

    {{- /* 1. 首页按钮 (使用新的房子SVG图标) */ -}}
<a href="/" aria-label="Go to Home" title="主站" class="scroll-btn" accesskey="h">
  <svg width="28" height="28" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" class="btn-icon">
    <path fill="currentColor" d="M 6.4544811,93.511263 C 2.8237775,89.948114 2.2766351,83.779465 5,80 c 3.3511153,-3.302301 7.777063,-5.497815 12.068254,-7.315711 8.00421,-2.595085 17.577743,-2.993331 25.873387,-2.562062 6.210766,0.165414 12.236177,-0.616651 17.856339,-0.80016 C 85.984584,66.085169 87.160569,60.659254 90,60 c 3.427405,-3.16102 4.000406,-3.642199 10,-10 3,-4.432975 1.57067,-3.393566 5,0 2.46159,1.612454 4.46232,0 3,2 -7.04712,9.09562 -7.95986,12.054235 -8.853494,15.667911 -1.042121,2.029207 -1.419205,6.487158 -0.732184,13.335033 0.242674,4.991216 2.649098,7.614875 3.987688,12.335231 3.10243,4.440024 9.10625,4.613859 13.47099,7.241525 3.26456,4.09204 -5.55687,3.69083 -8.00337,3.10394 -4.9096,-0.7905 -10.055256,-1.79726 -13.767607,-5.394904 C 89.621267,94.812035 90.712929,89.970755 90,85 90.284892,79.493371 85.701661,73.05675 80,75 c -8.306383,1.44458 -16.590992,1.336597 -24.960488,2.011547 -5.803563,0.118458 -3.602081,1.693594 -7.925163,7.46418 C 42.379087,90.980488 37.282612,97.091245 30,100 25.146298,102.2542 18.682588,103.80308 13.590472,101.2212 8.4392163,98.077132 15.076427,101.82802 20,100 c 6.458756,-1.381564 8.768813,-3.946154 15,-10 5.585312,-7.383552 7.523047,-9.991703 8.358102,-13.128726 -7.308109,0.08171 -14.732584,0.489292 -21.97663,1.552331 -4.814869,1.723223 -11.5367041,3.350384 -12.3715209,9.285691 -0.085774,2.075828 0.5544981,5.734419 -2.555475,5.801967 z M 47.977736,67.179962 c 5.218026,-1.45654 6.558186,1.125681 6.608421,-1.657348 C 55.587587,61.58231 58.045264,58.492366 60,55 c 2.352086,-3.585403 5.438271,-7.33576 8.409913,-10.468031 2.323912,-2.703053 4.623773,-5.593714 7.912586,-7.160479 5.004818,-2.900938 9.987381,-6.324186 15.83368,-7.164291 3.285086,-0.512833 7.314523,0.04376 9.493221,2.698591 1.21824,1.901892 1.93203,1.09421 2.55245,6.680419 C 103.44901,44 103.48836,43 105.60971,43.470573 111,45 111,48 111.80216,41.151121 c 0.75316,-4.016571 -0.10543,-8.627027 -1.37918,-12.58187 -0.99493,-2.414976 -2.46506,-4.53943 -4.50706,-6.18814 C 102.54876,20.006889 99,19 95,20 89,21 84.393566,22.472279 80,25 76,28 72,31.449008 67.768158,35.579361 60,43 59,44.590349 55,50 c -5,7 -4.549164,8 -7.972955,16.46477 -0.70482,0.534679 0.722479,0.511105 0.950691,0.715192 z" />

    <path fill="currentColor" d="M 29.989723,60.451726 35.001856,44.964807 53.367539,36.680344 35,30 30.14715,13.223815 25,30 C 25,30 4.7227911,37.310049 5.5099229,37.310049 6.2970547,37.310049 25,45 25,45 Z" />
  </svg>
</a>

    {{- /* 2. 返回上一级按钮 */ -}}
<a href="#" onclick="history.back(); return false;"
     id="scroll-to-parent" aria-label="Go to Parent Directory" title="返回" class="scroll-btn parent-btn" >
        <span class="btn-icon"></span>
    </a>


    {{- /* 3. 回到顶部按钮 */ -}}
    <a href="#top" id="scroll-to-top" aria-label="Go to Top" title="顶部" class="scroll-btn top-btn" >
        <span class="btn-icon"></span>
    </a>

    {{- /* ==========================================
         4. 新增:主题切换按钮 (正确地放在容器内部)
       ========================================== */ -}}
    <a href="javascript:void(0)" id="scroll-theme-toggle" aria-label="Toggle Theme" title="光暗" class="scroll-btn theme-toggle-vertical">
        <span class="btn-icon">
            {{- /* 月亮图标 */ -}}
            <svg id="moon-icon-vertical" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
            </svg>
            {{- /* 太阳图标 */ -}}
            <svg id="sun-icon-vertical" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                <circle cx="12" cy="12" r="5"></circle>
                <line x1="12" y1="1" x2="12" y2="3"></line>
                <line x1="12" y1="21" x2="12" y2="23"></line>
                <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
                <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
                <line x1="1" y1="12" x2="3" y2="12"></line>
                <line x1="21" y1="12" x2="23" y2="12"></line>
                <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
                <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
            </svg>
        </span>
    </a>
    {{- /* ========================================== */ -}}

</div> <!-- 这是主容器正确的结束位置 -->

{{- /* 3. 滚动按钮逻辑脚本 (IIFE 封装) */ -}}
<script>
(function() {
    // navigateToParent 函数定义在这里 (如果需要的话)
    function navigateToParent() {
        var level = 2; // 硬编码:回退两级
        var path = decodeURIComponent(window.location.pathname);
        if (path.endsWith('/')) path = path.slice(0, -1);

        var segments = path.split('/').filter(function(p) { return p.length > 0; });

        if (segments.length <= level) {
            window.location.href = "/";
            return;
        }

        var newSegments = segments.slice(0, segments.length - level);
        window.location.href = "/" + newSegments.join("/") + "/";
    }

    // --- DOM 加载后绑定事件 ---
    document.addEventListener("DOMContentLoaded", function() {
        var buttonsContainer = document.getElementById("scroll-buttons");
        var parentBtn = document.getElementById("scroll-to-parent");
        var topBtn = document.getElementById("scroll-to-top");
        var themeToggleBtn = document.getElementById("scroll-theme-toggle");

        // A. 绑定返回按钮
        if (parentBtn) {
            parentBtn.addEventListener("click", function(e) {
                e.preventDefault();
                navigateToParent();
            });
        }

        // B. 绑定回到顶部
        if (topBtn) {
            topBtn.addEventListener("click", function(e) {
                e.preventDefault();
                window.scrollTo({ top: 0, behavior: "smooth" });
            });
        }

        // C. 绑定主题切换按钮
        if (themeToggleBtn) {
            var headerThemeToggle = document.getElementById("theme-toggle");
            if (headerThemeToggle) {
                themeToggleBtn.addEventListener("click", function() {
                    headerThemeToggle.click();
                });
            }
        }

        // D. 按钮显示/隐藏逻辑
        if (buttonsContainer) {
            function toggleVisibility() {
                var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
                if (scrollTop > 600) {
                    buttonsContainer.classList.add("visible");
                } else {
                    buttonsContainer.classList.remove("visible");
                }
            }
            toggleVisibility();
            window.addEventListener("scroll", toggleVisibility, { passive: true });
        }
    });
})();
</script>
{{- end }}

DONE partials/index_profile.html 主页待优化

<div class="profile">
    {{- with site.Params.profileMode }}
    <div class="profile_inner">
        {{- if .imageUrl -}}
        {{- /* 图片处理代码保持不变 */ -}}
        {{- $img := "" }}
        {{- if not (urls.Parse .imageUrl).IsAbs }}
            {{- $img = resources.Get .imageUrl }}
        {{- end }}
        {{- if $img }}
            {{- $processableFormats := (slice "jpg" "jpeg" "png" "tif" "bmp" "gif") -}}
            {{- if hugo.IsExtended -}}
                {{- $processableFormats = $processableFormats | append "webp" -}}
            {{- end -}}
            {{- $prod := (hugo.IsProduction | or (eq site.Params.env "production")) }}
            {{- if and (in $processableFormats $img.MediaType.SubType) (eq $prod true)}}
                {{- if (not (and (not .imageHeight) (not .imageWidth))) }}
                    {{- $img = $img.Resize (printf "%dx%d" .imageWidth .imageHeight) }}
                {{- else if .imageHeight }}
                    {{- $img = $img.Resize (printf "x%d" .imageHeight) }}
                {{ else if .imageWidth }}
                    {{- $img = $img.Resize (printf "%dx" .imageWidth) }}
                {{ else }}
                    {{- $img = $img.Resize "150x150" }}
                {{- end }}
            {{- end }}
            <img draggable="false" src="{{ $img.Permalink }}" alt="{{ .imageTitle | default "profile image" }}" title="{{ .imageTitle }}"
                height="{{ .imageHeight | default 150 }}" width="{{ .imageWidth | default 150 }}" />
        {{- else }}
        <img draggable="false" src="{{ .imageUrl | absURL }}" alt="{{ .imageTitle | default "profile image" }}" title="{{ .imageTitle }}"
            height="{{ .imageHeight | default 150 }}" width="{{ .imageWidth | default 150 }}" />
        {{- end }}
        {{- end }}

        <h1>{{ .title | default site.Title | markdownify }}</h1>
        <span>{{ .subtitle | markdownify }}</span>

        {{- /* 在这里添加搜索框 - 在 social icons 之前 */ -}}
        <div id="searchbox" style="width: 100%; max-width: 600px; margin: 20px auto;">
            <input id="searchInput" placeholder="搜索全站内容..." aria-label="search" type="search" autocomplete="off" maxlength="64">
            <ul id="searchResults" aria-label="search results"></ul>
        </div>

        {{- partial "links.html" -}}

        {{- with .buttons }}
        <div class="buttons">
            {{- range . }}
            <a class="button" href="{{ trim .url " " }}" rel="noopener" title="{{ .name }}">
                <span class="button-inner">
                    {{ .name }}
                    {{- if (findRE "://" .url) }}&nbsp;
                    <svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
                        stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="14" width="14">
                        <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
                        <path d="M15 3h6v6"></path>
                        <path d="M10 14L21 3"></path>
                    </svg>
                    {{- end }}
                </span>
            </a>
            {{- end }}
        </div>
        {{- end }}
    </div>
    {{- end}}
</div>

代码块复制按钮逻辑

custom.css

/* 代码块复制按钮 */
.copy-code {
  display: none;
  position: absolute;
  top: 4px;
  right: 4px;
  color: rgba(255, 255, 255, 0.8);
  background: rgba(78, 78, 78, 0.8);
  border-radius: var(--radius);
  padding: 0 5px;
  font-size: 14px;
  user-select: none;
  cursor: pointer;
}

div.highlight:hover .copy-code,
pre:hover .copy-code {
  display: block;
}
{{- /* ==========================================
     6. 代码块复制按钮逻辑 (PaperMod 原生)
   ========================================== */ -}}
{{- if (and (eq .Kind "page") (ne .Layout "archives") (ne .Layout "search") (.Param "ShowCodeCopyButtons")) }}
<script>
    document.querySelectorAll('pre > code').forEach((codeblock) => {
        const container = codeblock.parentNode.parentNode;

        const copybutton = document.createElement('button');
        copybutton.classList.add('copy-code');
        copybutton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';

        function copyingDone() {
            copybutton.innerHTML = '{{- i18n "code_copied" | default "copied!" }}';
            setTimeout(() => {
                copybutton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
            }, 2000);
        }

        copybutton.addEventListener('click', (cb) => {
            if ('clipboard' in navigator) {
                navigator.clipboard.writeText(codeblock.textContent);
                copyingDone();
                return;
            }

            const range = document.createRange();
            range.selectNodeContents(codeblock);
            const selection = window.getSelection();
            selection.removeAllRanges();
            selection.addRange(range);
            try {
                document.execCommand('copy');
                copyingDone();
            } catch (e) { };
            selection.removeRange(range);
        });

        if (container.classList.contains("highlight")) {
            container.appendChild(copybutton);
        } else if (container.parentNode.firstChild == container) {
            // td containing LineNos
        } else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
            // table containing LineNos and code
            codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copybutton);
        } else {
            // code blocks not having highlight as parent class
            codeblock.parentNode.appendChild(copybutton);
        }
    });
</script>
{{- end }}

是否显示网站logo(小)

1 header.html

    <header class="header">
    <nav class="nav">
    <div class="logo">
    {{- $label_text := site.Params.label.text | default site.Title -}}
{{- if site.Title -}}
    <a href="{{ "" | absLangURL }}" accesskey="h" title="{{ $label_text }} (Alt + H)">

{{- /* --- 修改开始:图标逻辑 --- */ -}}
{{- if not .IsHome -}} {{/* 1. 只有非主页才尝试显示图标 */}}
{{- with site.Params.label.icon -}}
{{- $img := resources.Get . }}
{{- if $img -}}
    <img src="{{ $img.Permalink }}" alt="logo" height="{{ site.Params.label.iconHeight | default "30" }}">
    {{- else -}}
    <img src="{{ . | absURL }}" alt="logo" height="{{ site.Params.label.iconHeight | default "30" }}">
    {{- end -}}
{{- end -}}
{{- end -}} {{/* 1. 结束非主页判断 */}}
{{- /* --- 修改结束 --- */ -}}

{{- /* 文字始终显示 */ -}}
{{- $label_text -}}
</a>
    {{- end -}} {{/* 闭合 if site.Title */}}
    <div class="logo-switches">
    {{- if not site.Params.disableThemeToggle -}}
    <button id="theme-toggle" accesskey="t" title="白天/黑夜" aria-label="Toggle theme">
    <svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
    </svg>
    <svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <circle cx="12" cy="12" r="5"></circle>
    <line x1="12" y1="1" x2="12" y2="3"></line>
    <line x1="12" y1="21" x2="12" y2="23"></line>
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
    <line x1="1" y1="12" x2="3" y2="12"></line>
    <line x1="21" y1="12" x2="23" y2="12"></line>
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
    </svg>
    </button>
    {{- end -}}
</div>
    </div>

导航栏 顶部 返回按钮

custom.css

/* 让导航栏的子元素平均分布 */
.nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}

/* 按钮容器,用于占位和居中 */
.nav-center {
  flex-grow: 1;
  display: flex;
  justify-content: center;
}

/* 返回按钮本身 (优化版) */
.nav-back-button {
  height: 30px;
  line-height: 28px;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--tertiary);
  border-radius: var(--radius);
  font-size: 14px;
  font-weight: 500;
  color: var(--primary);
  background-color: var(--background);
  text-decoration: none;
  transition: background-color 0.2s, color 0.2s, border-color 0.2s;
  cursor: pointer;
  box-sizing: border-box;
}

/* 鼠标悬停效果 */
.nav-back-button:hover {
  background-color: var(--tertiary);
  color: var(--theme);
  border-color: var(--tertiary);
}

/* 在小屏幕上(例如手机),隐藏返回按钮以避免布局拥挤 */
@media (max-width: 768px) {
  .nav-center {
    display: none;
  }
}

2 header.html

{{- if not .IsHome -}}
    <div class="nav-center">
        <a href="#" onclick="history.back(); return false;" class="nav-back-button" title="返回上一页">返回</a>
    </div>
    {{- end -}}

阅读进度条

3 header.html

<div id="reading-progress" style="position: fixed; top: 0; left: 0; height: 3px; background: var(--custom-emphasis); width: 0%; z-index: 9999; transition: width 0.1s;"></div>
{{- /* ==========================================
     4. 阅读进度条逻辑
   ========================================== */ -}}
<script>
(function() {
    window.addEventListener('scroll', function() {
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        var scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
        var bar = document.getElementById('reading-progress');
        if (bar && scrollHeight > 0) {
            bar.style.width = ((scrollTop / scrollHeight) * 100) + "%";
        }
    }, { passive: true });
})();
</script>

暗色亮色主题切换

4 header.html





{{- if not site.Params.disableThemeToggle -}}
{{- $dt := site.Params.defaultTheme | default "auto" -}}
    <script>
    (function() {
        // 使用 sessionStorage 替代 localStorage
        const pref = sessionStorage.getItem("pref-theme");
        const systemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        const defaultTheme = "{{ $dt }}";

        const setDark = () => {
            document.body.classList.add('dark');
            document.body.classList.remove('light');
        };

        const setLight = () => {
            document.body.classList.remove('dark');
            document.body.classList.add('light');
        };

        if (pref === "dark") {
            setDark();
        } else if (pref === "light") {
            setLight();
        } else {
            if (defaultTheme === "dark") {
                setDark();
            } else if (defaultTheme === "light") {
                if (systemDark) setLight();
                else setLight();
            } else {
                // auto
                if (systemDark) setDark();
                else setLight();
            }
        }
    })();
</script>
    {{- end -}}
{{- /* ==========================================
     5. 主题切换逻辑 (保留 SessionStorage 设置)
   ========================================== */ -}}
{{- if (not site.Params.disableThemeToggle) }}
<script>
(function() {
    var toggleBtn = document.getElementById("theme-toggle");
    if (toggleBtn) {
        toggleBtn.addEventListener("click", function() {
            if (document.body.classList.contains("dark")) {
                document.body.classList.remove('dark');
                document.body.classList.add('light');
                sessionStorage.setItem("pref-theme", 'light');
            } else {
                document.body.classList.remove('light');
                document.body.classList.add('dark');
                sessionStorage.setItem("pref-theme", 'dark');
            }
        });
    }
})();
</script>
{{- end }}

header.css

.nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    max-width: calc(var(--nav-width) + var(--gap) * 2);
    margin-inline-start: auto;
    margin-inline-end: auto;
    line-height: var(--header-height);
}

.nav a {
    display: block;
}

.logo,
#menu {
    display: flex;
    margin: auto var(--gap);
}

.logo {
    flex-wrap: inherit;
}

.logo a {
    font-size: 24px;
    font-weight: 700;
}

.logo a img, .logo a svg {
    display: inline;
    vertical-align: middle;
    pointer-events: none;
    transform: translate(0, -10%);
    border-radius: 6px;
    margin-inline-end: 8px;
}

button#theme-toggle {
    font-size: 26px;
    margin: auto 4px;
}

body.dark #moon {
    vertical-align: middle;
    display: none;
}

body:not(.dark) #sun {
    display: none;
}

亮色暗色代码块 extend-head

<style>
/*
  亮色主题: Emacs
  默认生效
*/
/* Generated using Hugo Chroma Highlighting | Theme: Emacs (Modified) */

/* Background and General */
.bg { color: #000000; background-color: #f8f8f8; }
.chroma { color: #000000; background-color: #f8f8f8; }

/* Error */
.chroma .err { color: #a61717; background-color: #e3d2d2 }

/* Line numbers and Table */
.chroma .lnlinks { outline:none; text-decoration:none; color:inherit }
.chroma .lntd { vertical-align:top; padding:0; margin:0; border:0; }
.chroma .lntable { border-spacing:0; padding:0; margin:0; border:0; }
.chroma .hl { background-color: #dfdfdf; display: block; width: 100%; }
.chroma .lnt { white-space:pre; -webkit-user-select:none; user-select:none; margin-right:0.4em; padding:0 0.4em 0 0.4em; color:#7f7f7f }
.chroma .ln { white-space:pre; -webkit-user-select:none; user-select:none; margin-right:0.4em; padding:0 0.4em 0 0.4em; color:#7f7f7f }
.chroma .line { display:flex; }

/* Keywords */
.chroma .k, .chroma .kc, .chroma .kd, .chroma .kn, .chroma .kr, .chroma .ow { color: #a2f; font-weight: bold }
.chroma .kp { color: #a2f }
.chroma .kt { color: #0b0; font-weight: bold }

/* Name (Classes, Functions, Variables) */
.chroma .n, .chroma .py { color: #000 }
.chroma .na { color: #b44 }
.chroma .nc, .chroma .nn { color: #00f; text-decoration: none } /* 类名与命名空间 */
.chroma .no { color: #b8860b }
.chroma .nd { color: #af5f00 }
.chroma .ni { color: #999; font-weight: bold }
.chroma .ne { color: #d2413a; font-weight: bold }
.chroma .nf, .chroma .fm { color: #00a000 } /* 函数名 */
.chroma .nl { color: #a0a000 }
.chroma .nn { color: #00f; font-weight: bold }
.chroma .nt { color: #008000; font-weight: bold }
.chroma .nv, .chroma .vc, .chroma .vg, .chroma .vi, .chroma .vm { color: #b8860b } /* 变量名 */
.chroma .nb, .chroma .bp { color: #a2f } /* 内建函数/伪类 */

/* Literal Strings */
.chroma .s, .chroma .sa, .chroma .sb, .chroma .sc, .chroma .dl, .chroma .s2, .chroma .sh, .chroma .si, .chroma .sx, .chroma .sr, .chroma .s1, .chroma .ss { color: #b44 }
.chroma .sd { color: #b44; font-style: italic } /* 文档字符串 */
.chroma .se { color: #b44; font-weight: bold } /* 转义字符 */

/* Literal Numbers */
.chroma .m, .chroma .mb, .chroma .mf, .chroma .mh, .chroma .mi, .chroma .il, .chroma .mo { color: #666 }

/* Operator and Punctuation */
.chroma .o { color: #666 }
.chroma .p { color: #000 }

/* Comment */
.chroma .c, .chroma .ch, .chroma .cm, .chroma .c1, .chroma .cp, .chroma .cpf { color: var(--secondary) ; font-style: italic }
.chroma .cs { color: #008000; font-weight: bold; font-style: italic }

/* Generic (Diffs etc.) */
.chroma .gd { color: #a00000; background-color: #fff0f0 } /* Deleted */
.chroma .ge { font-style: italic } /* Emphasized */
.chroma .gr { color: #ff0000 } /* Error */
.chroma .gh { color: #000080; font-weight: bold } /* Heading */
.chroma .gi { color: #00a000; background-color: #f0fff0 } /* Inserted */
.chroma .go { color: #888888 } /* Output */
.chroma .gp { color: #c65d09; font-weight: bold } /* Prompt */
.chroma .gs { font-weight: bold } /* Strong */
.chroma .gu { color: #800080; font-weight: bold } /* Subheading */
.chroma .gt { color: #0044dd } /* Traceback */
.chroma .gl { text-decoration: underline } /* Underline */

/* Text Whitespace */
.chroma .w { color: #bbbbbb }

/*
  暗色主题: Base16 Snazzy
  当 <html> 或 <body> 标签有 .dark 类时生效
*/

/* Background and General */
.dark .bg { color: #e2e4e5; background-color: #282a36; }
.dark .chroma { color: #e2e4e5; background-color: #282a36; }

/* Error */
.dark .chroma .err { color: #ff5c57 }

/* Line numbers and Table Structure */
.dark .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
.dark .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
.dark .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
.dark .chroma .hl { background-color: #3d3f4a; display: block; width: 100%; }
.dark .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em; color: #7f7f7f }
.dark .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em; color: #7f7f7f }
.dark .chroma .line { display: flex; }

/* Keywords */
.dark .chroma .k, .dark .chroma .kc, .dark .chroma .kn, .dark .chroma .kp, .dark .chroma .kr, .dark .chroma .ow { color: #ff6ac1; font-weight: normal; }
.dark .chroma .kd { color: #ff5c57; font-weight: normal; }
.dark .chroma .kt { color: #9aedfe; font-weight: normal; }

/* Names (Functions, Classes, Variables) */
.dark .chroma .n, .dark .chroma .py { color: #e2e4e5; }
.dark .chroma .nf, .dark .chroma .fm { color: #57c7ff; } /* 函数名 */
.dark .chroma .nc, .dark .chroma .nn { color: #f3f99d; } /* 类名、命名空间 */
.dark .chroma .nb, .dark .chroma .bp { color: #ff5c57; } /* 内建名称 */
.dark .chroma .nv, .dark .chroma .vc, .dark .chroma .vg, .dark .chroma .vi, .dark .chroma .vm { color: #ff5c57; } /* 变量名 */
.dark .chroma .no { color: #ff9f43; } /* 常量 */
.dark .chroma .nd { color: #ff9f43; } /* 装饰器 */
.dark .chroma .ni { color: #e2e4e5; }
.dark .chroma .ne { color: #ff5c57; }
.dark .chroma .nl { color: #ff5c57; }
.dark .chroma .nt { color: #ff6ac1; } /* 标签名 (HTML/XML) */
.dark .chroma .na { color: #57c7ff; } /* 属性名 */

/* Literal Strings */
.dark .chroma .s, .dark .chroma .sa, .dark .chroma .sb, .dark .chroma .sc, .dark .chroma .dl, .dark .chroma .s2, .dark .chroma .sh, .dark .chroma .si, .dark .chroma .sx, .dark .chroma .sr, .dark .chroma .s1, .dark .chroma .ss { color: #5af78e; }
.dark .chroma .sd { color: #5af78e; font-style: normal; } /* 文档字符串 */
.dark .chroma .se { color: #5af78e; } /* 转义字符 */

/* Literal Numbers */
.dark .chroma .m, .dark .chroma .mb, .dark .chroma .mf, .dark .chroma .mh, .dark .chroma .mi, .dark .chroma .il, .dark .chroma .mo { color: #ff9f43; }

/* Operator and Punctuation */
.dark .chroma .o { color: #ff6ac1; }
.dark .chroma .p { color: #e2e4e5; }

/* Comment */
.dark .chroma .c, .dark .chroma .ch, .dark .chroma .cm, .dark .chroma .c1, .dark .chroma .cp, .dark .chroma .cpf, .dark .chroma .cs { color: #78787e; font-style: normal; }

/* Generic (Diffs etc.) */
.dark .chroma .gd { color: #ff5c57 } /* Deleted */
.dark .chroma .gi { color: #5af78e; font-weight: bold } /* Inserted */
.dark .chroma .gh { font-weight: bold } /* Heading */
.dark .chroma .gu { font-weight: bold } /* Subheading */
.dark .chroma .ge { text-decoration: underline } /* Emphasized */
.dark .chroma .gs { font-style: italic } /* Strong */
.dark .chroma .gl { text-decoration: underline } /* Underline */
.dark .chroma .go { color: #43454f } /* Output */
.dark .chroma .gp { color: #ff9f43; font-weight: bold } /* Prompt */
.dark .chroma .gt { color: #ff5c57 } /* Traceback */

/* Text Whitespace */
.dark .chroma .w { color: #43454f }
</style>

并列例子 extend-head

extend_head.html


{{ $js := resources.Get "js/tabs.js" | minify | fingerprint }}
<script src="{{ $js.RelPermalink }}" defer></script>

{{/* 正文折叠脚本 */}}
{{ $content_fold := resources.Get "js/content-fold.js" | minify | fingerprint }}
<script src="{{ $content_fold.RelPermalink }}" defer></script>

css

/* ========== PaperMod 风格 Tabs 组件 (按钮间虚线连接样式) ========== */
.tabs {
    margin: 2em 0;
}

.tab-buttons {
    display: flex;
    /* 关键: 设置按钮之间的间距,这个值需要和下面伪元素的宽度匹配 */
    gap: 40px;
    margin-bottom: 1em;
    padding-left: 0;
    list-style: none;
    align-items: center; /* 确保所有按钮垂直居中对齐 */
}

.tab-btn {
    /* 关键: 将按钮设置为相对定位,为其伪元素提供定位参考 */
    position: relative;

    /* 保留原始按钮样式 */
    padding: 8px 16px;
    cursor: pointer;
    color: var(--primary);
    border: 1px solid var(--tertiary);
    border-radius: var(--radius);
    user-select: none;
    background: var(--background);
}

/*
 * 核心部分: 使用 ::after 伪元素来创建连接线
 * 这会在每个 .tab-btn 元素之后创建一个“虚拟”的元素,我们可以把它样式化成一条线
*/
.tab-btn::after {
    content: ''; /* 伪元素必须有 content 属性才能显示 */
    position: absolute; /* 绝对定位,相对于 .tab-btn */

    /* 定位到按钮的右侧中间 */
    top: 50%;
    left: 100%; /* 从按钮的右边缘开始 */

    /* 绘制虚线 */
    border-top: 2px dashed var(--tertiary);
    /*其他常见的样式还有:
      solid: 实线 (-)
      dotted: 点线 (...)
      double: 双实线 (=)*/

    /* 线的长度,必须和上面 .tab-buttons 的 gap 值一致 */
    width: 40px;
}

/*
 * 最后一个按钮不需要向右的连接线,所以把它隐藏掉
*/
.tab-btn:last-child::after {
    display: none;
}


/* --- active 状态的样式保持不变 --- */
.tab-btn.active {
    background: var(--tertiary);
    color: var(--theme);
}

.tab-content {
    display: none;
    padding: 1em;
    border: 1px solid var(--tertiary);
    border-radius: var(--radius);
    background: var(--entry);
}

.tab-content.active {
    display: block;
}

tabs.js标签页切换功能

document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".tabs").forEach((tabset) => {
const buttons = tabset.querySelectorAll(".tab-btn");
const contents = tabset.querySelectorAll(".tab-content");
function activate(i) {
      buttons.forEach(btn => btn.classList.remove("active"));
      contents.forEach(c => c.classList.remove("active"));

      if (buttons[i]) buttons[i].classList.add("active");
      if (contents[i]) contents[i].classList.add("active");
  }

  buttons.forEach((btn, index) => {
      btn.addEventListener("click", () => activate(index));
  });

  // 默认激活第一个标签
  activate(0);
});
});

tab.html标签页组件

{{ $name := .Get "name" | default "未命名 Tab" }}
{{/* 检查是否在 tabs shortcode 内部 */}}
{{ if .Parent }}
{{ .Parent.Scratch.Add "tab_labels" (slice $name) }}
{{ else }}
{{ errorf "tab shortcode must be nested inside tabs shortcode. See %s" .Position }}
{{ end }}
<div class="tab-content">
    {{ .Inner  | .Page.RenderString }}
</div>

tabs.html

{{/* 先渲染一次 .Inner 让子 shortcode 填充 Scratch */}}
{{ .Inner }}
<div class="tabs" id="{{ .Get "id" | default (printf "tabs-%d" (now.UnixNano)) }}">
    <ul class="tab-buttons">
        {{ range $i, $tab := .Scratch.Get "tab_labels" }}
        <li class="tab-btn {{ if eq $i 0 }}active{{ end }}" data-tab="{{ $i }}">{{ $tab }}</li>
        {{ end }}
    </ul>
<div class="tab-contents">
    {{/* 再次渲染以显示内容 */}}
    {{ .Inner }}
</div>
</div>

DONE 在社交图标上显示图片

<div class="social-icons" {{ with .align}}align="{{.}}" {{- end }}>
    {{- range site.Params.socialIcons }}
    <div class="social-icon-wrapper">
        {{- if .qrCode }}
        <a href="javascript:void(0)" class="qr-code-trigger"
           data-qr="{{ .qrCode | absURL }}"
           title="{{ (.title | default .name) | title }}">
            {{ partial "svg.html" . }}
        </a>
        {{- else }}
        {{- $isExternal := or (hasPrefix .url "http://") (hasPrefix .url "https://") (hasPrefix .url "//") -}}
        <a href="{{ trim .url " " | safeURL }}" {{ if $isExternal }}target="_blank" rel="noopener noreferrer me"{{ end }}
            title="{{ (.title | default .name) | title }}">
            {{ partial "svg.html" . }}
        </a>
        {{- end }}
    </div>
    {{- end }}
</div>
<div id="qr-modal" class="qr-modal">
    <div class="qr-modal-content">
        <img id="qr-modal-image" src="" alt="QR Code">
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const modal = document.getElementById('qr-modal');
    const modalImg = document.getElementById('qr-modal-image');

    let hideTimeout = null;
    let showTimeout = null; // 【新增】用于记录显示的定时器
    let currentTrigger = null;

    const SHOW_DELAY = 300; // 【新增】鼠标悬停多久才显示(毫秒),可根据手感自行调整(建议300-500)

    // 检测设备是否支持 Hover
    const canHover = window.matchMedia('(hover: hover)').matches;

    // --- 核心定位函数 (现在对 PC 和 Mobile 都生效) ---
    function positionModal() {
        if (!currentTrigger) return;

        const rect = currentTrigger.getBoundingClientRect();
        const modalWidth = modal.offsetWidth;
        const modalHeight = modal.offsetHeight;

        // 1. 水平居中 (图标中心 - 气泡中心)
        let left = rect.left + (rect.width / 2) - (modalWidth / 2);

        // 2. 垂直定位 (图标顶部 - 气泡高度 - 箭头间隙)
        let top = rect.top - modalHeight - 12;

        // --- 边界检查 (非常重要!) ---

        // 左边缘
        if (left < 10) left = 10;

        // 右边缘 (屏幕宽度 - 气泡宽度 - 边距)
        if (left + modalWidth > window.innerWidth - 10) {
            left = window.innerWidth - modalWidth - 10;
        }

        // 上边缘 (核心修改!)
        // 如果计算出的 top 值小于 10px (太靠上或已超出),就把它固定在 10px 的位置
        if (top < 10) {
            top = 10;
        }

        modal.style.left = left + 'px';
        modal.style.top = top + 'px';
    }

    // --- 显示函数 ---
    function showModal(triggerElement) {
        currentTrigger = triggerElement;
        const qrUrl = triggerElement.getAttribute('data-qr');
        if (!qrUrl) return;

        // 检查图片是否需要更新
        if (modalImg.src !== qrUrl && !modalImg.src.endsWith(qrUrl)) {
            modal.classList.remove('active'); // 先隐藏,防止尺寸突变

            modalImg.onload = function() {
                // 图片加载完成,尺寸确定后,再计算位置并显示
                positionModal();
                modal.classList.add('active');
                modalImg.onload = null;
            };
            modalImg.src = qrUrl;
        } else {
            // 如果是同一张图,直接重新定位并显示
            requestAnimationFrame(() => {
                positionModal();
                modal.classList.add('active');
            });
        }
    }

    function hideModal() {
        modal.classList.remove('active');
        currentTrigger = null;
    }

    // --- 事件监听 ---
    document.querySelectorAll('.qr-code-trigger').forEach(trigger => {
        if (canHover) {
            // PC端
            trigger.addEventListener('mouseenter', function() {
                // 【修改】清除可能正在倒计时的隐藏定时器
                if (hideTimeout) clearTimeout(hideTimeout);

                // 【修改】延迟执行显示逻辑
                const _this = this; // 保存当前触发元素的引用
                showTimeout = setTimeout(function() {
                    showModal(_this);
                }, SHOW_DELAY);
            });

            trigger.addEventListener('mouseleave', function() {
                // 【新增核心】如果鼠标离开时,显示倒计时还没结束,说明用户只是路过,清除显示计划!
                if (showTimeout) clearTimeout(showTimeout);

                hideTimeout = setTimeout(hideModal, 300);
            });
        } else {
            // 移动端/触屏保持原样
            trigger.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                // 如果点击的是当前已激活的图标,则关闭;否则显示新的
                if (modal.classList.contains('active') && currentTrigger === this) {
                    hideModal();
                } else {
                    showModal(this);
                }
            });
        }
    });

    // 交互优化:鼠标移入气泡不消失 (仅 PC)
    if (canHover) {
        modal.addEventListener('mouseenter', () => {
            if (hideTimeout) clearTimeout(hideTimeout);
        });
        modal.addEventListener('mouseleave', () => {
            hideTimeout = setTimeout(hideModal, 300);
        });
    } else {
        // 移动端点击空白关闭
        document.addEventListener('click', (e) => {
            if (!e.target.closest('.qr-code-trigger') && !e.target.closest('.qr-modal')) {
                hideModal();
            }
        });
    }

    // 页面滚动或窗口大小改变时,隐藏或重新定位
    window.addEventListener('scroll', () => {
        if (modal.classList.contains('active')) hideModal();
    }, { passive: true });

    window.addEventListener('resize', () => {
        if (modal.classList.contains('active')) positionModal();
    });
});
</script>

custom.css

/* 修复社交图标水平排列 */
.social-icon-wrapper {
     display: inline-flex;
}
/* ================= 通用样式 (PC & Mobile) ================= */
.qr-modal {
    display: block;
    visibility: hidden;
    opacity: 0;
    position: fixed;
    z-index: 1000;
    background-color: var(--entry, #fff);
    border-radius: 6px;

    /* 统一的内边距 */
    padding: 8px;

    box-shadow: 0 8px 20px rgba(0,0,0,0.15);
    border: 1px solid var(--border, #f0f0f0);

    width: auto;
    height: auto;

    /* 动画 */
    transform: translateY(10px);
    transition: opacity 0.3s, transform 0.3s, visibility 0.3s;
    pointer-events: none;

    box-sizing: border-box;
}

.qr-modal.active {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
}

.qr-modal-content {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 0;
    margin: 0;
    padding: 0;
}

.qr-modal-close, .close {
    display: none !important;
}

/* 底部小箭头 (通用) */
.qr-modal::after {
    content: '';
    position: absolute;
    bottom: -6px;
    left: 50%;
    margin-left: -6px;
    transform: rotate(45deg);
    width: 12px;
    height: 12px;
    background-color: var(--entry, #fff);
    border-bottom: 1px solid var(--border, #f0f0f0);
    border-right: 1px solid var(--border, #f0f0f0);
    z-index: 1001;
}

/* ================= PC端图片样式 ================= */
#qr-modal-image {
    display: block;
    /* PC端限制最大高度为150px */
    max-height: 150px;
    width: auto;
    border-radius: 4px;
    background-color: var(--code-bg, #f5f5f5);
}

/* ================= 移动端适配 (核心修改) ================= */
@media (max-width: 768px) {
    /* 移动端优化:去掉边框和箭头 */
    .qr-modal {
        /* 手机端去掉边框 */
        border: none !important;

        /* 统一内边距 */
        padding: 8px !important;

        /* 调整阴影,使模态框更轻盈 */
        box-shadow: 0 6px 16px rgba(0,0,0,0.12) !important;

        /* 不再设置固定宽度,让内容决定宽度 */
        width: auto !important;
        min-width: auto !important;

        /* 最大宽度限制,避免过大 */
        max-width: 85vw !important;
    }

    /* 手机端隐藏小箭头 */
    .qr-modal::after {
        display: none !important;
    }

    #qr-modal-image {
        /* 关键修改:图片宽度自适应,但不超过容器 */
        width: auto !important;
        height: auto !important;

        /* 设置最大尺寸限制 - 重要:与PC端保持一致的最大高度 */
        max-height: 150px !important; /* 保持与PC端一致 */
        max-width: 85vw !important; /* 最大宽度不超过视口85% */

        /* 保持图片原始比例 */
        object-fit: contain;

        /* 移除不必要的圆角 */
        border-radius: 2px;

        /* 确保图片没有边距 */
        margin: 0 !important;
        padding: 0 !important;

        /* 图片背景色调整,适应无边框设计 */
        background-color: var(--theme, #fafbfc);
    }

    /* 针对小屏幕的进一步优化 */
    @media (max-width: 480px) {
        .qr-modal {
            /* 减小内边距 */
            padding: 6px !important;
            max-width: 90vw !important;

            /* 更轻的阴影 */
            box-shadow: 0 5px 12px rgba(0,0,0,0.1) !important;
        }

        #qr-modal-image {
            /* 保持最大高度150px,但稍微调整最大宽度 */
            max-height: 150px !important;
            max-width: 90vw !important;
        }
    }

    /* 针对超小屏幕 */
    @media (max-width: 375px) {
        .qr-modal {
            /* 进一步减小内边距 */
            padding: 4px !important;
            max-width: 95vw !important;

            /* 更轻的阴影 */
            box-shadow: 0 4px 10px rgba(0,0,0,0.08) !important;
        }

        #qr-modal-image {
            /* 在超小屏幕上可以稍微减小最大高度 */
            max-height: 140px !important;
            max-width: 95vw !important;
        }
    }
}

/* ================= 暗色主题适配 ================= */
@media (prefers-color-scheme: dark) {
    .qr-modal {
        background-color: var(--code-block-bg, #2d2d2d);
        border-color: var(--tertiary, #555);
        box-shadow: 0 8px 20px rgba(0,0,0,0.35);
    }

    .qr-modal::after {
        background-color: var(--code-block-bg, #2d2d2d);
        border-color: var(--tertiary, #555);
    }

    #qr-modal-image {
        background-color: var(--primary, #1a1a1a);
    }
}

/* 如果你的主题有特定的暗色类(如.dark) */
.dark .qr-modal {
    background-color: var(--code-block-bg, #2d2d2d);
    border-color: var(--tertiary, #555);
    box-shadow: 0 8px 20px rgba(0,0,0,0.35);
}

.dark .qr-modal::after {
    background-color: var(--code-block-bg, #2d2d2d);
    border-color: var(--tertiary, #555);
}

.dark #qr-modal-image {
    background-color: var(--primary, #1a1a1a);
}

/* 移动端暗色主题适配 */
@media (max-width: 768px) and (prefers-color-scheme: dark) {
    .qr-modal {
        background-color: var(--entry, #1e1e1e) !important;
        box-shadow: 0 6px 16px rgba(0,0,0,0.25) !important;
    }

    #qr-modal-image {
        background-color: var(--theme, #1a1a1a) !important;
    }
}

@media (max-width: 768px) {
    .dark .qr-modal {
        background-color: var(--entry, #1e1e1e) !important;
        box-shadow: 0 6px 16px rgba(0,0,0,0.25) !important;
    }

    .dark #qr-modal-image {
        background-color: var(--theme, #1a1a1a) !important;
    }
}

DONE 在文章的作者上显示二维码和跳转网站

DONE 作者html

{{- if or .Params.author site.Params.author }}
{{- $authors := (.Params.author | default site.Params.author) }}
{{- $author_type := (printf "%T" $authors) }}
{{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
{{- range $i, $author := $authors }}
<span class="author-item">
  {{- if $i }}{{ ", " }}{{ end }}
  {{- $authorInfo := index site.Params.authors $author }}
  {{- if $authorInfo }}
    {{- /* 统一处理链接:优先用配置的url,无则用默认链接 */}}
    {{- $authorUrl := $authorInfo.url | default (printf "/info/tele/#:~:text=%s" ($authorInfo.name | default $author)) }}
    <a href="{{ $authorUrl }}"  rel="noopener noreferrer"
       class="author-link"{{- if $authorInfo.qrCode }} data-qr="{{ $authorInfo.qrCode | absURL }}"{{- end }}>
      {{ $authorInfo.name | default $author }}
    </a>
    {{- /* 添加彩蛋图标:仅当有二维码时显示,前面加·分隔符 */}}
    {{- if $authorInfo.qrCode }}
    <span style="margin: 0 4px; color: var(--secondary);">·</span> <!-- 新增:分隔符 · -->
    <span class="egg-icon qr-code-trigger" data-qr="{{ $authorInfo.qrCode | absURL }}" style="cursor: pointer; vertical-align: middle;">
      <img src="/egg.svg" onerror="this.src='/egg.png'" alt="彩蛋" style="height: 1em; width: 1em; display: inline-block;">
    </span>
    {{- end }}
  {{- else }}
    {{- /* 无配置的作者:默认链接 + 无彩蛋 */}}
    <a href="{{ printf "/info/tele/#:~:text=%s" $author }}"  rel="noopener noreferrer" class="author-link">
      <span class="author-name">{{ $author }}</span>
    </a>
  {{- end }}
</span>
{{- end }}
{{- else }}
{{- $authorInfo := $authors }}
{{- if reflect.IsMap $authorInfo }}
    {{- /* 单个作者的链接处理 */}}
    {{- $authorUrl := $authorInfo.url | default (printf "/info/tele/#:~:text=%s" $authorInfo.name) }}
    <a href="{{ $authorUrl }}"  rel="noopener noreferrer"
       class="author-link"{{- if $authorInfo.qrCode }} data-qr="{{ $authorInfo.qrCode | absURL }}"{{- end }}>
      {{ $authorInfo.name }}
    </a>
    {{- /* 单个作者的彩蛋图标:前面加·分隔符 */}}
    {{- if $authorInfo.qrCode }}
    <span style="margin: 0 4px; color: var(--secondary);">·</span> <!-- 新增:分隔符 · -->
    <span class="egg-icon qr-code-trigger" data-qr="{{ $authorInfo.qrCode | absURL }}" style="cursor: pointer; vertical-align: middle;">
      <img src="/egg.svg" onerror="this.src='/egg.png'" alt="彩蛋" style="height: 1em; width: 1em; display: inline-block;">
    </span>
    {{- end }}
{{- else }}
    {{- /* 纯文本作者名:默认链接 + 无彩蛋 */}}
    <a href="{{ printf "/info/tele/#:~:text=%s" $authorInfo }}"  rel="noopener noreferrer" class="author-link">
      <span class="author-name">{{ $authorInfo }}</span>
    </a>
{{- end }}
{{- end }}
{{- end -}}

扩展CSS样式

/* 作者交互样式 - 复用现有的二维码触发器样式 */
.author-name.qr-code-trigger {
    cursor: pointer;
    transition: color 0.2s;
    // border-bottom: 1px dashed var(--primary);
}

.author-name.qr-code-trigger:hover {
    color: var(--primary);
    // border-bottom: 1px dashed var(--primary);
}

/* 彩蛋图标基础样式 */
.egg-icon img {
    transition: filter 0.2s ease, opacity 0.2s ease;
    /* 新增:绑定抖动动画 */
    animation: egg-shake 0.2s ease-in-out 0.5s 5; /* 动画名 时长 缓动 延迟 次数 */
}

/* 暗色模式下反转图标颜色(黑色变白色) */
html.dark .egg-icon img,
body.dark .egg-icon img {
    filter: invert(1) brightness(1.2);
}

/* 彩蛋图标hover效果 */
.egg-icon:hover img {
    opacity: 0.8;
}

/* ========== 新增:抖动动画关键帧 ========== */
@keyframes egg-shake {
    0% { transform: rotate(0deg); }
    25% { transform: rotate(-7deg); } /* 向左微转 */
    50% { transform: rotate(0deg); }
    75% { transform: rotate(5deg); }  /* 向右微转 */
    100% { transform: rotate(0deg); }
}

使用说明

---
title = "为什么数学家喜欢对称性"
author = [
  { name = "snake", qrCode = "/images/snake-qr.png" },
  { name = "alice", url = "https://alice.example.com" },
  "bob"
]
---

在文章底部标签中添加分享和留言

css 样式

/* --- 自定义功能标签样式 --- */
.post-tags li a.action-tag {
  border: 1px solid  var(--fold-h2);
  color:  var(--fold-h2);
  font-weight: bold;
  background-color: transparent;
  transition: all 0.2s ease-in-out;
}

.post-tags li a.action-tag:hover {
  background-color: transparent;
}

留言中[点击此处]作者主页

picatria.org.cn:
  name: "picatria.org.cn"
  contact: "https://picatria.org.cn/about/"

snake:
  name: "snake"
  contact: "https://picatria.org.cn/tele/#:~:text=Snake"

.dir-locals.el emacs 的配置

;; -*- mode: emacs-lisp -*-
((org-mode . (
              ;; 隔离归档: 归档到当前目录的 archive.org
              (my/enable-weekly-archive . t)
  ;; --- ox-hugo 基础设置 ---
  (org-hugo-base-dir . "../")
  (org-hugo-section . ".")
  ;; 核心设置:强制使用 dvisvgm 生成 SVG
  (org-hugo-export-with-latex . dvisvgm)
  ;; 确保预览也是用 dvisvgm
  (org-preview-latex-default-process . dvisvgm)
  ;; 强制生成的图片存放目录名
  (org-preview-latex-image-directory . "../static/ltximg/")
  ;; 自动设置 lastmod
  (org-hugo-auto-set-lastmod . t)
  (org-hugo-suppress-lastmod-period . 86400.0)

  ;; --- Org Mode 基础设置 ---
  (org-log-done . 'time)

  )))

DONE 菜单逻辑 { math | 主题 标签 书籍 }

5 header.html

{{- $currentSection := .Section | default "" -}}
<ul id="menu">
  {{- if or (eq $currentSection "") (in (slice "1_idea" "2_tool" "3_book" "4_math" "5_code" "6_nature") $currentSection) -}}
  <li class="menu-group">
    {{- if eq $currentSection "" -}}
    <span class="menu-prefix">{ home |</span><a href="/categories/">主题</a><a href="/info/tags_zh/">标签</a><a href="/info/tags_en/">TAGS</a><a href="/3_book/">书籍</a>
    {{- else if eq $currentSection "1_idea" -}}
    <span class="menu-prefix">{ idea |</span><a href="/1_idea/categories/">主题</a><a href="/1_idea/tags/">标签</a><a href="/tags/Idea/">书籍</a>
    {{- else if eq $currentSection "2_tool" -}}
    <span class="menu-prefix">{ tool |</span><a href="/2_tool/categories/">主题</a><a href="/2_tool/tags/">标签</a><a href="/tags/Tool/">书籍</a>
    {{- else if eq $currentSection "3_book" -}}
    <span class="menu-prefix">{ book |</span><a href="/3_book/categories/">主题</a><a href="/3_book/tags/">标签</a>
    {{- else if eq $currentSection "4_math" -}}
    <span class="menu-prefix">{ math |</span><a href="/4_math/categories/">主题</a><a href="/4_math/tags/">标签</a><a href="/tags/Math/">书籍</a>
    {{- else if eq $currentSection "5_code" -}}
    <span class="menu-prefix">{ code |</span><a href="/5_code/categories/">主题</a><a href="/5_code/tags/">标签</a><a href="/tags/Code/">书籍</a>
    {{- else if eq $currentSection "6_nature" -}}
    <span class="menu-prefix">{ nature |</span><a href="/6_nature/categories/">主题</a><a href="/6_nature/tags/">标签</a><a href="/tags/Nature/">书籍</a>
    {{- end -}}
    <span class="menu-suffix">}</span>
  </li>
  {{- end -}}
 </ul>
</nav>
</header>

custom.css

/* ==========================================================================
   1. 菜单导航样式 (Header Menu)
   ========================================================================== */

/* 菜单组容器布局 */
.menu-group {
  display: flex;
  align-items: center;
  gap: 8px; /* 元素间隔 */
}

/* 前缀 { math | 和后缀 } 的样式 */
.menu-prefix,
.menu-suffix {
  color: var(--secondary);
  font-weight: 500;
  font-family: var(--font-mono); /* 强制等宽字体 */

  /* --- 修改点:加大字体 --- */
  /* 原来是 16px,现改为 18px 以匹配 H3/正文大小 */
  font-size: 18px;
}

/* 链接文字 (主题、标签、书籍) 的样式 */
.menu-group a {
  color: var(--primary);
  text-decoration: underline;
  text-underline-offset: 4px;
  padding: 2px 6px;
  transition: color 0.2s;

  /* --- 修改点:同步加大字体 --- */
  font-size: 18px;
}

/* 鼠标悬停效果 */
.menu-group a:hover {
  color: var(--tertiary);
  text-decoration: underline;
}

/* 移动端适配 (小于 768px) */
@media screen and (max-width: 768px) {
  .menu-group {
    flex-wrap: wrap; /* 允许换行 */
    gap: 4px;
  }

  .menu-prefix,
  .menu-suffix,
  .menu-group a {
    /* --- 修改点:手机端也适当加大 --- */
    /* 原来是 14px,改为 16px 保持清晰 */
    font-size: 16px;
  }
}

分类和标签系统的模板文件

模板优先级(从高到低)

模板名 控制的页面 Hugo 查找顺序
layouts/tags/terms.html /tags/(标签索引页) 1
layouts/_default/terms.html /tags/(回退) 2
layouts/tags/list.html /tags/some-tag/(单个标签的文章列表) 1(用于 term 页)
layouts/_default/list.html /tags/some-tag/(回退) 2(用于 term 页)
layouts/taxonomy.html 非标准,需手动引用

term.html:分类术语页面模板 section-categories.html:分类章节页面 section-tags.html:标签章节页面

最简单的标签页面

{{- define "main" }}
<header class="page-header">
    <h1>{{ .Title }}</h1>
    {{- if .Content }}
    <div class="post-content">
        {{ .Content }}
    </div>
    {{- end }}
</header>

<ul class="terms-tags">
    {{- $hiddenTags := slice "idea" "tool" "book" "math" "code" "nature" "Idea" "Tool" "Book" "Math" "Code" "Nature" }}

    {{- range .Data.Terms.ByCount }}
        {{- if not (in $hiddenTags .Term) }}
            {{- /* 根据文章数量设置权重等级(你可以调整区间和数量) */ -}}
            {{- $weightClass := "" }}
            {{- if le .Count 1 }}{{ $weightClass = "tag-weight-1" }}
            {{- else if le .Count 3 }}{{ $weightClass = "tag-weight-2" }}
            {{- else if le .Count 6 }}{{ $weightClass = "tag-weight-3" }}
            {{- else }}{{ $weightClass = "tag-weight-4" }}
            {{- end }}

            <li class="{{ $weightClass }}">
                <a href="{{ .Page.Permalink }}">
                    {{ .Page.Title }} <sup><strong>{{ .Count }}</strong></sup>
                </a>
            </li>
        {{- end }}
    {{- end }}
</ul>
{{- end }}

TODO 多彩标签页(待改进)

HOLD layouts/tags/terms.html

:tangle ../layouts/tags/terms.html
{{- define "main" }}
<header class="page-header">
    <h1>{{ .Title }}</h1>
    {{- if .Content }}
    <div class="post-content">
        {{ .Content }}
    </div>
    {{- end }}
</header>

<ul class="terms-tags">
    {{- $hiddenTags := slice "idea" "tool" "book" "math" "code" "nature" "Idea" "Tool" "Book" "Math" "Code" "Nature" }}

    {{- range .Data.Terms.ByCount }}
        {{- if not (in $hiddenTags .Term) }}
            {{- /* 根据文章数量设置权重等级(控制字体大小) */ -}}
            {{- $weightClass := "" }}
            {{- if le .Count 1 }}{{ $weightClass = "tag-weight-1" }}
            {{- else if le .Count 3 }}{{ $weightClass = "tag-weight-2" }}
            {{- else if le .Count 6 }}{{ $weightClass = "tag-weight-3" }}
            {{- else }}{{ $weightClass = "tag-weight-4" }}
            {{- end }}

            {{- /* 基于标签名生成色相值(避免使用 crc32) */ -}}
            {{- $first := int (index .Term 0) }}
            {{- $len := len .Term }}
            {{- $hash := add (mul $len 31) (mul $first 7) }}
            {{- $hue := mod $hash 360 }}

            {{- /* 根据出现次数决定明度(数值,0-100,越小颜色越深) */ -}}
            {{- $lightness := 80 }}
            {{- if ge .Count 7 }}{{ $lightness = 40 }}
            {{- else if ge .Count 4 }}{{ $lightness = 55 }}
            {{- else if ge .Count 2 }}{{ $lightness = 70 }}
            {{- else }}{{ $lightness = 85 }}
            {{- end }}
            {{- $saturation := 70 }}

            <li class="{{ $weightClass }}">
                <a href="{{ .Page.Permalink }}"
                   style="--tag-hue: {{ $hue }}; --tag-saturation: {{ $saturation }}; --tag-lightness: {{ $lightness }};">
                    {{ .Page.Title }}
                </a>
            </li>
        {{- end }}
    {{- end }}
</ul>
{{- end }}

HOLD css

:tangle ../assets/css/extended/custom.css
/* 标签云容器:居中排列,自动换行 */
.terms-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.8rem 1.2rem;
    justify-content: center;  /* 弹性盒居中 */
    align-items: center;
    list-style: none;
    padding: 1.5rem 0;
    margin: 0;
    text-align: center;       /* 后备居中方式 */
}

/* 每个标签项设置为内联块,使换行居中生效 */
.terms-tags li {
    display: inline-block;
    margin: 0;
    transition: transform 0.2s ease;
}

.terms-tags li:hover {
    transform: translateY(-3px); /* 悬停微微上浮 */
}

/* 标签链接:纯文字,无边框背景 */
.terms-tags a {
    display: inline-block;
    text-decoration: none;
    line-height: 1.4;
    white-space: nowrap;
    background: none !important;
    border: none !important;
    padding: 0;
    /* 颜色通过自定义属性动态计算 */
    color: hsl(var(--tag-hue), calc(var(--tag-saturation) * 1%), calc(var(--tag-lightness) * 1%));
}

/* 字体大小由权重类控制 */
.tag-weight-1 a { font-size: 0.9rem; }
.tag-weight-2 a { font-size: 1.1rem; }
.tag-weight-3 a { font-size: 1.3rem; }
.tag-weight-4 a { font-size: 1.6rem; }

/* 暗色模式调整:提高明度,降低饱和度,确保可读性 */
@media (prefers-color-scheme: dark) {
    .terms-tags a {
        --tag-lightness: calc(var(--tag-lightness) + 20);
        --tag-saturation: calc(var(--tag-saturation) - 10);
    }
    .terms-tags a:hover {
        opacity: 0.9;
    }
}

DONE categories.html

{{- define "main" }}
<header class="page-header">
    <h1>{{ .Title }}</h1>
    {{- /* 显示是哪个分区的主题 */ -}}
    {{- $currentSection := .CurrentSection -}}
    {{- if ne $currentSection.Section "" }}
    <div class="post-description">
        {{ replaceRE "^[0-9]+_" "" $currentSection.Section | humanize }} 分区的所有主题
    </div>
    {{- end }}
</header>

<ul class="terms-tags">
    {{- /* 获取当前分区 */ -}}
    {{- $currentSection := .CurrentSection -}}
    {{- $sectionPath := $currentSection.RelPermalink -}}

    {{- /* 获取该分区下的所有页面 */ -}}
    {{- $sectionPages := where site.RegularPages "FirstSection.RelPermalink" $sectionPath -}}

    {{- /* 收集所有分类 */ -}}
    {{- $allCategories := slice }}
    {{- range $sectionPages }}
        {{- with .Params.categories }}
            {{- $allCategories = $allCategories | append . }}
        {{- end }}
    {{- end }}

    {{- /* 去重、排序并显示 */ -}}
    {{- range ($allCategories | uniq | sort) }}
        {{- $category := . }}
        {{- if $category }}
            {{- $count := len (where $sectionPages ".Params.categories" "intersect" (slice $category)) }}
            {{- if gt $count 0 }}
            <li>
                <a href="{{ $sectionPath }}categories/{{ $category | urlize }}/">
                    {{ $category }} <sup><strong>{{ $count }}</strong></sup>
                </a>
            </li>
            {{- end }}
        {{- end }}
    {{- end }}
</ul>
{{- end }}

HOLD tags.html

:tangle ../layouts/_default/tags.html
{{- define "main" }}
<header class="page-header">
    <h1>{{ .Title }}</h1>
    {{- /* 显示是哪个分区的标签 */ -}}
    {{- $currentSection := .CurrentSection -}}
    {{- $sectionName := "" -}}
    {{- if ne $currentSection.Section "" }}
      {{- $sectionName = replaceRE "^[0-9]+_" "" $currentSection.Section | humanize -}}
    {{- end -}}

    {{- if $sectionName }}
    <div class="post-description">
        {{ $sectionName }} 分区的所有标签
    </div>
    {{- end }}
</header>

{{- /* ========================= 数据处理开始 ========================= */ -}}

{{- /* 1. 定义隐藏标签 */ -}}
{{- $hiddenTags := slice "idea" "tool" "book" "math" "code" "nature" "Idea" "Tool" "Book" "Math" "Code" "Nature" }}

{{- /* 2. 获取当前分区路径和页面 */ -}}
{{- $sectionPath := $currentSection.RelPermalink -}}
{{- $sectionPages := slice -}}

{{- /* 判断是否为首页/根目录,如果是,获取全站页面;如果是分区,获取分区下页面 */ -}}
{{- if or (eq $currentSection.Kind "home") (eq $currentSection.Kind "taxonomy") -}}
    {{- $sectionPages = site.RegularPages -}}
{{- else -}}
    {{- $sectionPages = where site.RegularPages "FirstSection.RelPermalink" $sectionPath -}}
{{- end -}}

{{- /* 3. 收集所有标签并去重 */ -}}
{{- $allTags := slice }}
{{- range $sectionPages }}
    {{- with .Params.tags }}
        {{- $allTags = $allTags | append . }}
    {{- end }}
{{- end }}
{{- $allTags = $allTags | uniq }}

{{- /* 4. 构建分组数据结构:Map[首字母] -> Slice[Tag对象] */ -}}
{{- $groups := newScratch -}}
{{- $letters := slice -}}

{{- range $allTags }}
    {{- $tagName := . -}}
    {{- if not (in $hiddenTags $tagName) }}
        {{- /* 计算数量 */ -}}
        {{- $count := len (where $sectionPages ".Params.tags" "intersect" (slice $tagName)) }}

        {{- if gt $count 0 }}
            {{- /* 获取首字母 (大写) */ -}}
            {{- $firstChar := upper (substr $tagName 0 1) -}}

            {{- /* 如果是新建的分组,把首字母加入列表以便后续排序 */ -}}
            {{- if not ($groups.Get $firstChar) }}
                {{- $letters = $letters | append $firstChar }}
            {{- end }}

            {{- /* 构建标签对象 */ -}}
            {{- $tagData := dict "Name" $tagName "Count" $count -}}

            {{- /* 添加到对应分组 */ -}}
            {{- $groups.Add $firstChar (slice $tagData) }}
        {{- end }}
    {{- end }}
{{- end }}

{{- /* ========================= 数据处理结束,开始渲染 ========================= */ -}}

<div class="terms-container">
    {{- /* 5. 第一层循环:按首字母排序 (A -> Z -> 中文) */ -}}
    {{- range sort $letters }}
        {{- $letter := . -}}
        {{- $tagList := $groups.Get $letter -}}

        <div class="terms-group">
            {{- /* 显示分组标题 (首字母) */ -}}
            <h3 class="terms-letter">{{ $letter }}</h3>

            <ul class="terms-tags">
                {{- /* 6. 第二层循环:组内按数量倒序 (9 -> 1) */ -}}
                {{- range sort $tagList "Count" "desc" }}
                <li>
                    {{- /* 链接修复:指向全局 tags 路径 */ -}}
                    <a href="{{ "tags/" | relLangURL }}{{ .Name | urlize }}/">
                        {{ .Name }} <sup><strong>{{ .Count }}</strong></sup>
                    </a>
                </li>
                {{- end }}
            </ul>
        </div>
    {{- end }}
</div>

{{- end }}

DONE section-categotries

{{- define "main" }}

{{- /* ... (保留头部逻辑不变) ... */ -}}
{{- $currentSection := .CurrentSection -}}
{{- $parentSection := $currentSection.Parent -}}
{{- if eq $parentSection.Kind "home" }}
    {{- $parentSection = $currentSection -}}
{{- end -}}
{{- $sectionPath := $parentSection.RelPermalink -}}
{{- $sectionName := "" -}}
{{- if ne $parentSection.Section "" }}
  {{- $sectionName = replaceRE "^[0-9]+_" "" $parentSection.Section | humanize -}}
{{- end -}}

<header class="page-header">
    <h1>{{ .Title }}</h1>
    {{- if $sectionName }}
    <div class="post-description">
        {{ $sectionName }} 分区的所有主题
    </div>
    {{- end }}
</header>

<ul class="terms-tags">
    {{- /* 收集文章(支持子目录) */ -}}
    {{- $sectionPages := slice -}}
    {{- range site.RegularPages }}
        {{- if hasPrefix .RelPermalink $sectionPath }}
            {{- if and (ne .Layout "section-tags") (ne .Layout "section-categories") }}
                {{- $sectionPages = $sectionPages | append . -}}
            {{- end }}
        {{- end }}
    {{- end }}

    {{- /* 收集所有分类 */ -}}
    {{- $allCategories := slice }}
    {{- range $sectionPages }}
        {{- with .Params.categories }}
            {{- $allCategories = $allCategories | append . }}
        {{- end }}
    {{- end }}

    {{- /* 去重、排序并显示 */ -}}
    {{- range ($allCategories | uniq | sort) }}
        {{- $category := . }}
        {{- if $category }}
            {{- $count := len (where $sectionPages ".Params.categories" "intersect" (slice $category)) }}
            {{- if gt $count 0 }}
            <li>
                {{- /* 修改如下:移除 $sectionPath,使用 relLangURL 指向站点根目录的 categories */ -}}
                <a href="{{ "categories/" | relLangURL }}{{ $category | urlize }}/">
                    {{ $category }} <sup><strong>{{ $count }}</strong></sup>
                </a>
            </li>
            {{- end }}
        {{- end }}
    {{- end }}
</ul>
{{- end }}

DONE section-tags

{{- define "main" }}

{{- /* ... (保留头部逻辑不变) ... */ -}}
{{- $currentSection := .CurrentSection -}}
{{- $parentSection := $currentSection.Parent -}}
{{- if eq $parentSection.Kind "home" }}
    {{- $parentSection = $currentSection -}}
{{- end -}}
{{- $sectionPath := $parentSection.RelPermalink -}}
{{- $sectionName := "" -}}
{{- if ne $parentSection.Section "" }}
  {{- $sectionName = replaceRE "^[0-9]+_" "" $parentSection.Section | humanize -}}
{{- end -}}

<header class="page-header">
    <h1>{{ .Title }}</h1>
    {{- if $sectionName }}
    <div class="post-description">
        {{ $sectionName }} 分区的所有标签
    </div>
    {{- end }}
</header>

<ul class="terms-tags">
    {{- /* 定义要隐藏的标签 */ -}}
    {{- $hiddenTags := slice "idea" "tool" "book" "math" "code" "nature" "Idea" "Tool" "Book" "Math" "Code" "Nature" }}

    {{- /* 收集文章(支持子目录) */ -}}
    {{- $sectionPages := slice -}}
    {{- range site.RegularPages }}
        {{- if hasPrefix .RelPermalink $sectionPath }}
            {{- if and (ne .Layout "section-tags") (ne .Layout "section-categories") }}
                {{- $sectionPages = $sectionPages | append . -}}
            {{- end }}
        {{- end }}
    {{- end }}

    {{- /* 收集所有标签 */ -}}
    {{- $allTags := slice }}
    {{- range $sectionPages }}
        {{- with .Params.tags }}
            {{- $allTags = $allTags | append . }}
        {{- end }}
    {{- end }}

    {{- /* 去重、排序并显示(过滤隐藏标签) */ -}}
    {{- range ($allTags | uniq | sort) }}
        {{- $tag := . }}
        {{- if and $tag (not (in $hiddenTags $tag)) }}
            {{- $count := len (where $sectionPages ".Params.tags" "intersect" (slice $tag)) }}
            {{- if gt $count 0 }}
            <li>
                {{- /* 修改如下:移除 $sectionPath,使用 relLangURL 指向站点根目录的 tags */ -}}
                <a href="{{ "tags/" | relLangURL }}{{ $tag | urlize }}/">
                    {{ $tag }} <sup><strong>{{ $count }}</strong></sup>
                </a>
            </li>
            {{- end }}
        {{- end }}
    {{- end }}
</ul>
{{- end }}

layouts/taxonomy/categories.html

{{- define "main" }}
<header class="page-header">
  <h1>所有分类</h1>
  <div class="taxonomy-description">
    <p>按主题浏览所有文章</p>
  </div>
</header>

{{- /* 获取所有唯一分类 */ -}}
{{- $allCategories := slice }}
{{- range site.RegularPages }}
  {{- with .Params.categories }}
    {{- $allCategories = $allCategories | append . }}
  {{- end }}
{{- end }}
{{- $uniqueCategories := $allCategories | uniq | sort }}

<div class="taxonomy-grid">
  {{- range $uniqueCategories }}
    {{- $category := . }}
    {{- $pages := where site.RegularPages ".Params.categories" "intersect" (slice $category) }}
    {{- $count := len $pages }}
    {{- if gt $count 0 }}
    <div class="taxonomy-card">
      <h2>
        <a href="/categories/{{ $category | urlize }}/"
           title="查看 {{ $count }} 篇关于 '{{ $category }}' 的文章">
          {{ $category }}
        </a>
      </h2>
      <div class="taxonomy-count">{{ $count }} 篇文章</div>

      {{- /* 显示该分类下的最新3篇文章 */ -}}
      <div class="taxonomy-recent">
        <ul>
          {{- range first 3 ($pages.ByDate.Reverse) }}
          <li>
            <a href="{{ .Permalink }}" title="{{ .Title }}">
              {{ .Title }}
            </a>
            <span class="taxonomy-date">{{ .Date.Format "2006-01-02" }}</span>
          </li>
          {{- end }}
        </ul>
      </div>
    </div>
    {{- end }}
  {{- end }}
</div>
{{- end }}

标签和主题的上标的颜色

/* 设置数量上标的颜色 */
.terms-tags li a sup {
    color: var(--custom-emphasis) !important;
    font-size: 0.8em;
    font-weight: normal;
}

section/index.json

{{/* layouts/section/index.json */}}

{{- $.Scratch.Add "index" slice -}}

{{- /* 只索引当前section的内容 */ -}}
{{- $pages := where .Site.RegularPages "Section" .Section -}}
{{- $pages = where $pages "Params.hiddenInList" "!=" true -}}
{{- /* 删除了错误的筛选行 */ -}}

{{- range $pages -}}
    {{- $.Scratch.Add "index" (dict "title" .Title "permalink" .Permalink "summary" .Summary ) -}}
{{- end -}}

{{- $.Scratch.Get "index" | jsonify -}}

自动化集成

Makefile

项目自动化构建和管理的核心文件,提供以下功能:

  • 环境管理:切换测试分支(test)和生产分支(work)
  • 工作流程:本地预览(preview)、合并分支(merge)、部署网站(deploy)
  • 维护命令:清理文件(clean)、检查依赖(check)、版本控制(push/pull)
  • 图片优化:SVG图片压缩优化
  • 部署配置:通过rsync将网站部署到远程服务器
  • 安全检查:验证工作区干净、检查命令可用性等

环境管理

# 引入版本控制
include git-version.mk
# ====================================================
#                  项目配置
# =====================================================

# Git 分支配置
PROD_BRANCH := main
DEV_BRANCH  := test

# 实时获取当前分支
CURRENT_BRANCH := $(shell git branch --show-current 2>/dev/null || echo "未知")

# RSync 部署配置
REMOTE_USER   := tea
REMOTE_PATH   := /var/www/picatria.org.cn
RSYNC_FLAGS   := -avz --delete \
	--exclude='.env' \
	--exclude='*.swp' \
	--exclude='*.swo' \
	--exclude='.DS_Store'

# URL 配置
PROD_BASE_URL  := https://picatria.org.cn
LOCAL_BASE_URL := http://localhost:1313
HUGO_PORT      := 1313

# =============================================================================
#  图片优化配置
# =============================================================================

# 1. SVG 配置 (数学公式)
SVG_DIR    := static/ltximg
SVG_MARKER := .svg_last_optimized

# =============================================================================
#                  运行时命令检查
# =============================================================================

REQUIRED_COMMANDS := hugo git rsync

check_commands:
	@for cmd in $(REQUIRED_COMMANDS); do \
		if ! command -v $$cmd >/dev/null 2>&1; then \
			echo "错误: $$cmd 命令未找到,请安装 $$cmd"; \
			exit 1; \
		fi; \
	done
	@echo "✓ 所有必需命令都可用"

# =============================================================================
#  图片优化配置
# =============================================================================

# 1. SVG 配置 (数学公式)
SVG_DIR    := static/ltximg
SVG_MARKER := .svg_last_optimized

# =============================================================================
#  优化任务
# =============================================================================


# --- 任务 A: 压缩 SVG ---
optimize-svg:
	@echo "→ [1/2] 检查 SVG 公式优化 (svgcleaner)..."
	@if [ ! -f $(SVG_MARKER) ]; then \
		echo "  首次运行,压缩所有 SVG..."; \
		find $(SVG_DIR) -name "*.svg" -print0 | xargs -0 -r -P 4 -I {} svgcleaner {} {} --quiet; \
	else \
		find $(SVG_DIR) -name "*.svg" -newer $(SVG_MARKER) -print0 | xargs -0 -r -P 4 -I {} svgcleaner {} {} --quiet; \
	fi
	@touch $(SVG_MARKER)


# =============================================================================
#                  安全检查
# =============================================================================

# 检查是否有未提交的更改
check_clean:
	@git update-index --really-refresh -q
	@if ! git diff-index --quiet HEAD --; then \
		echo "错误:存在未提交的更改。"; \
		git status --short; \
		echo "请先提交或 stash 您的更改后再继续。"; \
		exit 1; \
	fi
	@echo "✓ 工作区干净"

# 检查分支是否存在
define check_branch
	@if ! git show-ref --quiet refs/heads/$(1); then \
		echo "错误: 分支 '$(1)' 不存在!"; \
		echo "请先创建分支: git switch -c $(1)"; \
		exit 1; \
	fi
endef

# =============================================================================
#                  主要命令
# =============================================================================

.PHONY: help test work merge preview deploy clean status check push pull

push:
	$(MAKE) -C ~/sync_file push

pull:
	$(MAKE) -C ~/sync_file pull

default: help

help:
	@echo "项目管理 Makefile"
	@echo ""
	@echo "用法: make <target>"
	@echo ""
	@echo "环境管理:"
	@echo "  test		切换到测试分支 ('$(DEV_BRANCH)')"
	@echo "  work		切换到生产分支 ('$(PROD_BRANCH)')"
	@echo "  status 	显示当前 git 状态"
	@echo ""
	@echo "工作流程:"
	@echo "  preview	启动本地预览 (端口: $(HUGO_PORT))"
	@echo "  merge		将测试分支合并到生产分支 (保护 org_content)"
	@echo "  deploy 	从生产分支构建网站并上传到服务器"
	@echo ""
	@echo "维护命令:"
	@echo "  clean		清理生成的文件"
	@echo "  check		检查所有必需的命令是否可用"
	@echo "  push/pull	上传和拉取图片"
	@echo "  vhelp		版本号的帮助"
	@echo ""
	@echo "当前分支: $(CURRENT_BRANCH)"

# 显示当前状态
status:
	@echo "当前分支: $(CURRENT_BRANCH)"
	@echo "未跟踪文件:"
	@git status --short
	@echo ""
	@echo "最近提交:"
	@git log --oneline -5 2>/dev/null || echo "  暂无提交历史"

# 切换到测试环境
test:
	@echo "→ 切换到生产分支 '$(PROD_BRANCH)'..."
	@git switch $(PROD_BRANCH) >/dev/null \
		|| { echo "错误:无法切换到 $(PROD_BRANCH)"; exit 1; }
	@echo "→ 检查 test 分支是否存在..."
	@if git show-ref --quiet refs/heads/$(DEV_BRANCH); then \
		echo "  → test 分支存在,强制删除..."; \
		git branch -D $(DEV_BRANCH) >/dev/null \
			|| { echo "错误:无法删除 test 分支"; exit 1; }; \
	else \
		echo "  → test 分支不存在"; \
	fi
	@echo "→ 创建新的 test 分支(基于 $(PROD_BRANCH))..."
	@git switch -c $(DEV_BRANCH) >/dev/null \
		|| { echo "错误:无法创建 test 分支"; exit 1; }
	@echo "✔ test 分支已覆盖为 $(PROD_BRANCH) 最新状态"
# 切换到工作环境
work:
	$(call check_branch,$(PROD_BRANCH))
	@echo "→ 切换到生产分支: $(PROD_BRANCH)..."
	@git switch -q $(PROD_BRANCH)
	@echo "✓ 现在在 $(PROD_BRANCH) 分支"

# 合并测试分支到生产分支
merge: work check_clean
	$(call check_branch,$(DEV_BRANCH))
	@git merge --no-ff $(DEV_BRANCH)
	@echo "✓ 合并完成,并创建了新的合并提交。"

# 本地预览
preview:
	@echo "→ 启动本地预览..."
	@echo "  访问地址: $(LOCAL_BASE_URL)"
	@echo "  端口: $(HUGO_PORT)"
	@hugo server \
		--port $(HUGO_PORT) \
		--disableFastRender \
		--baseURL $(LOCAL_BASE_URL) \
		--bind 0.0.0.0

# 部署网站
deploy: work optimize-svg
	@echo "→ 准备部署到生产服务器..."
	@echo "  目标: $(REMOTE_USER):$(REMOTE_PATH)"
	@echo "  当前分支: $$(git branch --show-current)"
	@echo -n "确认部署到生产环境?(y/N): "; \
	read confirm; \
	if [ "$$confirm" != "y" ] && [ "$$confirm" != "Y" ]; then \
		echo "部署取消"; \
		exit 0; \
	fi
	@echo "→ 为生产环境构建网站..."
	@hugo --minify --baseURL $(PROD_BASE_URL)
	@if [ ! -d "public" ] || [ ! -f "public/index.html" ]; then \
		echo "构建失败!public 目录不存在或为空"; \
		exit 1; \
	fi
	@echo "→ 上传文件到服务器..."
	@rsync $(RSYNC_FLAGS) public/ $(REMOTE_USER):$(REMOTE_PATH)
	@echo " 部署成功!"
	@echo "   网站地址: $(PROD_BASE_URL)"

# 清理生成的文件
clean:
	@echo "→ 清理生成的文件..."
	@rm -rf public resources .hugo_build.lock 2>/dev/null || true
	@echo "✓ 清理完成"

gen_static_logo.sh

网站图标生成脚本,功能包括:从SVG源文件生成多种尺寸的网站图标生成favicon(16x16, 32x32)、apple-touch-icon等对图标进行自动裁剪(trim)以最大化显示效果支持暗色模式的反色处理

#!/bin/bash

# ====================================================
# Picatria 网站图标生成器 V9 (视觉最大化版)
# 特性: 自动切除透明白边(-trim),让图标撑满格子
# ====================================================

# --- 配置 ---
SOURCE_SVG="static/logo.svg"
SMALL_SVG="static/logo_small.svg"
OUT_DIR="static"
SITE_URL="https://picatria.org.cn"

# ----------------------------------------------------

if [ ! -f "$SOURCE_SVG" ]; then SOURCE_SVG="assets/logo.svg"; fi
if [ ! -f "$SMALL_SVG" ]; then SMALL_SVG="assets/logo_small.svg"; fi

mkdir -p "$OUT_DIR"

# A. 处理高清源 (Master) - 用于 Safari 和 Apple Icon
if [ -f "$SOURCE_SVG" ]; then
    echo "-> [1/3] Processing High-Res Source..."
    MASTER="/tmp/master_logo.png"
    # 强制背景透明
    inkscape "$SOURCE_SVG" --export-type=png --export-filename="$MASTER" --export-width=1024 --export-height=1024 --export-background-opacity=0 2>/dev/null

    # Apple Icon 不需要 trim,保留一点呼吸感更好,或者你可以选择也 trim
    convert "$MASTER" -resize 180x180 -background white -flatten -set comment "$SITE_URL" "$OUT_DIR/apple-touch-icon.png"
    cat "$SOURCE_SVG" | sed "s|<svg|<svg data-site=\"$SITE_URL\"|" > "$OUT_DIR/safari-pinned-tab.svg"
else
    echo "Error: Main logo.svg not found!"
    exit 1
fi

# B. 处理小图标源 (Small)
echo "-> [2/3] Processing Small Icon Source..."
RAW_SMALL="/tmp/raw_small.png"

if [ -f "$SMALL_SVG" ]; then
    # 导出足够大的图以便裁剪 (256px)
    inkscape "$SMALL_SVG" --export-type=png --export-filename="$RAW_SMALL" --export-width=256 --export-height=256 --export-background-opacity=0 2>/dev/null
else
    # 回退方案
    cp "$MASTER" "$RAW_SMALL"
fi

# ====================================================
# 🔥 核心修改:创建 TRIMMED_MASTER (撑满版)
# ====================================================
# -trim: 自动切除周围所有透明像素,只保留核心图形
# +repage: 重置画布坐标 (必须步骤)
TRIMMED_MASTER="/tmp/trimmed_master.png"

echo "   Trimming whitespace to maximize icon size..."
convert "$RAW_SMALL" -trim +repage "$TRIMMED_MASTER"

# C. 生成小尺寸图标 (使用 TRIMMED_MASTER)
# 因为已经去掉了白边,现在 resize 16x16 会强制让图形的最长边顶满 16px
echo "-> [3/3] Generating Maximized Favicons..."

# 生成 PNG
convert "$TRIMMED_MASTER" -resize 16x16 "$OUT_DIR/favicon-16x16.png"
convert "$TRIMMED_MASTER" -resize 32x32 "$OUT_DIR/favicon-32x32.png"

# 生成 ICO
# 这里的逻辑是:先 trim 掉白边,再缩放。
# 这样你的图标在 16x16 的格子里就是"顶天立地"的,看起来最大。
convert \
    -background none \
    \( "$TRIMMED_MASTER" -resize 16x16 \) \
    \( "$TRIMMED_MASTER" -resize 32x32 \) \
    \( "$TRIMMED_MASTER" -resize 48x48 \) \
    "$OUT_DIR/favicon.ico"

# 清理
rm -f "$MASTER" "$RAW_SMALL" "$TRIMMED_MASTER"

echo "✅ All Done! Icons are now maximized (Trimmed)."

git-version.mk

Git版本管理辅助文件,提供语义化版本控制功能:版本查询:显示当前最新版本版本升级:支持patch、minor、major级别的版本递增标签推送:将版本标签推送到远程仓库工作区检查:确保在创建版本标签前工作区是干净的

# =============================================================================
#  通用 Git 版本管理 Makefile
#  这个文件与任何编程语言或构建系统都无关。
#  它只用于简化 Git 语义化版本标签的创建和管理。
# =============================================================================

# .DEFAULT_GOAL 设置了当只输入 `make` 时执行的默认命令
.DEFAULT_GOAL := vhelp

# 如果没有任何标签,则默认为 v0.0.0,以便计算可以从 0 开始。
LAST_TAG := $(shell git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")

# 解析主、次、修订号 (去除'v'前缀,然后用空格替换点)
# 例如 "v1.2.3" -> "1 2 3"
VERSION_PARTS := $(shell echo $(LAST_TAG) | sed 's/^v//' | tr . ' ')
MAJOR := $(word 1, $(VERSION_PARTS))
MINOR := $(word 2, $(VERSION_PARTS))
PATCH := $(word 3, $(VERSION_PARTS))

# 定义新的版本号
# 使用 shell 的 expr 命令进行数学计算
NEW_PATCH_VERSION := v$(MAJOR).$(MINOR).$(shell expr $(PATCH) + 1)
NEW_MINOR_VERSION := v$(MAJOR).$(shell expr $(MINOR) + 1).0
NEW_MAJOR_VERSION := v$(shell expr $(MAJOR) + 1).0.0

# 这是一个内部使用的“前置任务”,用于确保在打标签前工作目录是干净的
# 避免将未完成或未提交的修改打入版本标签
guard-clean-tree:
	@if ! git diff-index --quiet HEAD --; then \
		echo "错误:您的工作目录有未提交的更改。"; \
		echo "请先使用 'git commit' 或 'git stash' 清理后再创建版本标签。"; \
		exit 1; \
	fi

# =============================================================================
#  可执行命令 (用户接口)
# =============================================================================

vhelp: ## 显示此帮助信息
	@echo "Git 版本管理助手"
	@echo ""
	@awk -F ':.*?## ' '/^[a-zA-Z_-]+:.*?## / { printf "  \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) | sort
	@echo ""

# 【唯一的修改点在这里】
version: ## [实时] 显示当前最新的版本号
	@echo "当前最新版本: $(shell git describe --tags --abbrev=0 2>/dev/null || echo '未找到任何版本标签')"

patch: guard-clean-tree ## 用于 bug 修复
	@git tag $(NEW_PATCH_VERSION)
	@echo "成功创建本地标签: $(NEW_PATCH_VERSION)"

minor: guard-clean-tree ## 用于新增功能
	@git tag $(NEW_MINOR_VERSION)
	@echo "成功创建本地标签: $(NEW_MINOR_VERSION)"

major: guard-clean-tree ## 用于重大重构或不兼容的修改
	@git tag $(NEW_MAJOR_VERSION)
	@echo "成功创建本地标签: $(NEW_MAJOR_VERSION)"

v: ## Git 标签推送到远程仓库
	@git push --tags

# .PHONY 告诉 make,这些目标不是文件名,每次执行时都应该运行对应的命令
# 我将 vhelp 添加到了 .PHONY 列表中,这是一个好习惯。
.PHONY: vhelp version patch minor major v guard-clean-tree

shell

tags 和 categotries 的 _index.md

cd ~/picatria/content/

# 花括号里永远不要写空格,这是 bash 的“硬规则”
mkdir -p ./{\
1_idea,\
2_tool,\
3_book,\
4_math,\
5_code,\
6_nature\
}/{tags,categories}


for d in 1_idea 2_tool 3_book 4_math 5_code 6_nature; do
  cat > "$d/tags/_index.md" <<'EOF'
---
title: "标签"
layout: section-tags
build:
  list: never
  render: always
---
EOF
done

for d in 1_idea 2_tool 3_book 4_math 5_code 6_nature; do
  cat > "$d/categories/_index.md" <<'EOF'
---
title: "主题"
layout: section-categories
build:
  list: never
  render: always
---
EOF
done