[{"content":"随便写写 ~ 项目在下面：\nCloudPaste - 在线剪贴板 📋 基于 Cloudflare 的在线剪贴板和文件分享服务，支持 Markdown 编辑和文件上传 JavaScript ✨ 特点 📝 Markdown 编辑与分享 强大的编辑器：集成 Vditor，支持 GitHub 风格的 Markdown、数学公式、流程图、思维导图等 安全分享：内容可设置访问密码保护 灵活时效：支持设置内容过期时间 访问控制：可限制最大查看次数 个性化：自定义分享链接及备注 支持文本 Raw 直链：类似 gihub 的 Raw 直链，用于 yaml 配置文件来启动的服务 多格式导出：支持 PDF、Markdown、HTML、PNG 图片、Word 文档 导出 便捷分享：一键复制分享链接和生成二维码 自动保存：支持自动保存草稿功能 📤 文件上传与管理 多存储支持：兼容多种 S3 存储服务 (Cloudflare R2、Backblaze B2、AWS S3 等) 存储配置：可视化界面配置多个存储空间，灵活切换默认存储源 高效上传：通过预签名 URL 直接上传至 S3 存储，多文件上传 实时反馈：上传进度实时显示 自定义限制：单次上传限制和最大容量限制 元数据管理：文件备注、密码、过期时间、访问限制 数据分析：文件访问统计与趋势分析 服务器直传：支持调接口进行文件上传、下载等操作 🛠 便捷的文件/文本操作 统一管理：支持文件/文本创建、删除和属性修改 在线预览：常见文档、图片和媒体文件的在线预览与直链生成 分享工具：生成短链接和二维码，便于跨平台分享 批量管理：文件/文本批量操作与显示 🔄 WebDAV 和挂载点管理 WebDAV 协议支持：通过标准 WebDAV 协议访问和管理文件系统 网络驱动器挂载：支持 部分第三方客户端直接挂载 灵活的挂载点：支持创建多个挂载点，连接不同的存储服务 权限控制：精细的挂载点访问权限管理 API 密钥集成：通过 API 密钥授权 WebDAV 访问 大文件支持：自动使用分片上传机制处理大文件 目录操作：完整支持目录创建、上传、删除、重命名等操作 🔐 轻量权限管理 管理员权限控制 系统管理：全局系统设置配置 内容审核：所有用户内容的管理 存储管理：S3 存储服务的添加、编辑与删除 权限分配：API 密钥的创建与权限管理 数据分析：完整的统计数据访问 API 密钥权限控制 文本权限：创建/编辑/删除文本内容 文件权限：上传/管理/删除文件 存储权限：可选择特定的存储配置 读写分离：可设置只读或读写权限 时效控制：自定义有效期（从小时到月） 安全机制：自动失效与手动撤销功能 💫 系统功能 适配性强：响应式设计，适配移动设备和桌面 多语言：中/英文双语界面支持 视觉模式：明亮/暗黑主题切换 安全认证：基于 JWT 的管理员认证系统 离线体验：PWA 支持，可离线使用和安装到桌面 🚀 部署教程 前期准备 在开始部署前，请确保您已准备以下内容：\nCloudflare 账号（必需） 如使用 R2：开通 Cloudflare R2 服务并创建存储桶（需绑定支付方式） 其他 S3 存储服务的配置信息： S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY S3_BUCKET_NAME S3_ENDPOINT 开始部署 获取 Cloudflare API 令牌 访问 Cloudflare Dashboard 创建新的 API 令牌 选择\u0026quot;编辑 Cloudflare Workers\u0026quot;模板，并添加 D1 数据库编辑权限 获取cloudflare account ID： 配置 GitHub 仓库 Fork 或克隆仓库 https://github.com/ling-drag0n/CloudPaste 进入您的 GitHub 仓库设置 转到 Settings → Secrets and variables → Actions → New Repository secrets 添加以下 Secrets： Secret 名称 必需 用途 CLOUDFLARE_API_TOKEN ✅ Cloudflare API 令牌（需要 Workers、D1 和 Pages 权限） CLOUDFLARE_ACCOUNT_ID ✅ Cloudflare 账户 ID ENCRYPTION_SECRET ❌ 用于加密敏感数据的密钥（如不提供，将自动生成） 后端部署： 然后按照同样的步骤，部署前端： 后端部署完成后，在cloudflare pages里查看后端地址： 复制地址，回到前端 Cloudflare Pages 控制面板设置环境变量： 名称：VITE_BACKEND_URL 值：刚刚部署的后端 Worker URL（如 https://cloudpaste-backend.your-username.workers.dev），末尾不带\u0026quot;/\u0026quot;, 同时建议使用自定义的 worker 后端域名。 一定要完整的填写后端域名, https://xxxx.com 格式 重要步骤： 随后要再次运行一遍前端的工作流，以便完成后端域名加载！！！ ⚠️ 安全提示：请在系统初始化后立即修改默认管理员密码（用户名: admin, 密码: admin123）。\nR2 API 相关获取及跨域配置 登录 Cloudflare Dashboard 点击 R2 存储，创建一个存储桶 创建 API 令牌 创建后把全部数据都保存好，后续要填写密钥ID和密钥 配置跨域规则：点击对应存储桶，点击设置，编辑 CORS 策略，如下所示： 1[ 2 { 3 \u0026#34;AllowedOrigins\u0026#34;: [\u0026#34;http://localhost:3000\u0026#34;, \u0026#34;https://根据自己的前端域名来替代\u0026#34;], 4 \u0026#34;AllowedMethods\u0026#34;: [\u0026#34;GET\u0026#34;, \u0026#34;PUT\u0026#34;, \u0026#34;POST\u0026#34;, \u0026#34;DELETE\u0026#34;, \u0026#34;HEAD\u0026#34;], 5 \u0026#34;AllowedHeaders\u0026#34;: [\u0026#34;*\u0026#34;], 6 \u0026#34;ExposeHeaders\u0026#34;: [\u0026#34;ETag\u0026#34;], 7 \u0026#34;MaxAgeSeconds\u0026#34;: 3600 8 } 9] 注意！！！ 编辑 CORS 策略那一块的前端域名最后不能有 /，否则跨域配置会失败！我配置报错查了好久都没查到什么原因，最后莫名把反斜杠去掉就好了。 最后在管理页配置一下s3存储和挂载，就可以当文件中转站使用啦！\n","date":"2025-06-22T15:17:31+08:00","image":"https://langminjeii.ifantic.de/post/cloudpaste.png","permalink":"https://langminjeii.ifantic.de/post/cloudpaste%E9%83%A8%E7%BD%B2%E5%9C%A8%E7%BA%BF%E5%89%AA%E8%B4%B4%E6%9D%BF%E5%9B%BE%E5%BA%8A%E6%96%87%E4%BB%B6%E4%B8%AD%E8%BD%AC%E7%AB%99/","title":"Cloudpaste部署——在线剪贴板、图床、文件中转站"},{"content":"按照惯例，如果没有特别说明，css样式都是在 assets/scss/custom.scss 文件里修改，短代码的html文件放在 layouts/shortcodes 文件夹里。\n谷歌robots hugo博客seo优化\n【建站技术】在你的 Hugo 博客中使用 Google Analytics 来统计和分析流量数据\n链接样式 偶然看到了一个自己很喜欢的链接样式: https://www.shirakii.com/post/latex-chinese-record/\n但是作者没有公开它的配置，于是自己手搓了一版：\n1a.link { 2 color: dodgerblue; 3 font-weight: 600; 4 padding: 0 2px; 5 box-shadow: none; 6 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 7 position: relative; 8 display: inline; 9 box-decoration-break: clone; 10 -webkit-box-decoration-break: clone; 11 12 background-image: linear-gradient(to right, dodgerblue, dodgerblue); 13 background-size: 0 2px; 14 background-position: left 100%; 15 background-repeat: no-repeat; 16 transition: background-size 0.3s ease, background-position 0s ease 0.3s; 17 padding-bottom: 2px; /* 增加底部内边距 */ 18} 19 20a.link:hover { 21 background-size: 100% 2px; 22 box-shadow: none; 23 text-decoration: none; 24 background-position: left 100%; 25 transition: background-size 0.3s ease; 26} 27 28a.link:not(:hover) { 29 background-position: right 100%; 30 background-size: 0 2px; 31 transition: background-size 0.3s ease; 32} 33 34[data-scheme=\u0026#34;light\u0026#34;] a.link { 35 color: deepskyblue; 36 background-image: linear-gradient(to right, deepskyblue, deepskyblue); 37} 不过发现用这种跳转链接不会出现上面的scss效果：\u0026lt;a href=\u0026quot;/shortcodes\u0026quot; target=\u0026quot;_blank\u0026quot;\u0026gt;点我看看\u0026lt;/a\u0026gt;\n点我看看。修改代码：\n1a[linkscss=\u0026#34;true\u0026#34;], 2a.link { 3 color: dodgerblue; 4 font-weight: 600; 5 padding: 0 2px; 6 box-shadow: none; 7 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 8 position: relative; 9 display: inline; 10 box-decoration-break: clone; 11 -webkit-box-decoration-break: clone; 12 13 background-image: linear-gradient(to right, dodgerblue, dodgerblue); 14 background-size: 0 2px; 15 background-position: left 100%; 16 background-repeat: no-repeat; 17 transition: background-size 0.3s ease, background-position 0s ease 0.3s; 18 padding-bottom: 4px; /* 增加底部内边距 */ 19} 20 21a[linkscss=\u0026#34;true\u0026#34;]:hover, 22a.link:hover { 23 background-size: 100% 2px; 24 box-shadow: none; 25 text-decoration: none; 26 background-position: left 100%; 27 transition: background-size 0.3s ease; 28} 29 30a[linkscss=\u0026#34;true\u0026#34;]:not(:hover), 31a.link:not(:hover) { 32 background-position: right 100%; 33 background-size: 0 2px; 34 transition: background-size 0.3s ease; 35} 36 37[data-scheme=\u0026#34;light\u0026#34;] a[linkscss=\u0026#34;true\u0026#34;], 38[data-scheme=\u0026#34;light\u0026#34;] a.link { 39 color: deepskyblue; 40 background-image: linear-gradient(to right, deepskyblue, deepskyblue); 41} 于是 \u0026lt;a href=\u0026quot;/shortcodes\u0026quot; target=\u0026quot;_blank\u0026quot; linkscss=\u0026quot;true\u0026quot;\u0026gt;点这里\u0026lt;/a\u0026gt; 就能出现scss效果了：点这里\n文章添加点赞按钮 参考■■Loading：《hugo装修日志10》■■，说的很详细了。\n系列文章 参考在 Hugo 里显示系列文章，新建 layouts/partials/series.html：\n1{{ range $index, $series := .Params.series }} 2\u0026lt;div class=\u0026#34;article-list article-series\u0026#34;\u0026gt; 3 \u0026lt;header\u0026gt; 4 \u0026lt;div class=\u0026#34;series-section-title\u0026#34;\u0026gt; 5 {{ i18n \u0026#34;partOf\u0026#34; }} 6 \u0026lt;a href=\u0026#34;{{ \u0026#34;/series/\u0026#34; | relLangURL }}{{ . | urlize }}\u0026#34; linkscss=\u0026#34;true\u0026#34;\u0026gt;{{ $series }}\u0026lt;/a\u0026gt; 7 {{ i18n \u0026#34;series\u0026#34; }}: 8 \u0026lt;/div\u0026gt; 9 \u0026lt;/header\u0026gt; 10 11 \u0026lt;div class=\u0026#34;article-series-container\u0026#34;\u0026gt; 12 \u0026lt;ol\u0026gt; 13 {{ range $ind, $post := $.Site.Pages.ByDate }} 14 {{ if in $post.Params.series $series }} 15 \u0026lt;li class=\u0026#34;article-series-item\u0026#34;\u0026gt; 16 {{ if eq $post.Permalink $.Page.Permalink }} 17 \u0026lt;span class=\u0026#34;article-series-active\u0026#34;\u0026gt;{{ $post.Params.title }} ({{ i18n \u0026#34;currentArticle\u0026#34; }})\u0026lt;/span\u0026gt; 18 {{ else }} 19 \u0026lt;a href=\u0026#34;{{ $post.Permalink }}\u0026#34; linkscss=\u0026#34;true\u0026#34;\u0026gt;{{ $post.Params.title }}\u0026lt;/a\u0026gt; 20 {{ end }} 21 \u0026lt;/li\u0026gt; 22 {{ end }} 23 {{ end }} 24 \u0026lt;/ol\u0026gt; 25 \u0026lt;/div\u0026gt; 26\u0026lt;/div\u0026gt; 27{{ end }} 在 layouts/_default/single.html 对应位置加入：\n1{{ if .Params.links }} 2 {{ partial \u0026#34;article/components/links\u0026#34; . }} 3{{ end }} 4 5\u0026lt;!-- Add the series partial here --\u0026gt; 6{{ partial \u0026#34;series.html\u0026#34; . }} 7 8{{ partial \u0026#34;article/components/related-content\u0026#34; . }} 在 hugo.yaml 最后加入：\n1taxonomies: 2 category: categories 3 tag: tags 4 series: series 在多语言配置文件 i18n/zh-cn.yaml 最后面加入：\n1partOf: 2 other: \u0026#34;本文属于\u0026#34; 3 4series: 5 other: \u0026#34;系列\u0026#34; 6 7currentArticle: 8 other: \u0026#34;本文\u0026#34; 在 assets/scss/custom.scss 最后加入：\n1.article-series { 2 margin: 2rem 0; 3 padding: 1rem; 4 gap: 0; 5 color: var(--card-text-color-main); 6 background-color: var(--card-background); 7 border-radius: var(--card-border-radius); 8 box-shadow: var(--shadow-l1); 9} 10 11.article-series-item { 12 margin-bottom: 0.8rem; 13} 14 15.series-section-title { 16 margin-top: 8px; 17 margin-left: 8px; 18} 19 20.section-description { 21 color: var(--card-text-color-main); 22 font-weight: bold; 23} 24 25.article-series-container { 26 margin-bottom: -8px; 27} 新建 layouts/series/list.html：\n1{{ define \u0026#34;body-class\u0026#34; }}template-series{{ end }} 2{{ define \u0026#34;main\u0026#34; }} 3 \u0026lt;header\u0026gt; 4 {{/* Display the series title and description */}} 5 \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;{{ .Title }} {{ i18n \u0026#34;series\u0026#34; }}\u0026lt;/h2\u0026gt; 6 {{ if .Description }} 7 \u0026lt;div class=\u0026#34;section-description\u0026#34;\u0026gt; 8 {{ .Description }} 9 \u0026lt;/div\u0026gt; 10 {{ end }} 11 \u0026lt;/header\u0026gt; 12 13 {{/* Get the pages associated with this specific series term */}} 14 {{ $pagesInSeries := .Pages }} 15 16 {{/* Check if there are any pages in this series */}} 17 {{ if $pagesInSeries }} 18 {{/* Group the pages in this series by year, ordered reverse chronologically (newest year first) */}} 19 {{ range ($pagesInSeries.GroupByDate \u0026#34;2006\u0026#34;).Reverse }} 20 {{/* Create an ID for the year group, e.g., \u0026#34;2023\u0026#34; */}} 21 {{ $id := lower (replace .Key \u0026#34; \u0026#34; \u0026#34;-\u0026#34;) }} 22 \u0026lt;div class=\u0026#34;archives-group\u0026#34; id=\u0026#34;{{ $id }}\u0026#34;\u0026gt; 23 {{/* Display the year as a section title, linking to the ID */}} 24 \u0026lt;h2 class=\u0026#34;archives-date section-title\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;{{ $.RelPermalink }}#{{ $id }}\u0026#34;\u0026gt;{{ .Key }}\u0026lt;/a\u0026gt;\u0026lt;/h2\u0026gt; 25 {{/* Use the compact article list style */}} 26 \u0026lt;div class=\u0026#34;article-list--compact\u0026#34;\u0026gt; 27 {{/* Range through the pages within this year group (already sorted by date by Hugo) */}} 28 {{ range .Pages }} 29 {{/* Display each article using the compact partial */}} 30 {{ partial \u0026#34;article-list/compact\u0026#34; . }} 31 {{ end }} 32 \u0026lt;/div\u0026gt; 33 \u0026lt;/div\u0026gt; 34 {{ end }} 35 {{ else }} 36 \u0026lt;p\u0026gt;{{ i18n \u0026#34;noPostsInSeries\u0026#34; | default \u0026#34;There are no articles in this series yet.\u0026#34; }}\u0026lt;/p\u0026gt; 37 {{/* Optional: Add a default message or translation for when a series is empty */}} 38 {{/* You might need to add \u0026#34;noPostsInSeries\u0026#34; to your i18n files */}} 39 {{ end }} 40 41 {{/* Footer partial */}} 42 {{ partialCached \u0026#34;footer/footer\u0026#34; . }} 43{{ end }} 新建 layouts/series/terms.html：\n1{{ define \u0026#34;body-class\u0026#34; }}template-series{{ end }} 2{{ define \u0026#34;main\u0026#34; }} 3 \u0026lt;header\u0026gt; 4 {{- $taxonomy := $.Site.GetPage \u0026#34;taxonomyTerm\u0026#34; \u0026#34;series\u0026#34; -}} 5 {{- $terms := $taxonomy.Pages -}} 6 {{ if $terms }} 7 \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;{{ i18n \u0026#34;series\u0026#34; }}\u0026lt;/h2\u0026gt; 8 \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; 9 \u0026lt;div class=\u0026#34;article-list--tile\u0026#34; style=\u0026#34;display: flex; flex-direction: column;\u0026#34;\u0026gt; 10 {{ range $terms }} 11 \u0026lt;div class=\u0026#34;series-group\u0026#34; style=\u0026#34;flex: 1 auto; margin: 10px;\u0026#34;\u0026gt; 12 \u0026lt;h3 class=\u0026#34;series-title\u0026#34;\u0026gt; 13 \u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34;\u0026gt;{{ .Title }}\u0026lt;/a\u0026gt; 14 \u0026lt;/h3\u0026gt; 15 {{ if .Description }} 16 \u0026lt;div class=\u0026#34;series-description\u0026#34;\u0026gt; 17 {{ .Description }} 18 \u0026lt;/div\u0026gt; 19 {{ end }} 20 \u0026lt;div class=\u0026#34;article-list--horizontal\u0026#34; style=\u0026#34;display: flex; overflow-x: auto;\u0026#34;\u0026gt; 21 {{ $articles := where .Site.RegularPages \u0026#34;Params.series\u0026#34; \u0026#34;intersect\u0026#34; (slice .Title) }} 22 {{ range $index, $element := $articles }} 23 \u0026lt;div class=\u0026#34;article-tile\u0026#34; style=\u0026#34;flex: 0 0 auto; margin: 5px;\u0026#34;\u0026gt; 24 {{ partial \u0026#34;article-list/tile\u0026#34; (dict \u0026#34;context\u0026#34; $element \u0026#34;size\u0026#34; \u0026#34;250x150\u0026#34; \u0026#34;Type\u0026#34; \u0026#34;taxonomy\u0026#34;) }} 25 \u0026lt;/div\u0026gt; 26 {{ end }} 27 \u0026lt;/div\u0026gt; 28 \u0026lt;/div\u0026gt; 29 {{ end }} 30 \u0026lt;/div\u0026gt; 31 \u0026lt;/div\u0026gt; 32 {{ end }} 33 \u0026lt;/header\u0026gt; 34 35 {{ partialCached \u0026#34;footer/footer\u0026#34; . }} 36{{ end }} 然后只需要在文章头部加入：series: [\u0026quot;Hugo 博客搭建\u0026quot;] 就可以啦。\n过期提醒 参考在 MemE 主题文章开头添加过时提醒。由于我不想每篇文章开头都加上这个提醒，所以把它写成了短代码：\n1\u0026lt;!-- layouts/shortcodes/expiration-notice.html --\u0026gt; 2\u0026lt;div id=\u0026#34;update-info\u0026#34; class=\u0026#34;article-notice update-notice\u0026#34;\u0026gt; 3 \u0026lt;span class=\u0026#34;notice-icon\u0026#34;\u0026gt; 4 {{ partial \u0026#34;helper/icon\u0026#34; \u0026#34;clock\u0026#34; }} 5 \u0026lt;/span\u0026gt; 6 \u0026lt;span class=\u0026#34;notice-content\u0026#34;\u0026gt; 7 \u0026lt;p id=\u0026#34;update-text\u0026#34;\u0026gt;上次更新于 {{ .Page.Lastmod.Format \u0026#34;2006-01-02\u0026#34; }}，部分内容可能已经过期。\u0026lt;/p\u0026gt; 8 \u0026lt;/span\u0026gt; 9\u0026lt;/div\u0026gt; 10 11\u0026lt;script\u0026gt; 12 document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { 13 const lastModified = new Date(\u0026#34;{{ .Page.Lastmod.Format \u0026#34;2006-01-02T15:04:05-0700\u0026#34; }}\u0026#34;); 14 const now = new Date(); 15 const duration = now - lastModified; 16 const days = Math.floor(duration / (1000 * 60 * 60 * 24)); 17 const years = Math.floor(days / 365); 18 const remainingDays = days % 365; 19 20 let updateText = \u0026#34;\u0026#34;; 21 if (years \u0026gt;= 1) { 22 updateText = `上次更新于 ${years} 年 ${remainingDays} 天前，部分内容可能已经过期。`; 23 } else { 24 updateText = `上次更新于 ${days} 天前，部分内容可能已经过期。`; 25 } 26 27 document.getElementById(\u0026#34;update-text\u0026#34;).textContent = updateText; 28 }); 29\u0026lt;/script\u0026gt; 1.article-notice { 2 display: flex; 3 align-items: center; 4 padding: 0.75rem 1rem; 5 margin: 1rem 0; 6 border-radius: 0.8rem; 7 background-color: var(--card-background); 8 border-left: 3.5px solid var(--accent-color); 9} 10 11.update-notice { 12 background-color: rgba(var(--primary-color-rgb), 0.1); 13 border-left-color: var(--primary-color); 14} 15 16.notice-icon { 17 margin-right: 0.75rem; 18 color: var(--primary-color); 19 margin-top: 0.75rem; 20} 21 22.notice-content p { 23 margin: 0; 24 color: crimson; 25 font-weight: bold; 26} 27 28[data-scheme=dark] .notice-content p { 29 color: gold; 30} 在文章开头或者任意地方加入：\n1{{\u0026lt; expiration-notice \u0026gt;}} 上次更新于 2026-04-08，部分内容可能已经过期。\n代码变化显示 参考实验田-diffcode，新建 /layouts/partials/diffcode.html：\n1{{- $diffBlock := (. | replaceRE \u0026#34;(?m:^```\\\\w+)\u0026#34; \u0026#34;```diff\u0026#34;) -}} 2{{- $diffBlock := (index (split $diffBlock \u0026#34;```\u0026#34;) 1) -}} 3{{- $diffBlock := ($diffBlock | replaceRE \u0026#34;(?m:^([+\\\\-~])?.*$)\u0026#34; \u0026#34;$1\u0026#34;) -}} 4{{- $diffBlock := ($diffBlock | replaceRE \u0026#34;(?m:^$)\u0026#34; \u0026#34;.\u0026#34; | replaceRE \u0026#34;\\n\u0026#34; \u0026#34;\u0026#34;) -}} 5 6{{- $lines := split $diffBlock \u0026#34;\u0026#34; -}} 7{{- $mainBlock := (. | replaceRE \u0026#34;(?m:^[+\\\\-~])\u0026#34; \u0026#34;\u0026#34;) | markdownify -}} 8 9{{- $lineCount := 0 -}} 10{{- $output := \u0026#34;\u0026#34; -}} 11 12{{- $diffBlock := (. | replaceRE \u0026#34;(?m:^```\\\\w+)\u0026#34; \u0026#34;```diff\u0026#34;) -}} 13{{- $diffBlock := (index (split $diffBlock \u0026#34;```\u0026#34;) 1) -}} 14{{- $diffBlock := ($diffBlock | replaceRE \u0026#34;(?m:^([+\\\\-~])?.*$)\u0026#34; \u0026#34;$1\u0026#34;) -}} 15{{- $diffBlock := ($diffBlock | replaceRE \u0026#34;(?m:^$)\u0026#34; \u0026#34;.\u0026#34; | replaceRE \u0026#34;\\n\u0026#34; \u0026#34;\u0026#34;) -}} 16 17{{- $lines := split $diffBlock \u0026#34;\u0026#34; -}} 18{{- $mainBlock := (. | replaceRE \u0026#34;(?m:^[+\\\\-~])\u0026#34; \u0026#34;\u0026#34;) | markdownify -}} 19 20{{- $lineCount := 0 -}} 21{{- $output := \u0026#34;\u0026#34; -}} 22 23{{- range $i, $l := (split $mainBlock `\u0026lt;span class=\u0026#34;line\u0026#34;\u0026gt;`) -}} 24 {{- if gt $i 0 -}} 25 {{- $lineClass := \u0026#34;\u0026#34; -}} 26 {{- if and (gt $lineCount 0) (lt $lineCount (len $lines)) -}} 27 {{- if eq (index $lines (sub $lineCount -1)) \u0026#34;+\u0026#34; -}} 28 {{- $lineClass = \u0026#34;diff-add\u0026#34; -}} 29 {{- else if eq (index $lines (sub $lineCount -1)) \u0026#34;-\u0026#34; -}} 30 {{- $lineClass = \u0026#34;diff-remove\u0026#34; -}} 31 {{- else if eq (index $lines (sub $lineCount -1)) \u0026#34;~\u0026#34; -}} 32 {{- $lineClass = \u0026#34;diff-highlight\u0026#34; -}} 33 {{- end -}} 34 {{- end -}} 35 36 {{- if $lineClass -}} 37 {{- $output = printf \u0026#34;%s\u0026lt;span class=\\\u0026#34;line %s\\\u0026#34;\u0026gt;\u0026#34; $output $lineClass -}} 38 {{- else -}} 39 {{- $output = printf \u0026#34;%s\u0026lt;span class=\\\u0026#34;line\\\u0026#34;\u0026gt;\u0026#34; $output -}} 40 {{- end -}} 41 42 {{- $lineCount = add $lineCount 1 -}} 43 {{- else -}} 44 {{- $output = $l -}} 45 {{- end -}} 46 47 {{- if gt $i 0 -}} 48 {{- $output = printf \u0026#34;%s%s\u0026#34; $output $l -}} 49 {{- end -}} 50{{- end -}} 51 52{{ $output | safeHTML }} 新建 /layouts/shortcodes/diffcode.html：\n1{{ partial \u0026#34;diffcode.html\u0026#34; .Inner }} 在 /assets/scss/custom.scss 中加入：\n1.highlight { 2 .line { 3 \u0026amp;.diff-add { 4 background-color: rgba(0, 255, 0, 0.2); 5 6 \u0026amp;::before { 7 content: \u0026#34;+\u0026#34;; 8 color: green; 9 position: absolute; 10 left: 0.5em; 11 user-select: none; 12 } 13 } 14 15 \u0026amp;.diff-remove { 16 background-color: rgba(255, 0, 0, 0.2); 17 18 \u0026amp;::before { 19 content: \u0026#34;-\u0026#34;; 20 color: red; 21 position: absolute; 22 left: 1.1rem; 23 user-select: none; 24 } 25 } 26 27 \u0026amp;.diff-highlight { 28 background-color: rgba(255, 255, 0, 0.2); 29 30 \u0026amp;::before { 31 content: \u0026#34;~\u0026#34;; 32 color: orange; 33 position: absolute; 34 left: 0.5em; 35 user-select: none; 36 } 37 } 38 } 39} 40 41// 确保行号正确对齐 42.highlight table td { 43 padding: 5px; 44} 45 46.highlight table pre { 47 margin: 0; 48} 49 50// 为行号留出足够空间 51.highlight .lnt, .highlight .ln { 52 margin-right: 0.8em; 53 padding: 0 0.4em 0 0.4em; 54} 可以这样使用这个短代码：\n1{{\u0026lt; diffcode \u0026gt;}} 2```python 3def hello_world(): 4+ print(\u0026#34;Hello, World!\u0026#34;) 5- print(\u0026#34;Goodbye, World!\u0026#34;) 6~ return None 7{{\u0026lt; /diffcode \u0026gt;}} 1def hello_world(): 2 print(\u0026#34;Hello, World!\u0026#34;) 3 print(\u0026#34;Goodbye, World!\u0026#34;) 4 return None 代码块字体样式更改 代码块的字体还是用 \u0026lsquo;JetBrains Mono\u0026rsquo; 更有味道。在自定义的scss里加入：\n1.highlight *, 2.highlight pre, 3.highlight code, 4.chroma *, 5.chroma code, 6.chroma pre { 7 font-family: \u0026#39;JetBrains Mono\u0026#39;, \u0026#34;LXGW WenKai TC\u0026#34;, \u0026#34;Gowun Batang\u0026#34;, monospace; 8 font-size: 14px; 9} 个人主页 成品展示：ifantic.de\n無名の主页 简单的小主页，原来的看够了，重新弄了一个 Vue 我用的是赛博菩萨 CloudFlare Pages 部署的。先把项目clone到本地，安装node，然后安装pnpm，安装依赖：\n1npm install -g pnpm 2pnpm install 3// 预览 4pnpm dev 5// 构建 6pnpm build 然后把 .env.example 文件的名称修改为 .env，修改其中的配置。本地运行 pnpm dev 预览即可。\n预览后部署到 Cloudflare Pages：\n创建项目：点击 创建应用程序 -\u0026gt; 选择 Pages 选项卡 -\u0026gt; 点击 上传资源。 上传文件：给你的项目起个名字，然后将本地 dist 文件夹内的所有文件和子文件夹拖拽到上传区域，或者通过浏览文件选择 dist 文件夹。 部署站点：点击 部署站点。Cloudflare 会上传你的文件并完成部署。 修复站点名称字体不一致 将 public/font 的 Pacifico-Regular-all.ttf 替换原来的 Pacifico-Regular.ttf。\n修复默认的天气api 修改 src/api/index.js 第73行：\n1// https://api.oioweb.cn/doc/weather/GetWeather 2export const getOtherWeather = async () =\u0026gt; { 3 const res = await fetch(\u0026#34;https://api.oioweb.cn/api/weather/GetWeather\u0026#34;); 4 const res = await fetch(\u0026#34;https://weather.sl.al/\u0026#34;); 5 return await res.json(); 6}; 更改 src/components/Weather.vue 为以下内容：\n1\u0026lt;template\u0026gt; 2 \u0026lt;div class=\u0026#34;weather\u0026#34; v-if=\u0026#34;weatherData.adCode.city \u0026amp;\u0026amp; weatherData.weather.weather\u0026#34;\u0026gt; 3 \u0026lt;span v-if=\u0026#34;!mainKey\u0026#34;\u0026gt;{{ weatherData.adCode.region }}\u0026amp;nbsp;\u0026lt;/span\u0026gt; 4 \u0026lt;span\u0026gt;{{ weatherData.adCode.city }}\u0026amp;nbsp;\u0026lt;/span\u0026gt; 5 \u0026lt;span\u0026gt;{{ weatherData.weather.weather }}\u0026amp;nbsp;\u0026lt;/span\u0026gt; 6 \u0026lt;span\u0026gt;{{ weatherData.weather.temperature }}℃\u0026lt;/span\u0026gt; 7\u0026lt;!-- 8 \u0026lt;span class=\u0026#34;sm-hidden\u0026#34;\u0026gt; 9 \u0026amp;nbsp;{{ 10 weatherData.weather.winddirection?.endsWith(\u0026#34;风\u0026#34;) 11 ? weatherData.weather.winddirection 12 : weatherData.weather.winddirection + \u0026#34;风\u0026#34; 13 }}\u0026amp;nbsp; 14 \u0026lt;/span\u0026gt; 15 \u0026lt;span class=\u0026#34;sm-hidden\u0026#34;\u0026gt;{{ weatherData.weather.windpower }}\u0026amp;nbsp;级\u0026lt;/span\u0026gt; 16--\u0026gt; 17 \u0026lt;template v-if=\u0026#34;mainKey\u0026#34;\u0026gt; 18 \u0026lt;span class=\u0026#34;sm-hidden\u0026#34;\u0026gt; 19 \u0026amp;nbsp;{{ 20 weatherData.weather.winddirection?.endsWith(\u0026#34;风\u0026#34;) 21 ? weatherData.weather.winddirection 22 : weatherData.weather.winddirection + \u0026#34;风\u0026#34; 23 }}\u0026amp;nbsp; 24 \u0026lt;/span\u0026gt; 25 \u0026lt;span class=\u0026#34;sm-hidden\u0026#34;\u0026gt;{{ weatherData.weather.windpower }}\u0026amp;nbsp;级\u0026lt;/span\u0026gt; 26 \u0026lt;/template\u0026gt; 27 \u0026lt;/div\u0026gt; 28 \u0026lt;div class=\u0026#34;weather\u0026#34; v-else\u0026gt; 29 \u0026lt;span\u0026gt;天气数据获取失败\u0026lt;/span\u0026gt; 30 \u0026lt;/div\u0026gt; 31\u0026lt;/template\u0026gt; 32 33 34 35 36 37\u0026lt;script setup\u0026gt; 38import { getAdcode, getWeather, getOtherWeather } from \u0026#34;@/api\u0026#34;; 39import { Error } from \u0026#34;@icon-park/vue-next\u0026#34;; 40 41// 高德开发者 Key 42const mainKey = import.meta.env.VITE_WEATHER_KEY; 43 44// 天气数据 45const weatherData = reactive({ 46 adCode: { 47 city: null, // 城市 48 adcode: null, // 城市编码 49 }, 50 weather: { 51 weather: null, // 天气现象 52 temperature: null, // 实时气温 53 winddirection: null, // 风向描述 54 windpower: null, // 风力级别 55 }, 56}); 57 58// 取出天气平均值 59const getTemperature = (min, max) =\u0026gt; { 60 try { 61 // 计算平均值并四舍五入 62 const average = (Number(min) + Number(max)) / 2; 63 return Math.round(average); 64 } catch (error) { 65 console.error(\u0026#34;计算温度出现错误：\u0026#34;, error); 66 return \u0026#34;NaN\u0026#34;; 67 } 68}; 69 70// 获取天气数据 71const getWeatherData = async () =\u0026gt; { 72 try { 73 // 获取地理位置信息 74 if (!mainKey) { 75 76/* 77 console.log(\u0026#34;未配置，使用备用天气接口\u0026#34;); 78 const result = await getOtherWeather(); 79 console.log(result); 80 const data = result.result; 81 weatherData.adCode = { 82 city: data.city.City || \u0026#34;未知地区\u0026#34;, 83 // adcode: data.city.cityId, 84 }; 85 weatherData.weather = { 86 weather: data.condition.day_weather, 87 temperature: getTemperature(data.condition.min_degree, data.condition.max_degree), 88 winddirection: data.condition.day_wind_direction, 89 windpower: data.condition.day_wind_power, 90 }; 91*/ 92 93 console.log(\u0026#34;未配置，使用备用天气接口\u0026#34;); 94 const result = await getOtherWeather(); 95 console.log(result); 96 const data = result; // 不需要 .result 了，因为数据在顶层 97 98 weatherData.adCode = { 99 city: data.location?.city || \u0026#34;未知地区\u0026#34;, 100 // 如果需要 region 也可以加上 101 region: data.location?.region 102 }; 103 104 weatherData.weather = { 105 weather: data.current?.description, // 天气描述 106 temperature: data.current?.temperature, // 温度 107 // 如果需要体感温度 108 // feelsLike: data.current?.feelsLike, 109 110 // 如果新的数据结构中没有风向和风力数据，可以设置默认值 111 //winddirection: \u0026#34;暂无数据\u0026#34;, 112 //windpower: \u0026#34;暂无数据\u0026#34;, 113 114 // 如果需要添加空气质量信息 115 //airQuality: { 116 // category: data.current?.airQuality?.category, 117 // statement: data.current?.airQuality?.statement 118 //} 119 }; 120 121 } else { 122 // 获取 Adcode 123 const adCode = await getAdcode(mainKey); 124 console.log(adCode); 125 if (adCode.infocode !== \u0026#34;10000\u0026#34;) { 126 throw \u0026#34;地区查询失败\u0026#34;; 127 } 128 weatherData.adCode = { 129 city: adCode.city, 130 adcode: adCode.adcode, 131 }; 132 // 获取天气信息 133 const result = await getWeather(mainKey, weatherData.adCode.adcode); 134 weatherData.weather = { 135 weather: result.lives[0].weather, 136 temperature: result.lives[0].temperature, 137 winddirection: result.lives[0].winddirection, 138 windpower: result.lives[0].windpower, 139 }; 140 } 141 } catch (error) { 142 console.error(\u0026#34;天气信息获取失败:\u0026#34; + error); 143 onError(\u0026#34;天气信息获取失败\u0026#34;); 144 } 145}; 146 147// 报错信息 148const onError = (message) =\u0026gt; { 149 ElMessage({ 150 message, 151 icon: h(Error, { 152 theme: \u0026#34;filled\u0026#34;, 153 fill: \u0026#34;#efefef\u0026#34;, 154 }), 155 }); 156 console.error(message); 157}; 158 159onMounted(() =\u0026gt; { 160 // 调用获取天气 161 getWeatherData(); 162}); 163\u0026lt;/script\u0026gt; 小记 好像什么都没干一年就过去了。。。 ","date":"2025-05-08T18:42:25+08:00","image":"https://langminjeii.ifantic.de/post/meihua-3.png","permalink":"https://langminjeii.ifantic.de/post/hugo-stack%E3%81%AE%E7%BE%8E%E5%8C%96-iii/","title":"Hugo-stackの美化 III"},{"content":"Gin 的简单入门：基于 Gin 框架的登录 / 注册表单验证实例，Gin 中间件的原理分析，Gin 返回 html，静态文件的挂载和 Gin 优雅的退出。\n介绍 Gin 是一个用 Go 编写的 HTTP Web 框架。 它具有类似 Martini 的 API，但性能比 Martini 快 40 倍。它运行速度快，具有分组的路由器，良好的崩溃捕获和错误处理，非常好的支持中间件和json。如果你需要极好的性能，使用 Gin 吧。\n用以下命令安装：\n1D:\\user\\go\u0026gt;go get -u github.com/gin-gonic/gin 2 3go: module github.com/gin-gonic/gin: Get \u0026#34;https://proxy.golang.org/github.com/gin-gonic/gin/@v/list\u0026#34;: dial tcp 142.250.69.209:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond. 但是我在安装的时候碰到了上面这个报错。我的解决办法：\n1go env -w GOPROXY=https://goproxy.cn/ 快速入门 1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9//handle方法 10func Pong(c *gin.Context) { 11 c.JSON(http.StatusOK, gin.H{ 12 \u0026#34;name\u0026#34;: \u0026#34;ice_moss\u0026#34;, 13 \u0026#34;age\u0026#34;: 18, 14 \u0026#34;school\u0026#34;: \u0026#34;家里蹲大学\u0026#34;, 15 }) 16} 17 18func main() { 19 //初始化一个gin的server对象 20 //Default实例化对象具有日志和返回状态功能 21 r := gin.Default() 22 //注册路由，并编写处理方法 23 r.GET(\u0026#34;/ping\u0026#34;, Pong) 24 //监听端口：默认端口listen and serve on 0.0.0.0:8080 25 r.Run(\u0026#34;:8083\u0026#34;) 26} c.JSON()c.JSON() 方法用于发送 JSON 格式的响应:\n第一个参数 http.StatusOK 表示返回 200 状态码 第二个参数 gin.H{} 是 map[string]interface{} 的简写，用于构造 JSON 对象 响应包含三个字段：name、age 和 school gin.Default() 初始化带默认中间件的 Gin 引擎，包括日志记录和错误恢复功能\nr.GET(\u0026quot;/ping\u0026quot;, Pong) 注册一个 GET 类型的路由，当访问 \u0026ldquo;/ping\u0026rdquo; 路径时，调用 Pong 函数处理请求\nr.Run(\u0026quot;:8083\u0026quot;) 启动 HTTP 服务器并监听 8083 端口，默认情况下 Gin 会监听 0.0.0.0:8080\n这是一个简单的 RESTful API 示例，当用户向 http://localhost:8083/ping 发送 GET 请求时，服务器会返回一个包含 name、age 和 school 信息的 JSON 响应。这种模式是构建 Web API 的基础，可以轻松扩展为更复杂的应用程序。\nGin 的 GET 和 POST 方法 1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9func GinGet(c *gin.Context) { 10 c.JSON(http.StatusOK, map[string]interface{}{ 11 \u0026#34;name\u0026#34;: \u0026#34;ice_moss\u0026#34;, 12 }) 13} 14 15func GinPost(c *gin.Context) { 16 c.JSON(http.StatusOK, gin.H{ 17 \u0026#34;token\u0026#34;: \u0026#34;您好\u0026#34;, 18 }) 19} 20func main() { 21 router := gin.Default() 22 router.GET(\u0026#34;/GinGet\u0026#34;, GinGet) 23 router.POST(\u0026#34;/GinPost\u0026#34;, GinPost) 24 router.Run(\u0026#34;:8083\u0026#34;) 25} 我们看到 GinGet 和 GinPost 这两个方法中的 c.JSON () 第二个参数不一样，原因：gin.H{} 本质就是一个 map[string]interface{}\n1//H is a shortcut for map[string]interface{} 2type H map[string]any 然后我们就可以在浏览器中访问：localhost:8083/GinGet\n这里需要注意我们不能直接在浏览器中访问：localhost:8083/GinPost，因为他用的是 POST 方法。可以使用 postman 来发送 POST 请求：\n路由分组 Gin 为我们做了很好的路由分组，这样我们可以方便，对路由进行管理：\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9func ProductLists(c *gin.Context) { 10 c.JSON(http.StatusOK, gin.H{ 11 \u0026#34;矿泉水\u0026#34;: [5]string{\u0026#34;娃哈哈\u0026#34;, \u0026#34;2元\u0026#34;, \u0026#34;500\u0026#34;}, 12 \u0026#34;功能饮料\u0026#34;: [3]string{\u0026#34;红牛\u0026#34;, \u0026#34;6元\u0026#34;, \u0026#34;200\u0026#34;}, 13 }) 14} 15 16func Prouduct1(c *gin.Context) { 17 req := c.Param(\u0026#34;haha\u0026#34;) 18 19 c.JSON(http.StatusOK, gin.H{ 20 \u0026#34;矿泉水\u0026#34;: [5]string{\u0026#34;娃哈哈矿泉水\u0026#34;, \u0026#34;2元\u0026#34;, \u0026#34;500\u0026#34;}, 21 \u0026#34;token\u0026#34;: req, 22 }) 23} 24 25func CreateProduct(c *gin.Context) {} 26 27//路由分组 28func main() { 29 router := gin.Default() 30 31 //未使用路由分组 32 //获取商品列表 33 //router.GET(\u0026#34;/ProductList\u0026#34;, ProductLists) 34 //获取某一个具体商品信息 35 //router.GET(\u0026#34;/ProductList/1\u0026#34;, Prouduct1) 36 //添加商品 37 //router.POST(\u0026#34;ProductList/Add\u0026#34;, CreateProduct) 38 39 //路由分组 40 ProductList := router.Group(\u0026#34;/Produc\u0026#34;) 41 { 42 ProductList.GET(\u0026#34;/list\u0026#34;, ProductLists) 43 ProductList.GET(\u0026#34;/1\u0026#34;, Prouduct1) 44 ProductList.POST(\u0026#34;/Add\u0026#34;, CreateProduct) 45 } 46 router.Run(\u0026#34;:8083\u0026#34;) 47} ProductLists 函数: 1func ProductLists(c *gin.Context) { 2 c.JSON(http.StatusOK, gin.H{ 3 \u0026#34;矿泉水\u0026#34;: [5]string{\u0026#34;娃哈哈\u0026#34;, \u0026#34;2元\u0026#34;, \u0026#34;500\u0026#34;}, 4 \u0026#34;功能饮料\u0026#34;: [3]string{\u0026#34;红牛\u0026#34;, \u0026#34;6元\u0026#34;, \u0026#34;200\u0026#34;}, 5 }) 6} 返回了一个JSON格式的商品列表，包含矿泉水和功能饮料的信息。\nProuduct1 函数: 1func Prouduct1(c *gin.Context) { 2 req := c.Param(\u0026#34;haha\u0026#34;) 3 c.JSON(http.StatusOK, gin.H{ 4 \u0026#34;矿泉水\u0026#34;: [5]string{\u0026#34;娃哈哈矿泉水\u0026#34;, \u0026#34;2元\u0026#34;, \u0026#34;500\u0026#34;}, 5 \u0026#34;token\u0026#34;: req, 6 }) 7} 通过c.Param()获取URL中名为\u0026quot;haha\u0026quot;的路径参数，并返回特定商品信息和该参数值。（参考下一节）\nCreateProduct 函数: 一个空函数，预留用于创建商品的功能实现。 使用路由分组后，所有路由都共享\u0026quot;/Produc\u0026quot;前缀，最终生成的API路径为:\nGET /Produc/list - 调用ProductLists处理函数 GET /Produc/1 - 调用Prouduct1处理函数 POST /Produc/Add - 调用CreateProduct处理函数 URL 值的提取 很多时候我们需要对 URL 中数据的提取，或者动态的 URL，我们不可能将 URL 写成固定的。\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9func ProductLists(c *gin.Context) { 10 c.JSON(http.StatusOK, gin.H{ 11 \u0026#34;矿泉水\u0026#34;: [5]string{\u0026#34;娃哈哈矿泉水\u0026#34;, \u0026#34;2元\u0026#34;, \u0026#34;500\u0026#34;}, 12 \u0026#34;功能饮料\u0026#34;: [3]string{\u0026#34;红牛\u0026#34;, \u0026#34;6元\u0026#34;, \u0026#34;200\u0026#34;}, 13 }) 14} 15 16func Prouduct1(c *gin.Context) { 17 //获取url中的参数 18 id := c.Param(\u0026#34;id\u0026#34;) 19 action := c.Param(\u0026#34;action\u0026#34;) 20 c.JSON(http.StatusOK, gin.H{ 21 \u0026#34;矿泉水\u0026#34;: [5]string{\u0026#34;娃哈哈矿泉水\u0026#34;, \u0026#34;2元\u0026#34;, \u0026#34;500\u0026#34;}, 22 \u0026#34;id\u0026#34;: id, 23 \u0026#34;action\u0026#34;: action, 24 }) 25} 26 27func CreateProduct(c *gin.Context) {} 28 29//url取值 30func main() { 31 router := gin.Default() 32 //路由分组 33 ProductList := router.Group(\u0026#34;/Product\u0026#34;) 34 { 35 ProductList.GET(\u0026#34;\u0026#34;, ProductLists) 36 //使用\u0026#34;/:id\u0026#34;动态匹配参数 37 ProductList.GET(\u0026#34;/:id/:action\u0026#34;, Prouduct1) 38 ProductList.POST(\u0026#34;\u0026#34;, CreateProduct) 39 } 40 router.Run(\u0026#34;:8083\u0026#34;) 41} 访问 localhost:8083/Product/01/product1 {\u0026quot;action\u0026quot;:\u0026quot;product1\u0026quot;,\u0026quot;id\u0026quot;:\u0026quot;01\u0026quot;,\u0026quot;矿泉水\u0026quot;:[\u0026quot;娃哈哈矿泉水\u0026quot;,\u0026quot;2元\u0026quot;,\u0026quot;500\u0026quot;,\u0026quot;\u0026quot;,\u0026quot;\u0026quot;]} 访问 localhost:8083/Product/100/product2000 {\u0026quot;action\u0026quot;:\u0026quot;product2000\u0026quot;,\u0026quot;id\u0026quot;:\u0026quot;100\u0026quot;,\u0026quot;矿泉水\u0026quot;:[\u0026quot;娃哈哈矿泉水\u0026quot;,\u0026quot;2元\u0026quot;,\u0026quot;500\u0026quot;,\u0026quot;\u0026quot;,\u0026quot;\u0026quot;]} 关键部分是路由注册，使用了 冒号语法 ( :parameter ) 来定义动态路由参数:\n1ProductList.GET(\u0026#34;/:id/:action\u0026#34;, Prouduct1) :id - 匹配 URL 中的第一个路径段 :action - 匹配 URL 中的第二个路径段 任何形如 /Product/任意值/任意值 的请求都会被路由到 Prouduct1 处理函数。而在 Prouduct1 处理函数中，使用 c.Param() 方法提取路由参数值:\n1id := c.Param(\u0026#34;id\u0026#34;) 2action := c.Param(\u0026#34;action\u0026#34;) 这种方式可以获取 URL 路径中对应位置的实际值，并将其赋给变量以便在处理函数中使用。\n其他 URL 参数提取方式 除了路径参数外，Gin 还支持其他方式获取 URL 数据:\n查询参数：使用 c.Query() 获取 URL 查询字符串中的参数。例如: /products?category=electronics\u0026amp;sort=price 表单数据：使用 c.PostForm() 获取表单提交的数据 结构体声明并做约束 1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9//结构体声明，并做一些约束 10type Porsen struct { 11 ID int `uri:\u0026#34;id\u0026#34; binding:\u0026#34;required\u0026#34;` //uri指在client中的名字为id，binding:\u0026#34;required指必填 12 Name string `uri:\u0026#34;name\u0026#34; binding:\u0026#34;required\u0026#34;` //同理 13} 14 15//url参数获取 16func main() { 17 router := gin.Default() 18 router.GET(\u0026#34;/:name/:id\u0026#34;, func(c *gin.Context) { 19 //使用porsen对数据进行解组 20 var porsen Porsen 21 if err := c.ShouldBindUri(\u0026amp;porsen); err != nil { 22 c.Status(404) 23 return 24 } 25 c.JSON(http.StatusOK, gin.H{ 26 \u0026#34;name\u0026#34;: porsen.Name, 27 \u0026#34;id\u0026#34;: porsen.ID, 28 }) 29 }) 30 router.Run(\u0026#34;:8083\u0026#34;) 31} 访问 localhost:8083/test100/2000 {\u0026quot;id\u0026quot;:2000,\u0026quot;name\u0026quot;:\u0026quot;test100\u0026quot;} 但是访问 localhost:8083/100/test2000 1找不到 localhost 的网页找不到与以下网址对应的网页：http://localhost:8083/100/test2000 2HTTP ERROR 404 这和我们约束条件一致。\n结构体声明与标签 1type Porsen struct { 2 ID int `uri:\u0026#34;id\u0026#34; binding:\u0026#34;required\u0026#34;` 3 Name string `uri:\u0026#34;name\u0026#34; binding:\u0026#34;required\u0026#34;` 4} 这里，结构体定义了两个重要的标签注解：\nuri:\u0026quot;id\u0026quot; 和 uri:\u0026quot;name\u0026quot; 指定这些字段对应哪些 URL 参数 binding:\u0026quot;required\u0026quot; 表示这些字段在请求中必须存在 路由处理器设置 1router.GET(\u0026#34;/:name/:id\u0026#34;, func(c *gin.Context) { 2 var porsen Porsen 3 if err := c.ShouldBindUri(\u0026amp;porsen); err != nil { 4 c.Status(404) 5 return 6 } 7 // 返回 JSON 响应 8}) 路由模式是 /:name/:id，所以 URL 路径的第一部分映射到 name，第二部分映射到 id c.ShouldBindUri(\u0026amp;porsen) 尝试解析 URL 参数并将它们绑定到结构体字段 为什么一个 URL 可以工作而另一个失败 可工作的 URL：localhost:8083/test100/2000\n在这个 URL 中，\u0026ldquo;test100\u0026rdquo; 被绑定到 Name 字段（字符串类型），\u0026ldquo;2000\u0026rdquo; 被绑定到 ID 字段（整数类型），两个值都符合它们预期的类型因此绑定成功。\n失败的 URL：localhost:8083/100/test2000\n\u0026ldquo;100\u0026rdquo; 被绑定到 Name 字段（字符串），但是 \u0026ldquo;test2000\u0026rdquo; 被绑定到 ID 字段（整数），因为 \u0026ldquo;test2000\u0026rdquo; 无法转换为整数所以绑定失败。\n错误处理 当绑定失败时，代码返回 404 状态码：\n1if err := c.ShouldBindUri(\u0026amp;porsen); err != nil { 2 c.Status(404) 3 return 4} c.ShouldBindUri(\u0026amp;porsen)：尝试从请求的 URI 中解析参数，并将它们绑定到 porsen 结构体中。它会根据结构体中定义的标签(uri:\u0026quot;id\u0026quot;, uri:\u0026quot;name\u0026quot;)来匹配 URL 参数，并进行类型验证。 if err := ... ; err != nil：这是 Go 中常见的写法，将 ShouldBindUri 的返回值赋给 err，然后检查它是否为 nil（即是否有错误）。 c.Status(404)：如果存在错误（例如，参数无法正确绑定），此代码将响应的 HTTP 状态码设置为 404（资源未找到）。 return：提前退出处理函数，防止执行后续代码（也就不会返回正常的 JSON 响应）。 Gin 还提供了 BindUri 方法，它在绑定失败时会自动中止请求并返回 400 状态码。\nURL 参数的提取 GET 请求参数获取 URL 参数的提取是 GET 方法常用的方法，URL 中有需要的参数，例如我们访问这个百度图片：\n图片链接 https://image.baidu.com/search/index?ct=201326592\u0026tn=baiduimage\u0026word=%E5%9B%BE%E7%89%87%E5%A3%81%E7%BA%B8\u0026pn=\u0026spn=\u0026ie=utf-8\u0026oe=utf-8\u0026cl=2\u0026lm=-1\u0026fr=ala\u0026se=\u0026sme=\u0026cs=\u0026os=\u0026objurl=\u0026di=\u0026gsm=1e\u0026dyTabStr=MCwzLDYsMSw0LDIsNSw3LDgsOQ%3D%3D ? 以后的都是参数，用 \u0026amp; 分隔：?ct=201326592 \u0026amp;tn=baiduimage \u0026amp;word=%E5...\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9func Welcom(c *gin.Context) { 10 //DefaultQuery根据字段名获取client请求的数据，client未提供数据则可以设置默认值 11 first_name := c.DefaultQuery(\u0026#34;first_name\u0026#34;, \u0026#34;未知\u0026#34;) 12 last_mame := c.DefaultQuery(\u0026#34;last_name\u0026#34;, \u0026#34;未知\u0026#34;) 13 c.JSON(http.StatusOK, gin.H{ 14 \u0026#34;firstname\u0026#34;: first_name, 15 \u0026#34;lastname\u0026#34;: last_mame, 16 \u0026#34;work\u0026#34;: [...]string{\u0026#34;公司：Tencent\u0026#34;, \u0026#34;职位：Go开发工程师\u0026#34;, \u0026#34;工资：20000\u0026#34;}, 17 }) 18} 19 20//url参数获取 21func main() { 22 //实例化server对象 23 router := gin.Default() 24 router.GET(\u0026#34;/welcom\u0026#34;, Welcom) 25 router.Run(\u0026#34;:8083\u0026#34;) 26} 访问 localhost:8083/welcom?first_name=moss\u0026amp;last_name=ice {\u0026quot;firstname\u0026quot;:\u0026quot;moss\u0026quot;,\u0026quot;lastname\u0026quot;:\u0026quot;ice\u0026quot;,\u0026quot;work\u0026quot;:[\u0026quot;公司：Tencent\u0026quot;,\u0026quot;职位：Go开发工程师\u0026quot;,\u0026quot;工资：20000\u0026quot;]} 这样我们的后台就拿到了 client 提供的参数并做业务处理。\nPOST 表单提交及其数据获取 在很多时候我们都需要使用 post 方法来传输数据，例如用户登录/注册都需要提交表单等。下面我们来看看简单的表单提交：\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9func Postform(c *gin.Context) { 10 UserName := c.DefaultPostForm(\u0026#34;username\u0026#34;, \u0026#34;unkown\u0026#34;) 11 PassWord := c.DefaultPostForm(\u0026#34;password\u0026#34;, \u0026#34;unkown\u0026#34;) 12 if UserName == \u0026#34;ice_moss@163.com\u0026#34; \u0026amp;\u0026amp; PassWord == \u0026#34;123456\u0026#34; { 13 c.JSON(http.StatusOK, gin.H{ 14 \u0026#34;name\u0026#34;: \u0026#34;ice_moss\u0026#34;, 15 \u0026#34;username\u0026#34;: UserName, 16 \u0026#34;tokenstatus\u0026#34;: \u0026#34;认证通过\u0026#34;, 17 }) 18 } else { 19 c.JSON(http.StatusInternalServerError, gin.H{ 20 \u0026#34;tokenstatus\u0026#34;: \u0026#34;认证未通过\u0026#34;, 21 }) 22 } 23} 24 25//url参数获取 26func main() { 27 //实例化server对象 28 router := gin.Default() 29 router.POST(\u0026#34;/Postform\u0026#34;, Postform) 30 router.Run(\u0026#34;:8083\u0026#34;) 31} 由于是 post 请求我们使用 postman 提交表单：localhost:8083/Postform\n记住是 post 请求。后台输出： 1username ice_moss password 18dfdf 2[GIN] 2022/06/23 - 19:24:24 | 500 | 108.029µs | ::1 | POST \u0026#34;/Postform\u0026#34; 表单提交处理函数解释：\n1func Postform(c *gin.Context) { 2 UserName := c.DefaultPostForm(\u0026#34;username\u0026#34;, \u0026#34;unkown\u0026#34;) 3 PassWord := c.DefaultPostForm(\u0026#34;password\u0026#34;, \u0026#34;unkown\u0026#34;) 4 if UserName == \u0026#34;ice_moss@163.com\u0026#34; \u0026amp;\u0026amp; PassWord == \u0026#34;123456\u0026#34; { 5 c.JSON(http.StatusOK, gin.H{ 6 \u0026#34;name\u0026#34;: \u0026#34;ice_moss\u0026#34;, 7 \u0026#34;username\u0026#34;: UserName, 8 \u0026#34;tokenstatus\u0026#34;: \u0026#34;认证通过\u0026#34;, 9 }) 10 } else { 11 c.JSON(http.StatusInternalServerError, gin.H{ 12 \u0026#34;tokenstatus\u0026#34;: \u0026#34;认证未通过\u0026#34;, 13 }) 14 } 15} 表单数据获取 c.DefaultPostForm(\u0026quot;username\u0026quot;, \u0026quot;unkown\u0026quot;) 从 POST 表单数据中获取\u0026quot;username\u0026quot;字段的值。如果该字段不存在，则返回默认值\u0026quot;unkown\u0026quot;。 c.DefaultPostForm(\u0026quot;password\u0026quot;, \u0026quot;unkown\u0026quot;) 同样方式获取密码字段，有默认值作为备选。 认证逻辑 函数检查用户名是否为 ice_moss@163.com 且密码是否为 123456。如果凭证匹配，返回 200 OK 状态码和包含用户信息的 JSON；如果凭证不匹配，返回 500 Internal Server Error 状态码。 1func main() { 2 //实例化server对象 3 router := gin.Default() 4 router.POST(\u0026#34;/Postform\u0026#34;, Postform) 5 router.Run(\u0026#34;:8083\u0026#34;) 6} 创建一个默认的 Gin 路由器( gin.Default() )，它预先配置了 Logger 和 Recovery 中间件； 在\u0026quot;/Postform\u0026quot;路径注册一个 POST 路由，由 Postform 函数处理； 在 8083 端口启动 HTTP 服务器。 GET 和 POST 混合使用 1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9func GetPost(c *gin.Context) { 10 id := c.Query(\u0026#34;id\u0026#34;) 11 page := c.DefaultQuery(\u0026#34;page\u0026#34;, \u0026#34;未知的\u0026#34;) 12 name := c.DefaultPostForm(\u0026#34;name\u0026#34;, \u0026#34;未知的\u0026#34;) 13 password := c.PostForm(\u0026#34;password\u0026#34;) 14 c.JSON(http.StatusOK, gin.H{ 15 \u0026#34;id\u0026#34;: id, 16 \u0026#34;page\u0026#34;: page, 17 \u0026#34;name\u0026#34;: name, 18 \u0026#34;password\u0026#34;: password, 19 }) 20} 21 22//url参数获取 23func main() { 24 //实例化server对象 25 router := gin.Default() 26 27 //GET和POST混合使用 28 router.POST(\u0026#34;/Post\u0026#34;, GetPost) 29 router.Run(\u0026#34;:8083\u0026#34;) 30} 因为是 post 方法使用，访问：localhost:8083/Post?id=1\u0026amp;page=2\n1func GetPost(c *gin.Context) { 2 id := c.Query(\u0026#34;id\u0026#34;) 3 page := c.DefaultQuery(\u0026#34;page\u0026#34;, \u0026#34;未知的\u0026#34;) 4 name := c.DefaultPostForm(\u0026#34;name\u0026#34;, \u0026#34;未知的\u0026#34;) 5 password := c.PostForm(\u0026#34;password\u0026#34;) 6 c.JSON(http.StatusOK, gin.H{ 7 \u0026#34;id\u0026#34;: id, 8 \u0026#34;page\u0026#34;: page, 9 \u0026#34;name\u0026#34;: name, 10 \u0026#34;password\u0026#34;: password, 11 }) 12} 这个函数演示了四种不同的参数获取方法 获取 GET 参数: c.Query(\u0026quot;id\u0026quot;): 获取 URL 中的 id 参数，如果不存在则返回空字符串 c.DefaultQuery(\u0026quot;page\u0026quot;, \u0026quot;未知的\u0026quot;): 获取 URL 中的 page 参数，如果不存在则返回默认值\u0026quot;未知的\u0026quot; 获取 POST 表单数据: c.DefaultPostForm(\u0026quot;name\u0026quot;, \u0026quot;未知的\u0026quot;): 获取 POST 表单中的 name 字段，如果不存在则返回默认值\u0026quot;未知的\u0026quot; c.PostForm(\u0026quot;password\u0026quot;): 获取 POST 表单中的 password 字段，如果不存在则返回空字符串 返回响应: 使用 c.JSON() 将所有参数组合成 JSON 格式返回给客户端 1func main() { 2 //实例化server对象 3 router := gin.Default() 4 5 //GET和POST混合使用 6 router.POST(\u0026#34;/Post\u0026#34;, GetPost) 7 router.Run(\u0026#34;:8083\u0026#34;) 8} 主函数的作用 创建一个 Gin 默认路由器实例 注册 POST 路由 \u0026ldquo;/Post\u0026rdquo;，由 GetPost 函数处理 启动 HTTP 服务器并监听 8083 端口 数据格式 JSON 和 ProtoBuf 我们知道前后端数据交互大多数都是以 json 的格式，Go 也同样满足。我们知道 GRPC 的数据交互是以 ProtoBuf 格式的，这里我们用到了 proto 文件夹下的代码，结构如下：\n1proto 2├── user.pb.go 3└── user.proto 可参考 proto 相关代码： GoWeb框架Gin学习总结proto文件 | Go 技术论坛，把对应的 proto 文件夹放在 Go/src 根目录下。\n下面我们来看看 Go 是如何处理 json 的，如何处理 ProtoBuf 的。\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;StudyGin/HolleGin/ch07/proto\u0026#34; 7 8 \u0026#34;github.com/gin-gonic/gin\u0026#34; 9) 10 11func moreJSON(c *gin.Context) { 12 var msg struct { 13 Nmae string `json:\u0026#34;UserName\u0026#34;` 14 Message string 15 Number int 16 } 17 msg.Nmae = \u0026#34;ice_moss\u0026#34; 18 msg.Message = \u0026#34;This is a test of JSOM\u0026#34; 19 msg.Number = 101 20 21 c.JSON(http.StatusOK, msg) 22} 23 24//使用ProtoBuf 25func returnProto(c *gin.Context) { 26 course := []string{\u0026#34;python\u0026#34;, \u0026#34;golang\u0026#34;, \u0026#34;java\u0026#34;, \u0026#34;c++\u0026#34;} 27 user := \u0026amp;proto.Teacher{ 28 Name: \u0026#34;ice_moss\u0026#34;, 29 Course: course, 30 } 31 //返回protobuf 32 c.ProtoBuf(http.StatusOK, user) 33} 34 35//使用结构体和JSON对结构体字段进行标签，使用protobuf返回值 36func main() { 37 router := gin.Default() 38 router.GET(\u0026#34;/moreJSON\u0026#34;, moreJSON) 39 router.GET(\u0026#34;/someProtoBuf\u0026#34;, returnProto) 40 router.Run(\u0026#34;:8083\u0026#34;) 41} 访问 localhost:8083/moreJSON {\u0026quot;UserName\u0026quot;:\u0026quot;ice_moss\u0026quot;,\u0026quot;Message\u0026quot;:\u0026quot;This is a test of JSOM\u0026quot;,\u0026quot;Number\u0026quot;:101} 当访问 http://localhost:8083/someProtoBuf 的时候，会返回 someProtoBuf 数据的下载文件，当然我们可以使用 GRPC 中的方法将数据接收解析出来。\n主要区别解释 数据格式 JSON： 一种基于文本、人类可读的格式，广泛用于网络 API 和前后端通信。 ProtoBuf： 由 Google 开发的二进制序列化格式，更加紧凑高效。 可读性与效率对比 JSON： 当访问 /moreJSON 时，得到的是人类可读的响应：{\u0026quot;UserName\u0026quot;:\u0026quot;ice_moss\u0026quot;,\u0026quot;Message\u0026quot;:\u0026quot;This is a test of JSOM\u0026quot;,\u0026quot;Number\u0026quot;:101} ProtoBuf： 当访问 /someProtoBuf 时，得到的是二进制数据，会被下载而不是直接显示，因为它是设计给程序解析的，而非直接阅读。 工作原理 JSON： Go 通过结构体标签如 json:\u0026quot;UserName\u0026quot; 内置支持 JSON 序列化，允许自定义字段名。 ProtoBuf： 需要在 .proto 文件中定义数据结构，然后编译成特定语言的代码（此例中是 user.pb.go 文件）。 Gin 解析特殊字符 我们很多时候需要处理特殊的字符，比如：JSON 会将特殊的 HTML 字符替换为对应的 unicode 字符，比如 \u0026lt; 替换为 \\u003c，如果想原样输出 html，则使用 PureJSON。\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 6 \u0026#34;github.com/gin-gonic/gin\u0026#34; 7) 8 9//通常情况下，JSON会将特殊的HTML字符替换为对应的unicode字符，比如\u0026lt;替换为\\u003c，如果想原样输出html，则使用PureJSON 10func main() { 11 router := gin.Default() 12 router.GET(\u0026#34;/json\u0026#34;, func(c *gin.Context) { 13 c.JSON(http.StatusOK, gin.H{ 14 \u0026#34;html\u0026#34;: \u0026#34;\u0026lt;b\u0026gt;您好，世界!\u0026lt;/b\u0026gt;\u0026#34;, 15 }) 16 }) 17 18 router.GET(\u0026#34;/pureJSON\u0026#34;, func(c *gin.Context) { 19 c.PureJSON(http.StatusOK, gin.H{ 20 \u0026#34;html\u0026#34;: \u0026#34;\u0026lt;div\u0026gt;\u0026lt;b\u0026gt;您好，世界!\u0026lt;/b\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;, 21 }) 22 }) 23 router.Run(\u0026#34;:8083\u0026#34;) 24} 访问 localhost:8083/json {\u0026quot;html\u0026quot;:\u0026quot;\\u003cb\\u003e您好，世界!\\u003c/b\\u003e\u0026quot;} 访问 localhost:8083/pureJSON {\u0026quot;html\u0026quot;:\u0026quot;\u0026lt;div\u0026gt;\u0026lt;b\u0026gt;您好，世界!\u0026lt;/b\u0026gt;\u0026lt;/div\u0026gt;\u0026quot;} Gin 翻译器的实现 在这段代码中，我们是将注册代码实现翻译功能：在 Go web 应用程序中使用 Gin 框架结合 go-playground/validator 包实现表单验证，并提供验证错误消息的国际化(i18n)支持。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;net/http\u0026#34; 6 \u0026#34;reflect\u0026#34; 7 \u0026#34;strings\u0026#34; 8 9 \u0026#34;github.com/gin-gonic/gin/binding\u0026#34; 10 \u0026#34;github.com/go-playground/locales/en\u0026#34; 11 \u0026#34;github.com/go-playground/locales/zh\u0026#34; 12 ut \u0026#34;github.com/go-playground/universal-translator\u0026#34; 13 \u0026#34;github.com/go-playground/validator/v10\u0026#34; 14 enTranslations \u0026#34;github.com/go-playground/validator/v10/translations/en\u0026#34; 15 zhTranslations \u0026#34;github.com/go-playground/validator/v10/translations/zh\u0026#34; 16 17 \u0026#34;github.com/gin-gonic/gin\u0026#34; 18) 19 20// 定义一个全局翻译器T 21var trans ut.Translator 22 23//Login登录业务，字段添加tag约束条件 24type Login struct { 25 User string `json:\u0026#34;user\u0026#34; binding:\u0026#34;required\u0026#34;` //必填 26 Password string `json:\u0026#34;password\u0026#34; binding:\u0026#34;required\u0026#34;` //必填 27} 28 29//SignUp注册业务，字段添加tag约束条件 30type SignUp struct { 31 Age int `json:\u0026#34;age\u0026#34; binding:\u0026#34;gte=18\u0026#34;` //gte大于等于 32 Name string `json:\u0026#34;name\u0026#34; binding:\u0026#34;required\u0026#34;` //必填 33 Email string `json:\u0026#34;email\u0026#34; binding:\u0026#34;required,email\u0026#34;` //必填邮件 34 Password string `json:\u0026#34;password\u0026#34; binding:\u0026#34;required\u0026#34;` //必填 35 RePassword string `json:\u0026#34;re_password\u0026#34; binding:\u0026#34;required,eqfield=Password\u0026#34;` //RePassword和Password值一致 36} 37 38//RemoveTopStruct去除以\u0026#34;.\u0026#34;及其左部分内容 39func RemoveTopStruct(fields map[string]string) map[string]string { 40 res := map[string]string{} 41 for field, value := range fields { 42 res[field[strings.Index(field, \u0026#34;.\u0026#34;)+1:]] = value 43 } 44 return res 45} 46 47// InitTrans 初始化翻译器 48func InitTrans(locale string) (err error) { 49 // 修改gin框架中的Validator引擎属性，实现自定制 50 if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 51 //注册一个获取json的自定义方法 52 v.RegisterTagNameFunc(func(field reflect.StructField) string { 53 name := strings.SplitN(field.Tag.Get(\u0026#34;json\u0026#34;), \u0026#34;,\u0026#34;, 2)[0] 54 if name == \u0026#34;-\u0026#34; { 55 return \u0026#34;\u0026#34; 56 } 57 return name 58 }) 59 zhT := zh.New() // 中文翻译器 60 enT := en.New() // 英文翻译器 61 62 // 第一个参数是备用（fallback）的语言环境 63 // 后面的参数是应该支持的语言环境（支持多个） 64 // uni := ut.New(zhT, zhT) 也是可以的 65 uni := ut.New(enT, zhT, enT) 66 67 // locale 通常取决于 http 请求头的 \u0026#39;Accept-Language\u0026#39; 68 var ok bool 69 // 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找 70 trans, ok = uni.GetTranslator(locale) 71 if !ok { 72 return fmt.Errorf(\u0026#34;uni.GetTranslator(%s) failed\u0026#34;, locale) 73 } 74 75 // 注册翻译器 76 switch locale { 77 case \u0026#34;en\u0026#34;: 78 err = enTranslations.RegisterDefaultTranslations(v, trans) 79 case \u0026#34;zh\u0026#34;: 80 err = zhTranslations.RegisterDefaultTranslations(v, trans) 81 default: 82 err = enTranslations.RegisterDefaultTranslations(v, trans) 83 } 84 return 85 } 86 return 87} 88 89func main() { 90 res := map[string]string{ 91 \u0026#34;ice_moss.habbit\u0026#34;: \u0026#34;打球\u0026#34;, 92 \u0026#34;ice_moss.from\u0026#34;: \u0026#34;贵州 中国\u0026#34;, 93 } 94 fmt.Println(RemoveTopStruct(res)) 95 96 //初始化翻译器, 翻译器代码看不懂不要紧，我们只需知道这样使用就行 97 if err := InitTrans(\u0026#34;zh\u0026#34;); err != nil { 98 fmt.Println(\u0026#34;初始化翻译器失败\u0026#34;, err) 99 return 100 } 101 102 router := gin.Default() 103 router.POST(\u0026#34;/loginJSON\u0026#34;, func(c *gin.Context) { 104 var login Login 105 if err := c.ShouldBind(\u0026amp;login); err != nil { 106 fmt.Println(err.Error()) 107 errs, ok := err.(validator.ValidationErrors) 108 if !ok { 109 c.JSON(http.StatusOK, gin.H{ 110 \u0026#34;msg\u0026#34;: err.Error(), 111 }) 112 } 113 c.JSON(http.StatusInternalServerError, gin.H{ 114 \u0026#34;error\u0026#34;: errs.Translate(trans), 115 }) 116 return 117 } 118 c.JSON(http.StatusOK, gin.H{ 119 \u0026#34;msg\u0026#34;: \u0026#34;验证通过\u0026#34;, 120 }) 121 }) 122 123 router.POST(\u0026#34;/signupJSON\u0026#34;, func(c *gin.Context) { 124 var signup SignUp 125 //ShouldBind()对数据进行绑定，解组 126 if err := c.ShouldBind(\u0026amp;signup); err != nil { 127 fmt.Println(err.Error()) 128 //获取validator.ValidationErrors类型的error 129 errs, ok := err.(validator.ValidationErrors) 130 if !ok { 131 c.JSON(http.StatusOK, gin.H{ 132 \u0026#34;msg\u0026#34;: err.Error(), 133 }) 134 } 135 //validator.ValidationErrors类型错误则进行翻译 136 c.JSON(http.StatusInternalServerError, gin.H{ 137 \u0026#34;error\u0026#34;: RemoveTopStruct(errs.Translate(trans)), 138 }) 139 return 140 } 141 142 c.JSON(http.StatusOK, gin.H{ 143 \u0026#34;msg\u0026#34;: \u0026#34;注册成功\u0026#34;, 144 }) 145 }) 146 router.Run(\u0026#34;:8083\u0026#34;) 147} 我们 POST 访问：localhost:8083/signupJSON：\n如果参数不满足 tag 中的条件，则会返回如下结果 当我们输入如下满足 tag 中的条件 1{ 2 \u0026#34;age\u0026#34;:20, 3 \u0026#34;name\u0026#34;:\u0026#34;ifantic\u0026#34;, 4 \u0026#34;email\u0026#34;:\u0026#34;uasm@linux.do\u0026#34;, 5 \u0026#34;password\u0026#34;:\u0026#34;123456\u0026#34;, 6 \u0026#34;re_password\u0026#34;:\u0026#34;123456\u0026#34; 7} 结构体标签验证 代码定义了两个带有验证规则的结构体，使用结构体标签设置规则： 1type Login struct { 2 User string `json:\u0026#34;user\u0026#34; binding:\u0026#34;required\u0026#34;` //必填 3 Password string `json:\u0026#34;password\u0026#34; binding:\u0026#34;required\u0026#34;` //必填 4} 5 6type SignUp struct { 7 Age int `json:\u0026#34;age\u0026#34; binding:\u0026#34;gte=18\u0026#34;` //gte大于等于 8 Name string `json:\u0026#34;name\u0026#34; binding:\u0026#34;required\u0026#34;` //必填 9 Email string `json:\u0026#34;email\u0026#34; binding:\u0026#34;required,email\u0026#34;` //必填邮件 10 Password string `json:\u0026#34;password\u0026#34; binding:\u0026#34;required\u0026#34;` //必填 11 RePassword string `json:\u0026#34;re_password\u0026#34; binding:\u0026#34;required,eqfield=Password\u0026#34;` //RePassword和Password值一致 12} 这些标签定义了如下验证规则：\nrequired：字段不能为空 gte=18：值必须大于等于18 email：必须是有效的邮箱格式 eqfield=Password：必须与Password字段的值相等 翻译器设置 InitTrans 函数初始化了验证错误消息的翻译 1func InitTrans(locale string) (err error) { 2 // ... 3 zhT := zh.New() // 中文翻译器 4 enT := en.New() // 英文翻译器 5 uni := ut.New(enT, zhT, enT) 6 // ... 7} 这允许验证错误消息以不同语言显示（本例中为中文或英文）。\n错误消息中的自定义JSON字段名 代码注册了一个自定义函数，用于在错误消息中使用JSON字段名 1v.RegisterTagNameFunc(func(field reflect.StructField) string { 2 name := strings.SplitN(field.Tag.Get(\u0026#34;json\u0026#34;), \u0026#34;,\u0026#34;, 2)[0] 3 if name == \u0026#34;-\u0026#34; { 4 return \u0026#34;\u0026#34; 5 } 6 return name 7}) 错误消息清理 RemoveTopStruct 函数从错误消息中删除结构体名称前缀 1func RemoveTopStruct(fields map[string]string) map[string]string { 2 res := map[string]string{} 3 for field, value := range fields { 4 res[field[strings.Index(field, \u0026#34;.\u0026#34;)+1:]] = value 5 } 6 return res 7} 例如，它将错误消息中的SignUp.name转换为仅name。\n请求处理流程 定义了两个端点 1router.POST(\u0026#34;/loginJSON\u0026#34;, func(c *gin.Context) { 2 // 登录验证逻辑 3}) 4 5router.POST(\u0026#34;/signupJSON\u0026#34;, func(c *gin.Context) { 6 // 注册验证逻辑 7}) 验证工作流程：\n使用c.ShouldBind(\u0026amp;struct)将传入的JSON绑定到结构体 检查验证错误 将错误转换为适当的类型（validator.ValidationErrors） 使用配置的翻译器翻译错误 将干净、翻译好的错误消息返回给客户端 错误响应示例 对于无效输入，API返回翻译后的验证错误 1{ 2 \u0026#34;error\u0026#34;: { 3 \u0026#34;age\u0026#34;: \u0026#34;Age必须大于等于18\u0026#34;, 4 \u0026#34;email\u0026#34;: \u0026#34;Email为必填字段\u0026#34;, 5 \u0026#34;re_password\u0026#34;: \u0026#34;Re_password必须等于Password\u0026#34; 6 } 7} 这些验证规则来自 github.com/go-playground/validator 包，这是一个专门用于Go语言结构体和字段验证的开源库。\nValidator 提供了大量内置的验证标签，可以直接在结构体标签（struct tags）中使用。\nValidator 内置验证标签 比较验证 eq: 等于 ne: 不等于 gt: 大于 gte: 大于等于 lt: 小于 lte: 小于等于 字段间比较 eqfield: 与另一个字段相等 nefield: 与另一个字段不相等 gtfield: 大于另一个字段 gtefield: 大于等于另一个字段 ltfield: 小于另一个字段 ltefield: 小于等于另一个字段 字符串验证 alpha: 仅包含字母 alphanum: 仅包含字母和数字 numeric: 仅包含数字字符 number: 有效数字值 email: 有效电子邮件格式 url: 有效URL格式 uri: 有效URI格式 contains=xyz: 包含指定文本 containsany: 包含任何指定字符 excludes: 不包含指定文本 startswith: 以指定文本开头 endswith: 以指定文本结尾 lowercase: 全部为小写字符 uppercase: 全部为大写字符 长度验证 len: 精确长度 min: 最小长度 max: 最大长度 格式验证 json: 有效的JSON jwt: 有效的JWT令牌 uuid: 有效的UUID uuid3/4/5: 特定版本的UUID base64: 有效的base64字符串 isbn/isbn10/isbn13: 有效的ISBN 网络验证 ip: 有效IP地址(v4或v6) ipv4: 有效IPv4地址 ipv6: 有效IPv6地址 cidr: 有效CIDR表示法 mac: 有效MAC地址 条件验证 required: 字段必须存在且不为空 required_if: 当另一个字段等于某值时必填 required_unless: 除非另一个字段等于某值否则必填 required_with: 当任一指定字段存在时必填 required_without: 当任一指定字段不存在时必填 其他验证 oneof: 值必须是指定的多个值之一 dive: 深入切片、数组或映射并验证元素 unique: 切片/数组中的元素必须唯一 datetime: 根据给定格式的有效日期时间字符串 latitude: 有效纬度坐标 longitude: 有效经度坐标 file: 有效文件路径 iscolor: 是有效的颜色 自定义验证 validator还支持注册自定义验证函数，可以通过RegisterValidation方法添加自己的验证规则。\n这些验证标签可以组合使用，例如 required,min=3,max=10 表示字段必填且长度在3到10之间。 Gin 中间件原理及自定义中间件 中间件系统会在 HTTP 请求到达路由处理器之前或生成响应后对其进行处理。\n在此之前我们先来看一下 Gin 实例化 server, 我们在之前是使用 router := gin.Default()，但其实我们是可以直接使用 router := gin.New(), 那么在之前是实例中我们为什么不使用 gin.New() 呢？\n别急，我们先来看看 gin.Default() 的源码：\n1// Default returns an Engine instance with the Logger and Recovery middleware already attached. 2func Default() *Engine { 3 debugPrintWARNINGDefault() 4 engine := New() 5 engine.Use(Logger(), Recovery()) 6 return engine 7} 我们可以看到 Default () 其实是对 gin.New() 做了一层封装，并且做了其他事情，这里的其他事情就有 “中间件”。\n即：engine.Use(Logger(), Recovery()) 创建了一个新的 Gin 引擎，并已附带两个内置中间件：\nLogger 中间件：输出请求日志 Recovery 中间件：从任何恐慌（panic）中恢复 (recovers) 并返回 500 错误响应 如果要使用 gin.New() 代替，那你获得的是没有附加任何中间件的干净引擎，在你想要完全控制要使用哪些中间件时很有用。\nGin 中间件原理 我们来看看：engine.Use ()：\n1func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { 2 engine.RouterGroup.Use(middleware...) 3 engine.rebuild404Handlers() 4 engine.rebuild405Handlers() 5 return engine 6} 入参是 HandlerFunc 类型，Use 方法接受一个或多个类型为 HandlerFunc 的中间件函数。那么我们接着往下看 HandlerFunc：\n1// HandlerFunc defines the handler used by gin middleware as return value. 2type HandlerFunc func(*Context) 其实 HandlerFunc 是 func(*Context) 类型，到这里中间件我们就可以自定义了。\n自定义中间件 我们定义一个监控服务运行时间，运行状态的中间件：\n1//自定义中间件，这里我们以函数调用的形式，对中间件进一步封装 2func MyTimeLogger() gin.HandlerFunc { 3 return func(c *gin.Context) { //真正的中间件类型 4 t := time.Now() 5 c.Set(\u0026#34;msg\u0026#34;, \u0026#34;This is a test of middleware\u0026#34;) 6 //它执行调用处理程序内链中的待处理处理程序 7 //让原本执行的逻辑继续执行 8 c.Next() 9 10 end := time.Since(t) 11 fmt.Printf(\u0026#34;耗时：%D\\n\u0026#34;, end.Seconds()) 12 status := c.Writer.Status() 13 fmt.Println(\u0026#34;状态监控:\u0026#34;, status) 14 } 15} 我们在 main 函数中：\n1func main() { 2 router := gin.Default() 3 router.Use(MyTimeLogger()) //这里使用函数调用 4 router.GET(\u0026#34;/ping\u0026#34;, func(c *gin.Context) { 5 c.JSON(http.StatusOK, gin.H{ 6 \u0026#34;msg\u0026#34;: \u0026#34;Pong\u0026#34;, 7 }) 8 }) 9 router.Run(\u0026#34;:8083\u0026#34;) 10} 访问 localhost:8083/ping {\u0026quot;msg\u0026quot;:\u0026quot;Pong\u0026quot;} 完整代码 1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;net/http\u0026#34; 6 \u0026#34;time\u0026#34; 7 8 \u0026#34;github.com/gin-gonic/gin\u0026#34; 9) 10 11// 自定义中间件，记录执行时间和状态 12func MyTimeLogger() gin.HandlerFunc { 13 return func(c *gin.Context) { 14 // 预处理：请求处理前的操作 15 t := time.Now() 16 c.Set(\u0026#34;msg\u0026#34;, \u0026#34;This is a test of middleware\u0026#34;) 17 18 // 将控制权传递给下一个中间件/处理程序 19 c.Next() 20 21 // 后处理：请求处理后的操作 22 duration := time.Since(t) 23 fmt.Printf(\u0026#34;请求耗时: %v 秒\\n\u0026#34;, duration.Seconds()) 24 status := c.Writer.Status() 25 fmt.Printf(\u0026#34;响应状态: %d\\n\u0026#34;, status) 26 } 27} 28 29func main() { 30 // 创建带有默认中间件的 Gin 路由器 31 router := gin.Default() 32 33 // 添加我们的自定义中间件 34 router.Use(MyTimeLogger()) 35 36 // 定义路由 37 router.GET(\u0026#34;/ping\u0026#34;, func(c *gin.Context) { 38 // 如果需要，获取中间件数据 39 msg, exists := c.Get(\u0026#34;msg\u0026#34;) 40 if exists { 41 fmt.Println(\u0026#34;来自中间件的消息:\u0026#34;, msg) 42 } 43 44 // 模拟一些工作 45 time.Sleep(200 * time.Millisecond) 46 47 // 返回响应 48 c.JSON(http.StatusOK, gin.H{ 49 \u0026#34;msg\u0026#34;: \u0026#34;Pong\u0026#34;, 50 }) 51 }) 52 53 // 启动服务器 54 fmt.Println(\u0026#34;服务器在 :8083 端口启动...\u0026#34;) 55 router.Run(\u0026#34;:8083\u0026#34;) 56} 中间件工作步骤详解 当您访问 localhost:8083/ping 时:\n请求首先通过 Gin 的默认中间件（Logger 和 Recovery）; 然后通过我们的自定义 MyTimeLogger 中间件，记录当前时间、向上下文添加消息、调用 c.Next()，暂停中间件并将控制权传递给后续处理程序； /ping 的路由处理程序执行并返回 JSON 响应; 控制权返回到我们的中间件，然后计算经过的时间，持续时间和响应状态 响应被发送到客户端：{\u0026quot;msg\u0026quot;:\u0026quot;Pong\u0026quot;}。 关键概念 中间件链：多个中间件可以一起使用，形成一个链，每个中间件按顺序处理请求。 c.Next()：这个函数至关重要 - 它暂停当前中间件，执行剩余的链，然后返回继续在当前中间件中执行。 上下文（Context）：Context 对象允许中间件相互之间以及与处理程序共享数据。 这种方法提供了一种干净的方式来将关注点（如日志记录、身份验证和错误处理）与主应用程序逻辑分开。\n中间件实际应用 中间件非常适合用来实现身份验证逻辑，因为它可以在请求到达业务处理逻辑之前进行拦截和验证。下面的例子展示了如何使用自定义中间件来验证请求头中的令牌(Token)，即基于中间件模拟登录：\n1//自定义中间件 2func TokenRequired() gin.HandlerFunc { 3 return func(c *gin.Context) { 4 var token string 5 //从请求头中获取数据 6 for k, v := range c.Request.Header { 7 if k == \u0026#34;X-Token\u0026#34; { 8 token = v[0] 9 } else { 10 fmt.Println(k, v) 11 } 12 } 13 fmt.Println(token) 14 if token != \u0026#34;ice_moss\u0026#34; { 15 c.JSON(http.StatusUnauthorized, gin.H{ 16 \u0026#34;msg\u0026#34;: \u0026#34;认证未通过\u0026#34;, 17 }) 18 19 //return在这里不会有被执行 20 c.Abort() //这里先不用理解，后面会讲解，这里先理解为return 21 } 22 //继续往下执行该执行的逻辑 23 c.Next() 24 } 25} 将中间件加入 gin 中：\n1func main() { 2 router := gin.Default() 3 //中间件 4 router.Use(TokenRequired()) 5 router.GET(\u0026#34;/ping\u0026#34;, func(c *gin.Context) { 6 c.JSON(http.StatusOK, gin.H{ 7 \u0026#34;msg\u0026#34;: \u0026#34;Pong\u0026#34;, 8 }) 9 }) 10 router.Run(\u0026#34;:8083\u0026#34;) 11} 我们在 postman 中进行请求：localhost:8083/ping，在 Headers 中增加字段：\nKey Value X-Token ice_moss 正确参数返回 1{ 2 \u0026#34;msg\u0026#34;: \u0026#34;Pong\u0026#34; 3} 不正确参数返回 1{ 2 \u0026#34;msg\u0026#34;: \u0026#34;认证未通过\u0026#34; 3} 这里我们围绕 c.Abort() 和 c.Next() 两个方法来对中间件原理进一步剖析。\nc.Abort() 在上面的例子中我们看到：\n1func TokenRequired() gin.HandlerFunc { 2 return func(c *gin.Context) { 3 var token string 4 for k, v := range c.Request.Header { 5 if k == \u0026#34;X-Token\u0026#34; { 6 token = v[0] 7 } else { 8 fmt.Println(k, v) 9 } 10 } 11 fmt.Println(token) 12 if token != \u0026#34;ice_moss\u0026#34; { 13 c.JSON(http.StatusUnauthorized, gin.H{ 14 \u0026#34;msg\u0026#34;: \u0026#34;认证未通过\u0026#34;, 15 }) 16 //return 不会被执行，需要使用c.Abort()来结束当前 17 c.Abort() 18 } 19 //继续执行该执行的逻辑 20 c.Next() 21 } 22} 为什么 return 不能直接返回，而是使用 c.Abort()？ 当我们启动服务后，Gin 会有一个类似于任务队列将所有配置的中间件和在注册处理方法压入队列中：\n在处理业务代码之前，会将所有注册路由中的中间件以队列的执行方式执行，比如上面我们：\n当我们在上面的例子中执行 return 时，他只是将当前函数返回，但是后面的方法仍然是按逻辑执行的，很显然这不是我们想要的结果，不满足验证条件的情况，应该将对此时的 client 终止服务。如果要终止服务就应该将图中的箭头跳过所有方法：\n这样整个服务才是真正的终止。 下面来看看 c.Abort() 的代码：\n1func (c *Context) Abort() { 2 c.index = abortIndex 3} 当代码执行到 Abort() 时，index 被赋值为 abortIndex。\nabortIndex 是什么？ const abortIndex int8 = math.MaxInt8 \u0026gt;\u0026gt; 1 可以看到，最后 index 指向任务末端，这就是 const abortIndex int8 = math.MaxInt8 \u0026gt;\u0026gt; 1 作用的效果。\nc.Next() 理解了 Abort()，Next() 自然就好理解了，我们来看看它的定义\n1func (c *Context) Next() { 2 c.index++ 3 for c.index \u0026lt; int8(len(c.handlers)) { 4 c.handlers[c.index](c) 5 c.index++ 6 } 7} 执行过程 Gin 返回 html 模板 我们使用 html 模板，将后端获取到的数据，直接填充至 html 中。我们先来编写一个 html (实例为 tmpl, 无影响)：\n1\u0026lt;!DOCTYPE html\u0026gt; 2\u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; 3\u0026lt;head\u0026gt; 4 \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; 5 \u0026lt;title\u0026gt;{{ .title }}\u0026lt;/title\u0026gt; 6\u0026lt;/head\u0026gt; 7\u0026lt;body\u0026gt; 8\u0026lt;h1\u0026gt;{{ .menu }}\u0026lt;/h1\u0026gt; 9\u0026lt;/body\u0026gt; 10\u0026lt;/html\u0026gt; 其中数据以 {{ .title }} 从 web 层填充进来。\n1ch11 2├─ main.go 3└─ templates 4 └─ index.tmpl 我们需要注意目录结构，程序的执行入口 main 需要和模板 templates 放置同一目录下，这样保证 main 能读取文件 html：\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 \u0026#34;github.com/gin-gonic/gin\u0026#34;) 6 7func main() { 8 router := gin.Default() 9 //读取文件 10 router.LoadHTMLFiles(\u0026#34;templates/index.tmpl\u0026#34;) 11 router.GET(\u0026#34;/index\u0026#34;, func(c *gin.Context) { 12 //写入数据， key必须要tmpl一致 13 c.HTML(http.StatusOK, \u0026#34;index.tmpl\u0026#34;, gin.H{ 14 \u0026#34;title\u0026#34;: \u0026#34;购物网\u0026#34;, 15 \u0026#34;menu\u0026#34;: \u0026#34;菜单栏\u0026#34;, 16 }) 17 }) 18 router.Run(\u0026#34;:8085\u0026#34;) 19} 访问 localhost:8085/index 当然 router.LoadHTMLFiles() 方法可以加载多个 html 文件：\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 \u0026#34;github.com/gin-gonic/gin\u0026#34;) 6 7func main() { 8 router := gin.Default() 9 10 //读取模板文件，按指定个读取 11 router.LoadHTMLFiles(\u0026#34;templates/index.tmpl\u0026#34;, \u0026#34;templates/goods.html\u0026#34;) 12 router.GET(\u0026#34;/index\u0026#34;, func(c *gin.Context) { 13 c.HTML(http.StatusOK, \u0026#34;index.tmpl\u0026#34;, gin.H{ 14 \u0026#34;title\u0026#34;: \u0026#34;shop\u0026#34;, 15 \u0026#34;menu\u0026#34;: \u0026#34;菜单栏\u0026#34;, 16 }) 17 }) 18 19 router.GET(\u0026#34;goods\u0026#34;, func(c *gin.Context) { 20 c.HTML(http.StatusOK, \u0026#34;goods.html\u0026#34;, gin.H{ 21 \u0026#34;title\u0026#34;: \u0026#34;goods\u0026#34;, 22 \u0026#34;goods\u0026#34;: [4]string{\u0026#34;矿泉水\u0026#34;, \u0026#34;面包\u0026#34;, \u0026#34;薯片\u0026#34;, \u0026#34;冰淇淋\u0026#34;}, 23 }) 24 }) 25 router.Run(\u0026#34;:8085\u0026#34;) 26} 这样就可以访问：localhost:8085/goods 或者 localhost:8085/index 了。返回结果略\n当然如果 html 文件很多，Gin 还提供了 ：\n1func (engine *Engine) LoadHTMLGlob(pattern string) {……} 我们只需要这样调用：\n1//将\u0026#34;templates文件夹下所有文件加载 2router.LoadHTMLGlob(\u0026#34;templates/*\u0026#34;) 对应二级目录，我们又是如何处理的呢？\n1//加载templates目录下的目录中的所有文件 2router.LoadHTMLGlob(\u0026#34;templates/**/*\u0026#34;) 实例十六：\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 \u0026#34;github.com/gin-gonic/gin\u0026#34;) 6 7func main() { 8 router := gin.Default() 9 router.LoadHTMLGlob(\u0026#34;templates/**/*\u0026#34;) 10 router.GET(\u0026#34;user/list\u0026#34;, func(c *gin.Context) { 11 c.HTML(http.StatusOK, \u0026#34;list.html\u0026#34;, gin.H{ 12 \u0026#34;title\u0026#34;: \u0026#34;shop\u0026#34;, 13 \u0026#34;list\u0026#34;: \u0026#34;用户列表\u0026#34;, 14 }) 15 }) 16 17 router.GET(\u0026#34;goods/list\u0026#34;, func(c *gin.Context) { 18 c.HTML(http.StatusOK, \u0026#34;list.html\u0026#34;, gin.H{ 19 \u0026#34;title\u0026#34;: \u0026#34;shop\u0026#34;, 20 \u0026#34;list\u0026#34;: \u0026#34;商品列表\u0026#34;, 21 }) 22 }) 23 router.Run(\u0026#34;:8085\u0026#34;) 24} 与上节相似，创建 list.html：\n1\u0026lt;!DOCTYPE html\u0026gt; 2\u0026lt;html\u0026gt; 3\u0026lt;head\u0026gt; 4 \u0026lt;title\u0026gt;{{ .title }}\u0026lt;/title\u0026gt; 5\u0026lt;/head\u0026gt; 6\u0026lt;body\u0026gt; 7 \u0026lt;h1\u0026gt;{{ .list }}\u0026lt;/h1\u0026gt; 8\u0026lt;/body\u0026gt; 9\u0026lt;/html\u0026gt; 这样我们访问：localhost:8085/goods/list 或者 localhost:8085/user/list 都能访问到。\nGin 静态文件的挂载 在 web 开发中经常需要将 js 文件和 css 文件，进行挂载，来满足需求。目录结构：\n1ch11 2├─ main.go 3├─ static 4│ └─ style.css 5└─ templates 6 └─ user 7 └─ list.html html 文件 1\u0026lt;!DOCTYPE html\u0026gt; 2\u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; 3\u0026lt;head\u0026gt; 4 \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; 5 \u0026lt;title\u0026gt;{{ .title }}\u0026lt;/title\u0026gt; 6 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/static/style.css\u0026#34;\u0026gt; 7\u0026lt;/head\u0026gt; 8\u0026lt;body\u0026gt; 9\u0026lt;h1\u0026gt;{{ .list }}\u0026lt;/h1\u0026gt; 10\u0026lt;/body\u0026gt; 11\u0026lt;/html\u0026gt; css 文件 1*{ 2 background-color: aquamarine; 3} 静态文件挂载方法：router.Static(\u0026quot;/static\u0026quot;, \u0026quot;./static\u0026quot;)：\n该方法会去在 html 文件中 \u0026lt;link\u0026gt; 标签中找到以 static 开头的链接，然后去找在当前 main 所在的目录下找到以第二个参数 ./static 名称的目录下找到静态文件，然后挂载。\n实例十七：\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 \u0026#34;github.com/gin-gonic/gin\u0026#34;) 6 7func main() { 8 router := gin.Default() 9 //挂载静态文件 10 router.Static(\u0026#34;/static\u0026#34;, \u0026#34;./static\u0026#34;) 11 router.LoadHTMLGlob(\u0026#34;templates/**/*\u0026#34;) 12 router.GET(\u0026#34;user/list\u0026#34;, func(c *gin.Context) { 13 c.HTML(http.StatusOK, \u0026#34;list.html\u0026#34;, gin.H{ 14 \u0026#34;title\u0026#34;: \u0026#34;shop\u0026#34;, 15 \u0026#34;list\u0026#34;: \u0026#34;用户列表\u0026#34;, 16 }) 17 }) 18 19 router.Run(\u0026#34;:8085\u0026#34;) 20} 访问 localhost:8085/user/list Gin 优雅退出 在业务中，我们很多时候涉及到服务的退出，如：各种订单处理中，用户突然退出，支付费用时，程序突然退出，这里我们是需要是我们的服务合理的退出，进而不造成业务上的矛盾。实例十八：\n1package main 2 3import ( 4 \u0026#34;net/http\u0026#34; 5 \u0026#34;github.com/gin-gonic/gin\u0026#34;) 6 7func main() { 8 router := gin.Default() 9 router.GET(\u0026#34;ping\u0026#34;, func(c *gin.Context) { 10 c.JSON(http.StatusOK, gin.H{ 11 \u0026#34;msg\u0026#34;: \u0026#34;ping\u0026#34;, 12 }) 13 }) 14 15 go func() { 16 router.Run(\u0026#34;:8085\u0026#34;) 17 }() 18 19 qiut := make(chan os.Signal) 20 //接收control+c 21 //当接收到退出指令时，我们向chan收数据 22 signal.Notify(qiut, syscall.SIGINT, syscall.SIGTERM) 23 \u0026lt;-qiut 24 25 //服务退出前做处理 26 fmt.Println(\u0026#34;服务退出中\u0026#34;) 27 fmt.Println(\u0026#34;服务已退出\u0026#34;) 28} 在 terminal 中运行：go run main.go。服务启动后在 terminal 中退出 (control+c) 就可以看到:\n1[GIN-debug] Listening and serving HTTP on :8085 2服务退出中 3服务已退出 小记 累 ~\n","date":"2025-04-29T10:00:20+08:00","image":"https://langminjeii.ifantic.de/post/go-2.png","permalink":"https://langminjeii.ifantic.de/post/go-%E8%90%8C%E6%96%B0%E7%9A%84%E5%88%9D%E5%AD%A6%E4%B9%8B%E8%B7%AF-v/","title":"Go 萌新的初学之路 V"},{"content":"Go 协程 Go 程（goroutine）是由 Go 运行时管理的轻量级线程：go f(x, y, z)，会启动一个新的 Go 协程并执行 f(x, y, z)。\nf, x, y 和 z 的求值发生在当前的 Go 协程中，而 f 的执行发生在新的 Go 协程中。\nGo 程在相同的地址空间中运行，因此在访问共享的内存时必须进行同步。sync 包提供了这种能力，不过在 Go 中并不经常用到，因为还有其它的办法（见下一页）。\n1package main 2 3import ( 4\t\u0026#34;fmt\u0026#34; 5\t\u0026#34;time\u0026#34; 6) 7 8func say(s string) { 9\tfor i := 0; i \u0026lt; 5; i++ { 10\ttime.Sleep(100 * time.Millisecond) 11\tfmt.Println(s) 12\t} 13} 14 15func main() { 16\tgo say(\u0026#34;world\u0026#34;) 17\tsay(\u0026#34;hello\u0026#34;) 18} 输出 hello\nworld\nworld\nhello\nhello\nworld\nworld\nhello\nhello 主程序完成 say(\u0026quot;hello\u0026quot;)（打印 5 次后）后，程序退出。 此时，say(\u0026quot;world\u0026quot;) 例行程序只来得及打印 4 次。\n信道 信道（Channel）是带有类型的（通信）管道，你可以通过它用信道操作符 \u0026lt;- 来发送或者接收值，“箭头”就是数据流的方向，允许数据在不同的 Go 协程（goroutine）之间流动。信道是 Go 语言并发编程的核心工具之一，使得不同的并发任务能够安全地交换数据。\n1ch \u0026lt;- v // 将 v 发送至信道 ch。 2v := \u0026lt;-ch // 从 ch 接收值并赋予 v。 和映射与切片一样，信道在使用前必须创建：ch := make(chan int)。默认情况下，发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。以下示例对切片中的数进行求和，将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算，它就能算出最终的结果。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func sum(s []int, c chan int) { 6\tsum := 0 7\tfor _, v := range s { 8\tsum += v 9\t} 10\tc \u0026lt;- sum // 发送 sum 到 c 11} 12 13func main() { 14\ts := []int{7, 2, 8, -9, 4, 0} 15 16\tc := make(chan int) 17\tgo sum(s[:len(s)/2], c) 18\tgo sum(s[len(s)/2:], c) 19\tx, y := \u0026lt;-c, \u0026lt;-c // 从 c 接收 20 21\tfmt.Println(x, y, x+y) 22} 输出 -5 17 12 有一组数字 {7, 2, 8, -9, 4, 0}，将它分成两半。第一个计算 {7, 2, 8} 的和，第二个计算 {-9, 4, 0} 的和； 创建一个信道 c； 两个通道（用 go 关键字启动）同时独立工作，计算各自负责部分的总和； main 函数在 x, y := \u0026lt;-c, \u0026lt;-c 处等待，直到信道 c 传回各自的结果。 带缓冲的信道 信道可以是带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道。\nch := make(chan int, 100) 创建了一个可以存储100个整数的缓冲区。\n仅当信道的缓冲区填满后，向其发送数据时才会阻塞。当缓冲区为空时，接受方会阻塞。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6\tch := make(chan int, 2) 7\tch \u0026lt;- 1 8\tch \u0026lt;- 2 9\tfmt.Println(\u0026lt;-ch) 10\tfmt.Println(\u0026lt;-ch) 11} 信道有2个空位，所以前两次发送操作都不会阻塞 如果尝试发送第三个值 ch \u0026lt;- 3（当缓冲区已满），程序会阻塞直到有人从信道接收值 当缓冲区有值时，接收操作不会阻塞 如果缓冲区为空时尝试接收，程序会阻塞直到有新值发送到信道 带缓冲的信道提供了一种异步通信机制，让发送方和接收方不必同时准备好，提高了程序的并发效率。\nrange 和 close 发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭：若没有值可以接收且信道已被关闭，那么在执行完 v, ok := \u0026lt;-ch，此时 ok 会被设置为 false。\n当从 channel 接收数据时，可以使用两个返回值的形式来检查 channel 是否已关闭：\n1v, ok := \u0026lt;-ch 如果 ok 是 true：成功接收到值，v 是接收到的值 如果 ok 是 false：channel 已关闭且没有更多值可接收，v 是该类型的零值 信息 只应由发送者关闭信道，而不应油接收者关闭。向一个已经关闭的信道发送数据会引发程序 panic。 信息 信道与文件不同，通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭，例如终止一个 range 循环。 1package main 2 3import ( 4\t\u0026#34;fmt\u0026#34; 5) 6 7func fibonacci(n int, c chan int) { 8 x, y := 0, 1 9 for i := 0; i \u0026lt; n; i++ { 10 c \u0026lt;- x // 将斐波那契数列的值发送到 channel 11 x, y = y, x+y // 计算下一个斐波那契数 12 } 13 close(c) // 发送完毕后关闭 channel 14} 15 16func main() { 17 c := make(chan int, 10) // 创建一个容量为 10 的缓冲 channel 18 `Go` fibonacci(cap(c), c) // 启动一个协程，生成斐波那契数并发送到 channel 19 for i := range c { // 不断从 channel 接收值，直到它被关闭 20 fmt.Println(i) 21 } 22} 输出 0\n1\n1\n2\n3\n5\n8\n13\n21\n34 创建一个缓冲容量为 10 的 channel 启动一个新的 goroutine 来计算斐波那契数列 fibonacci 函数向 channel 中发送 10 个斐波那契数 发送完后，fibonacci 函数关闭 channel main 函数中的 range 循环接收 channel 中的所有值并打印 当 channel 关闭且没有更多值时，range 循环结束 range 与 channel for i := range c 循环提供了一种更简洁的方式来不断从 channel 接收值，直到它被关闭：\n1for i := range c { 2 fmt.Println(i) 3} 这相当于：\n1for { 2 i, ok := \u0026lt;-c 3 if !ok { 4 break // channel 已关闭，退出循环 5 } 6 fmt.Println(i) 7} select 语句 select 语句使一个 Go 程可以等待多个通信操作。它会阻塞到某个分支可以继续执行为止，这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func fibonacci(c, quit chan int) { 6\tx, y := 0, 1 7\tfor { 8\tselect { 9\tcase c \u0026lt;- x: 10\tx, y = y, x+y 11\tcase \u0026lt;-quit: 12\tfmt.Println(\u0026#34;quit\u0026#34;) 13\treturn 14\t} 15\t} 16} 17 18func main() { 19\tc := make(chan int) 20\tquit := make(chan int) 21\tgo func() { 22\tfor i := 0; i \u0026lt; 10; i++ { 23\tfmt.Println(\u0026lt;-c) 24\t} 25\tquit \u0026lt;- 0 26\t}() 27\tfibonacci(c, quit) 28} 输出 0\n1\n1\n2\n3\n5\n8\n13\n21\n34\nquit 有两个通道：c（用于发送斐波那契数）和 quit（用于结束程序） 无限循环中的 select 语句在等待两种可能的操作： 发送操作：将当前的斐波那契数发送到 c 通道 接收操作：从 quit 通道接收退出信号 1func main() { 2 c := make(chan int) 3 quit := make(chan int) 4 `Go` func() { 5 for i := 0; i \u0026lt; 10; i++ { 6 fmt.Println(\u0026lt;-c) 7 } 8 quit \u0026lt;- 0 9 }() 10 fibonacci(c, quit) 11} 主函数中：\n创建了两个通道：c 和 quit； 启动了一个匿名 goroutine，接收并打印 10 个斐波那契数，发送退出信号到 quit 通道； 调用 fibonacci 函数。 select 的特殊情况 多个通道就绪：如果多个 case 同时准备好，select 会随机选择一个执行 没有通道就绪：select 会阻塞，直到某个通道可以操作。 default 分支：如果包含 default 分支，当没有其他通道操作可以进行时会执行 default，使 select 变成非阻塞的。 默认选择 当 select 中的其它分支都没有准备好时，default 分支就会执行。为了在尝试发送或者接收时不发生阻塞，可使用 default 分支：\n1select { 2 case i := \u0026lt;-c: 3 // 使用 i 4 default: 5 // 从 c 中接收会阻塞时执行 6} 1package main 2 3import ( 4\t\u0026#34;fmt\u0026#34; 5\t\u0026#34;time\u0026#34; 6) 7 8func main() { 9\ttick := time.Tick(100 * time.Millisecond) 10\tboom := time.After(500 * time.Millisecond) 11\tfor { 12\tselect { 13\tcase \u0026lt;-tick: 14\tfmt.Println(\u0026#34;tick.\u0026#34;) 15\tcase \u0026lt;-boom: 16\tfmt.Println(\u0026#34;BOOM!\u0026#34;) 17\treturn 18\tdefault: 19\tfmt.Println(\u0026#34; .\u0026#34;) 20\ttime.Sleep(50 * time.Millisecond) 21\t} 22\t} 23} 输出 1 . 2 . 3tick. 4 . 5 . 6tick. 7 . 8 . 9tick. 10 . 11 . 12tick. 13 . 14 . 15BOOM! 在 Go 语言中，select 是一种处理多个通道(channel)操作的机制。它类似于 switch 语句，但专门用于通道操作。select 语句会监听多个通道，并在某个通道准备好时执行相应的代码块。\ndefault 分支的核心作用是：当 select 中的其他条件分支都没有准备好时，default 分支会被执行。这是实现非阻塞操作的关键。\n如果没有 default：select 会一直等待，直到某个通道可以操作 有 default：如果所有通道都不能立即操作，就执行 default 分支代码 1func main() { 2 tick := time.Tick(100 * time.Millisecond) 3 boom := time.After(500 * time.Millisecond) 4 for { 5 select { 6 case \u0026lt;-tick: 7 fmt.Println(\u0026#34;tick.\u0026#34;) 8 case \u0026lt;-boom: 9 fmt.Println(\u0026#34;BOOM!\u0026#34;) 10 return 11 default: 12 fmt.Println(\u0026#34; .\u0026#34;) 13 time.Sleep(50 * time.Millisecond) 14 } 15 } 16} 这个例子生动展示了 select 和 default 的使用场景：\ntick 通道每 100 毫秒产生一个信号 boom 通道在 500 毫秒后产生一个信号 程序在无限循环中： 如果 tick 通道有数据，打印 \u0026ldquo;tick.\u0026rdquo; 如果 boom 通道有数据，打印 \u0026ldquo;BOOM!\u0026rdquo; 并结束程序 如果两个通道都没准备好，执行 default：打印 \u0026quot; .\u0026quot; 并等待 50 毫秒 程序运行效果大致是：\n刚开始，两个通道都没数据，所以执行 default（约每 50ms 打印一个点） 每 100ms，tick 通道会准备好，此时打印 \u0026ldquo;tick.\u0026rdquo; 500ms 后，boom 通道准备好，打印 \u0026ldquo;BOOM!\u0026rdquo; 并退出 select 的一个重要特点是：整个 select 代码段只执行一次，如果没有可用的通道操作，就会执行 default 分支。这里因为有 for 循环，所以 select 会被反复执行，使我们能看到不同时间点的行为。\n练习：等价二叉查找树 不同二叉树的叶节点上可以保存相同的值序列。例如，以下两个二叉树都保存了序列 1，1，2，3，5，8，13。\n在大多数语言中，检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go 的并发和信道来编写一个简单的解法。\n本例使用了 tree 包，它定义了类型：\n1type Tree struct { 2 Left *Tree 3 Value int 4 Right *Tree 5} 1. 实现 Walk 函数。\n2. 测试 Walk 函数。\n函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树，它保存了值 k, 2k, 3k, \u0026hellip;, 10k。\n创建一个新的信道 ch 并且对其进行步进：go Walk(tree.New(1), ch)；然后从信道中读取并打印 10 个值。应当是数字 1, 2, 3, \u0026hellip;, 10.\n3. 用 Walk 实现 Same 函数来检测 t1 和 t2 是否存储了相同的值。\n4. 测试 Same 函数。\nSame(tree.New(1), tree.New(1)) 应当返回 true，而 Same(tree.New(1), tree.New(2)) 应当返回 false。\nTree 的文档可在这里找到。\nWalk 函数实现 Walk 函数需要遍历二叉树并将所有值发送到信道。我们可以使用中序遍历（左子树-节点-右子树）实现这一点：\n范例 1// Walk 遍历树 t，将树中所有的值发送到信道 ch 2func Walk(t *tree.Tree, ch chan int) { 3 // 定义一个递归的辅助函数进行树的遍历 4 var walkTree func(t *tree.Tree) 5 walkTree = func(t *tree.Tree) { 6 if t == nil { 7 return 8 } 9 // 先遍历左子树 10 walkTree(t.Left) 11 // 发送当前节点的值 12 ch \u0026lt;- t.Value 13 // 再遍历右子树 14 walkTree(t.Right) 15 } 16 17 walkTree(t) 18 close(ch) // 遍历结束后关闭信道 19} Same 函数实现 Same 函数用于检查两个二叉树是否包含相同的值序列：\n范例 1// Same 判断 t1 和 t2 是否包含相同的值 2func Same(t1, t2 *tree.Tree) bool { 3 ch1 := make(chan int) 4 ch2 := make(chan int) 5 6 `Go` Walk(t1, ch1) 7 `Go` Walk(t2, ch2) 8 9 // 比较两个树的遍历结果 10 for { 11 v1, ok1 := \u0026lt;-ch1 12 v2, ok2 := \u0026lt;-ch2 13 14 // 如果通道状态不同或值不同，则树不相等 15 if ok1 != ok2 || v1 != v2 { 16 return false 17 } 18 19 // 如果两个通道都已关闭，并且所有值都匹配，则树相等 20 if !ok1 { 21 break 22 } 23 } 24 25 return true 26} 主函数（测试） 范例 1func main() { 2 // 测试 Walk 函数 3 fmt.Println(\u0026#34;测试 Walk 函数:\u0026#34;) 4 ch := make(chan int) 5 `Go` Walk(tree.New(1), ch) 6 7 // 应该打印出 1 到 10 的数字 8 for i := 0; i \u0026lt; 10; i++ { 9 fmt.Printf(\u0026#34;%d \u0026#34;, \u0026lt;-ch) 10 } 11 fmt.Println() 12 13 // 测试 Same 函数 14 fmt.Println(\u0026#34;测试 Same 函数:\u0026#34;) 15 fmt.Println(\u0026#34;Same(tree.New(1), tree.New(1)):\u0026#34;, Same(tree.New(1), tree.New(1))) 16 fmt.Println(\u0026#34;Same(tree.New(1), tree.New(2)):\u0026#34;, Same(tree.New(1), tree.New(2))) 17} 完整代码 代码 1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;golang.org/x/tour/tree\u0026#34; 6) 7 8// Walk 遍历树 t，将树中所有的值发送到信道 ch 9func Walk(t *tree.Tree, ch chan int) { 10 var walkTree func(t *tree.Tree) 11 walkTree = func(t *tree.Tree) { 12 if t == nil { 13 return 14 } 15 walkTree(t.Left) 16 ch \u0026lt;- t.Value 17 walkTree(t.Right) 18 } 19 20 walkTree(t) 21 close(ch) 22} 23 24// Same 判断 t1 和 t2 是否包含相同的值 25func Same(t1, t2 *tree.Tree) bool { 26 ch1 := make(chan int) 27 ch2 := make(chan int) 28 29 `Go` Walk(t1, ch1) 30 `Go` Walk(t2, ch2) 31 32 for { 33 v1, ok1 := \u0026lt;-ch1 34 v2, ok2 := \u0026lt;-ch2 35 36 if ok1 != ok2 || v1 != v2 { 37 return false 38 } 39 40 if !ok1 { 41 break 42 } 43 } 44 45 return true 46} 47 48func main() { 49 // 测试 Walk 函数 50 fmt.Println(\u0026#34;测试 Walk 函数:\u0026#34;) 51 ch := make(chan int) 52 `Go` Walk(tree.New(1), ch) 53 54 for i := 0; i \u0026lt; 10; i++ { 55 fmt.Printf(\u0026#34;%d \u0026#34;, \u0026lt;-ch) 56 } 57 fmt.Println() 58 59 // 测试 Same 函数 60 fmt.Println(\u0026#34;测试 Same 函数:\u0026#34;) 61 fmt.Println(\u0026#34;Same(tree.New(1), tree.New(1)):\u0026#34;, Same(tree.New(1), tree.New(1))) 62 fmt.Println(\u0026#34;Same(tree.New(1), tree.New(2)):\u0026#34;, Same(tree.New(1), tree.New(2))) 63} 测试 Walk 函数:\n1 2 3 4 5 6 7 8 9 10\n测试 Same 函数:\nSame(tree.New(1), tree.New(1)): true\nSame(tree.New(1), tree.New(2)): false 注意：这个实现使用了中序遍历，确保了我们从二叉查找树中获取的值是按顺序排列的，这对于比较两棵树是否包含相同的值序列非常重要。\nsync.Mutex 我们已经看到信道非常适合在各个 Go 程间进行通信。但是如果我们并不需要通信呢？比如说，若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量，从而避免冲突？\n这里涉及的概念叫做 互斥（mutualexclusion）* ，我们通常使用 互斥锁（Mutex） 这一数据结构来提供这种机制。Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法：\nLock Unlock 我们可以通过在代码前调用 Lock 方法，在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。\n1package main 2 3import ( 4\t\u0026#34;fmt\u0026#34; 5\t\u0026#34;sync\u0026#34; 6\t\u0026#34;time\u0026#34; 7) 8 9// SafeCounter 是并发安全的 10type SafeCounter struct { 11\tmu sync.Mutex 12\tv map[string]int 13} 14 15// Inc 对给定键的计数加一 16func (c *SafeCounter) Inc(key string) { 17\tc.mu.Lock() 18\t// 锁定使得一次只有一个 `Go` 协程可以访问映射 c.v。 19\tc.v[key]++ 20\tc.mu.Unlock() 21} 22 23// Value 返回给定键的计数的当前值。 24func (c *SafeCounter) Value(key string) int { 25\tc.mu.Lock() 26\t// 锁定使得一次只有一个 `Go` 协程可以访问映射 c.v。 27\tdefer c.mu.Unlock() 28\treturn c.v[key] 29} 30 31func main() { 32\tc := SafeCounter{v: make(map[string]int)} 33\tfor i := 0; i \u0026lt; 1000; i++ { 34\tgo c.Inc(\u0026#34;somekey\u0026#34;) 35\t} 36 37\ttime.Sleep(time.Second) 38\tfmt.Println(c.Value(\u0026#34;somekey\u0026#34;)) 39} 输出 1000 SafeCounter 结构体：包含一个互斥锁和一个映射（Go 中的 map 不是并发安全的） Inc 方法： 在增加计数器前锁定互斥锁 操作完成后解锁 Value 方法： 在读取前锁定互斥锁 使用 defer 确保函数返回时互斥锁始终会被解锁 互斥锁（Mutex） 互斥锁（Mutex，\u0026ldquo;mutual exclusion\u0026quot;的缩写）是并发编程中的一种同步机制，用于防止多个 goroutine 同时访问共享资源。在 Go 语言中，sync.Mutex 是最常用的同步原语之一 为什么我们需要互斥锁？\nGo 的 goroutine 让并发编程变得简单——只需在函数调用前添加 go 关键字即可启动并发执行。然而，当多个 goroutine 访问相同数据时：\n没有保护：会发生数据竞争，导致不可预测的行为 使用互斥锁保护：goroutine 会轮流安全地访问共享资源 sync.Mutex 的工作原理 Go 中的互斥锁提供两个基本方法：\nLock()：获取独占访问权（如果已被锁定则阻塞） Unlock()：释放互斥锁的独占访问权就像是一把房间的钥匙——只有持有钥匙的 goroutine 才能进入，其他 goroutine 必须等待钥匙被归还。 main() 函数启动了 1000 个 goroutine，它们都试图增加同一个计数器。如果没有互斥锁，这会导致竞态条件。\n练习：Web 爬虫 在这个练习中，我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。修改 Crawl 函数来并行地抓取 URL，使爬虫能够并行地爬取多个网页，同时确保不重复爬取同一个页面。\n提示： 你可以用一个 map 来缓存已经获取的 URL，但是要注意 map 本身并不是并发安全的！\n1package main 2 3import ( 4\t\u0026#34;fmt\u0026#34; 5) 6 7type Fetcher interface { 8\t// Fetch 返回 URL 所指向页面的 body 内容， 9\t// 并将该页面上找到的所有 URL 放到一个切片中。 10\tFetch(url string) (body string, urls []string, err error) 11} 12 13// Crawl 用 fetcher 从某个 URL 开始递归的爬取页面，直到达到最大深度。 14func Crawl(url string, depth int, fetcher Fetcher) { 15\t// TODO: 并行地爬取 URL。 16\t// TODO: 不重复爬取页面。 17\t// 下面并没有实现上面两种情况： 18\tif depth \u0026lt;= 0 { 19\treturn 20\t} 21\tbody, urls, err := fetcher.Fetch(url) 22\tif err != nil { 23\tfmt.Println(err) 24\treturn 25\t} 26\tfmt.Printf(\u0026#34;found: %s %q\\n\u0026#34;, url, body) 27\tfor _, u := range urls { 28\tCrawl(u, depth-1, fetcher) 29\t} 30\treturn 31} 32 33func main() { 34\tCrawl(\u0026#34;https://golang.org/\u0026#34;, 4, fetcher) 35} 36 37// fakeFetcher 是待填充结果的 Fetcher。 38type fakeFetcher map[string]*fakeResult 39 40type fakeResult struct { 41\tbody string 42\turls []string 43} 44 45func (f fakeFetcher) Fetch(url string) (string, []string, error) { 46\tif res, ok := f[url]; ok { 47\treturn res.body, res.urls, nil 48\t} 49\treturn \u0026#34;\u0026#34;, nil, fmt.Errorf(\u0026#34;not found: %s\u0026#34;, url) 50} 51 52// fetcher 是填充后的 fakeFetcher。 53var fetcher = fakeFetcher{ 54\t\u0026#34;https://golang.org/\u0026#34;: \u0026amp;fakeResult{ 55\t\u0026#34;The `Go` Programming Language\u0026#34;, 56\t[]string{ 57\t\u0026#34;https://golang.org/pkg/\u0026#34;, 58\t\u0026#34;https://golang.org/cmd/\u0026#34;, 59\t}, 60\t}, 61\t\u0026#34;https://golang.org/pkg/\u0026#34;: \u0026amp;fakeResult{ 62\t\u0026#34;Packages\u0026#34;, 63\t[]string{ 64\t\u0026#34;https://golang.org/\u0026#34;, 65\t\u0026#34;https://golang.org/cmd/\u0026#34;, 66\t\u0026#34;https://golang.org/pkg/fmt/\u0026#34;, 67\t\u0026#34;https://golang.org/pkg/os/\u0026#34;, 68\t}, 69\t}, 70\t\u0026#34;https://golang.org/pkg/fmt/\u0026#34;: \u0026amp;fakeResult{ 71\t\u0026#34;Package fmt\u0026#34;, 72\t[]string{ 73\t\u0026#34;https://golang.org/\u0026#34;, 74\t\u0026#34;https://golang.org/pkg/\u0026#34;, 75\t}, 76\t}, 77\t\u0026#34;https://golang.org/pkg/os/\u0026#34;: \u0026amp;fakeResult{ 78\t\u0026#34;Package os\u0026#34;, 79\t[]string{ 80\t\u0026#34;https://golang.org/\u0026#34;, 81\t\u0026#34;https://golang.org/pkg/\u0026#34;, 82\t}, 83\t}, 84} 思路：\n使用 goroutines 实现并行爬取：为每个URL创建一个goroutine来并行爬取 使用 map 跟踪已访问的URL：创建一个映射表来记录已经爬取过的URL 使用 mutex 保护共享数据：因为多个goroutine会同时访问映射表，需要使用互斥锁保护它 使用 WaitGroup 等待所有爬取完成：确保所有并行的爬取任务都完成后再结束程序 实现示例 Fetcher 接口：定义了如何获取网页内容的方法 1type Fetcher interface { 2 // 获取URL内容并返回页面上的所有链接 3 Fetch(url string) (body string, urls []string, err error) 4} Crawl 函数：目前是按顺序爬取网页的函数，需要使其并行工作 1func Crawl(url string, depth int, fetcher Fetcher) { 2 // 需要修改的部分 3} fakeFetcher：一个模拟的网页获取器，用于测试我们的爬虫程序 输出 1package main 2 3import ( 4\t\u0026#34;fmt\u0026#34; 5\t\u0026#34;sync\u0026#34; 6) 7 8type Fetcher interface { 9\tFetch(url string) (body string, urls []string, err error) 10} 11 12// SafeCache provides thread-safe access to the visited URLs map 13type SafeCache struct { 14\tvisited map[string]bool 15\tmux sync.Mutex 16} 17 18// CheckAndMark checks if URL was visited and marks it as visited 19func (c *SafeCache) CheckAndMark(url string) bool { 20\tc.mux.Lock() 21\tdefer c.mux.Unlock() 22\tif c.visited[url] { 23\treturn true // Already visited 24\t} 25\tc.visited[url] = true 26\treturn false // First time visiting 27} 28 29// Crawl uses fetcher to recursively crawl pages starting with url, to a maximum of depth. 30func Crawl(url string, depth int, fetcher Fetcher) { 31\t// Create a cache for tracking visited URLs 32\tcache := \u0026amp;SafeCache{visited: make(map[string]bool)} 33\t34\t// WaitGroup to track all goroutines 35\tvar wg sync.WaitGroup 36\t37\t// Define crawl function to use in goroutines 38\tvar crawler func(string, int) 39\tcrawler = func(url string, depth int) { 40\tdefer wg.Done() 41\t42\t// Stop if we\u0026#39;ve reached max depth 43\tif depth \u0026lt;= 0 { 44\treturn 45\t} 46\t47\t// Check if we\u0026#39;ve already visited this URL 48\tif cache.CheckAndMark(url) { 49\treturn 50\t} 51\t52\tbody, urls, err := fetcher.Fetch(url) 53\tif err != nil { 54\tfmt.Println(err) 55\treturn 56\t} 57\tfmt.Printf(\u0026#34;found: %s %q\\n\u0026#34;, url, body) 58\t59\t// Create a goroutine for each URL 60\tfor _, u := range urls { 61\twg.Add(1) 62\tgo crawler(u, depth-1) 63\t} 64\t} 65\t66\t// Start the first crawl 67\twg.Add(1) 68\tgo crawler(url, depth) 69\t70\t// Wait for all crawling to complete 71\twg.Wait() 72} 73 74func main() { 75\tCrawl(\u0026#34;https://golang.org/\u0026#34;, 4, fetcher) 76} 77 78// fakeFetcher is Fetcher that returns canned results. 79type fakeFetcher map[string]*fakeResult 80 81type fakeResult struct { 82\tbody string 83\turls []string 84} 85 86func (f fakeFetcher) Fetch(url string) (string, []string, error) { 87\tif res, ok := f[url]; ok { 88\treturn res.body, res.urls, nil 89\t} 90\treturn \u0026#34;\u0026#34;, nil, fmt.Errorf(\u0026#34;not found: %s\u0026#34;, url) 91} 92 93// fetcher is a populated fakeFetcher. 94var fetcher = fakeFetcher{ 95\t\u0026#34;https://golang.org/\u0026#34;: \u0026amp;fakeResult{ 96\t\u0026#34;The Go Programming Language\u0026#34;, 97\t[]string{ 98\t\u0026#34;https://golang.org/pkg/\u0026#34;, 99\t\u0026#34;https://golang.org/cmd/\u0026#34;, 100\t}, 101\t}, 102\t\u0026#34;https://golang.org/pkg/\u0026#34;: \u0026amp;fakeResult{ 103\t\u0026#34;Packages\u0026#34;, 104\t[]string{ 105\t\u0026#34;https://golang.org/\u0026#34;, 106\t\u0026#34;https://golang.org/cmd/\u0026#34;, 107\t\u0026#34;https://golang.org/pkg/fmt/\u0026#34;, 108\t\u0026#34;https://golang.org/pkg/os/\u0026#34;, 109\t}, 110\t}, 111\t\u0026#34;https://golang.org/pkg/fmt/\u0026#34;: \u0026amp;fakeResult{ 112\t\u0026#34;Package fmt\u0026#34;, 113\t[]string{ 114\t\u0026#34;https://golang.org/\u0026#34;, 115\t\u0026#34;https://golang.org/pkg/\u0026#34;, 116\t}, 117\t}, 118\t\u0026#34;https://golang.org/pkg/os/\u0026#34;: \u0026amp;fakeResult{ 119\t\u0026#34;Package os\u0026#34;, 120\t[]string{ 121\t\u0026#34;https://golang.org/\u0026#34;, 122\t\u0026#34;https://golang.org/pkg/\u0026#34;, 123\t}, 124\t}, 125} 1found: https://golang.org/ \u0026#34;The Go Programming Language\u0026#34; 2not found: https://golang.org/cmd/ 3found: https://golang.org/pkg/ \u0026#34;Packages\u0026#34; 4found: https://golang.org/pkg/os/ \u0026#34;Package os\u0026#34; 5found: https://golang.org/pkg/fmt/ \u0026#34;Package fmt\u0026#34; 小记 没有小记 进阶教程：\nbuild-web-application-with-golang\n《Go入门指南》\nGoWeb框架Gin学习总结 | Go 技术论坛\n","date":"2025-04-21T20:12:11+08:00","image":"https://langminjeii.ifantic.de/post/go-3.png","permalink":"https://langminjeii.ifantic.de/post/go-%E8%90%8C%E6%96%B0%E7%9A%84%E5%88%9D%E5%AD%A6%E4%B9%8B%E8%B7%AF-iv/","title":"Go 萌新的初学之路 IV"},{"content":"方法 在 Go 语言中，方法是一种特殊的函数，它与特定类型相关联。与其他面向对象语言不同，Go 没有类的概念，但允许你在任何自定义类型上定义方法。方法接收者在它自己的参数列表内，位于 func 关键字和方法名之间。在此例中，Abs 方法拥有一个名字为 v，类型为 Vertex 的接收者。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Vertex struct { 9 X, Y float64 10} 11 12func (v Vertex) Abs() float64 { 13 return math.Sqrt(v.X*v.X + v.Y*v.Y) 14} 15 16func main() { 17 v := Vertex{3, 4} 18 fmt.Println(v.Abs()) 19} 输出 5 什么是方法？ 在 Go 语言中，方法其实就是一种特殊的函数 ，只不过这个函数和某个特定的数据类型关联在一起了，可以把它想象成是\u0026quot;某种类型专属的函数\u0026quot;。\n接收者 就是那个拥有这个方法的具体对象 。你可以把它理解为：\n普通函数是独立的，任何人都可以调用 方法是\u0026quot;专属\u0026quot;的，只有特定类型的对象才能调用 实际例子 1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Circle struct { 9 radius float64 10} 11 12// 这是一个方法，属于Circle类型 13func (c Circle) Area() float64 { 14 return math.Pi * c.radius * c.radius 15} 16 17func main() { 18 myCircle := Circle{radius: 5} 19 20 // 调用方法 21 circleArea := myCircle.Area() 22 fmt.Println(\u0026#34;圆的面积是:\u0026#34;, circleArea) 23} 输出 圆的面积是: 78.53981633974483 在这个例子中：\n我们创建了一个 Circle 类型 给这个类型定义了一个计算面积的方法 Area (c Circle) 是接收者，表示这个方法只属于 Circle 类型 当我们创建一个圆 myCircle 后，可以用点号调用它的方法：myCircle.Area() 值接收者与指针接收者的区别 Go 语言中方法的接收者可以是值也可以是指针，这有点类似于：\n值接收者：你给朋友看照片，他只能看不能改 指针接收者：你把实物给朋友，他可以修改它 1// 值接收者 - 不会修改原始数据 2func (c Circle) Double() { 3 c.radius = c.radius * 2 // 这个修改不会影响到原始的Circle 4} 5 6// 指针接收者 - 会修改原始数据 7func (c *Circle) RealDouble() { 8 c.radius = c.radius * 2 // 这个修改会真正改变Circle的radius 9} 方法即函数 如上所说，方法只是个带接收者参数的函数。现在这个 Abs 的写法就是个正常的函数，功能并没有什么变化。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Vertex struct { 9 X, Y float64 10} 11 12func Abs(v Vertex) float64 { 13 return math.Sqrt(v.X*v.X + v.Y*v.Y) 14} 15 16func main() { 17 v := Vertex{3, 4} 18 fmt.Println(Abs(v)) 19} 输出 5 你也可以为非结构体类型声明方法。在下面的例子里，我们看到了一个带 Abs 方法的数值类型 MyFloat。\n你只能为在同一个包中定义的接收者类型声明方法，而不能为其它别的包中定义的类型 （包括 int 之类的内置类型）声明方法。\n就是说接收者的类型定义和方法声明必须在同一包内。因此下面的例子是创建了 MyFloat 类型，而不是直接为 float64 添加方法。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type MyFloat float64 9 10func (f MyFloat) Abs() float64 { 11 if f \u0026lt; 0 { 12 return float64(-f) 13 } 14 return float64(f) 15} 16 17func main() { 18 f := MyFloat(-math.Sqrt2) 19 fmt.Println(f.Abs()) 20} 输出 1.4142135623730951 指针类型的接收者 你可以为指针类型的接收者声明方法。\n这意味着对于某类型 T，接收者的类型可以用 *T 的文法。 （此外，T 本身不能是指针，比如不能是 *int。）\n指针接收者的方法可以修改接收者指向的值。 由于方法经常需要修改它的接收者，指针接收者比值接收者更常用。\n例如，下面为 *Vertex 定义了 Scale 方法。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Vertex struct { 9 X, Y float64 10} 11 12func (v Vertex) Abs() float64 { 13 return math.Sqrt(v.X*v.X + v.Y*v.Y) 14} 15 16func (v *Vertex) Scale(f float64) { 17 v.X = v.X * f 18 v.Y = v.Y * f 19} 20 21func main() { 22 v := Vertex{3, 4} 23 v.Scale(10) 24 fmt.Println(v.Abs()) 25} 输出 50 试着移除第 16 行 Scale 函数声明中的 *，观察此程序的行为如何变化。\n若使用值接收者，那么 Scale 方法会对原始 Vertex 值的副本进行操作。（对于函数的其它参数也是如此。）Scale 方法必须用指针接收者来更改 main 函数中声明的 Vertex 的值。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Vertex struct { 9 X, Y float64 10} 11 12func (v Vertex) Abs() float64 { 13 return math.Sqrt(v.X*v.X + v.Y*v.Y) 14} 15 16func (v Vertex) Scale(f float64) { 17 v.X = v.X * f 18 v.Y = v.Y * f 19} 20 21func main() { 22 v := Vertex{3, 4} 23 v.Scale(10) 24 fmt.Println(v.Abs()) // output: 5 25} 输出 5 指针与函数 现在我们要把 Abs 和 Scale 方法重写为函数。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Vertex struct { 9 X, Y float64 10} 11 12func Abs(v Vertex) float64 { 13 return math.Sqrt(v.X*v.X + v.Y*v.Y) 14} 15 16func Scale(v *Vertex, f float64) { 17 v.X = v.X * f 18 v.Y = v.Y * f 19} 20 21func main() { 22 v := Vertex{3, 4} 23 Scale(\u0026amp;v, 10) 24 fmt.Println(Abs(v)) 25} 输出 50 方法与指针重定向 1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Vertex struct { 6 X, Y float64 7} 8 9func (v *Vertex) Scale(f float64) { 10 v.X = v.X * f 11 v.Y = v.Y * f 12} 13 14func ScaleFunc(v *Vertex, f float64) { 15 v.X = v.X * f 16 v.Y = v.Y * f 17} 18 19func main() { 20 v := Vertex{3, 4} 21 v.Scale(2) 22 ScaleFunc(\u0026amp;v, 10) 23 24 p := \u0026amp;Vertex{4, 3} 25 p.Scale(3) 26 ScaleFunc(p, 8) 27 28 fmt.Println(v, p) 29} 输出 {60 80} \u0026amp;{96 72} 比较前两个程序，你大概会注意到带指针参数的函数必须接受一个指针：\n1var v Vertex 2ScaleFunc(v, 5) // 编译错误！ 3ScaleFunc(\u0026amp;v, 5) // OK 而接收者为指针的的方法被调用时，接收者既能是值又能是指针：\n1var v Vertex 2v.Scale(5) // OK 3p := \u0026amp;v 4p.Scale(10) // OK 对于语句 v.Scale(5) 来说，即便 v 是一个值而非指针，带指针接收者的方法也能被直接调用。 也就是说，由于 Scale 方法有一个指针接收者，为方便起见，Go 会将语句 v.Scale(5) 解释为 (\u0026amp;v).Scale(5)。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Vertex struct { 9 X, Y float64 10} 11 12func (v Vertex) Abs() float64 { 13 return math.Sqrt(v.X*v.X + v.Y*v.Y) 14} 15 16func AbsFunc(v Vertex) float64 { 17 return math.Sqrt(v.X*v.X + v.Y*v.Y) 18} 19 20func main() { 21 v := Vertex{3, 4} 22 fmt.Println(v.Abs()) 23 fmt.Println(AbsFunc(v)) 24 25 p := \u0026amp;Vertex{4, 3} 26 fmt.Println(p.Abs()) 27 fmt.Println(AbsFunc(*p)) 28} 输出 5\n5\n5\n5 这种情况下，方法调用 p.Abs() 会被解释为 (*p).Abs()。\n1var v Vertex 2fmt.Println(AbsFunc(v)) // OK 3fmt.Println(AbsFunc(\u0026amp;v)) // 编译错误！ 而以值为接收者的方法被调用时，接收者既能为值又能为指针：\n1var v Vertex 2fmt.Println(v.Abs()) // OK 3p := \u0026amp;v 4fmt.Println(p.Abs()) // OK 选择值或指针作为接收者 使用指针接收者的原因有二：方法能够修改其接收者指向的值，还可以避免在每次调用方法时复制该值。若值的类型为大型结构体时，这样会更加高效。\n在本例中，Scale 和 Abs 接收者的类型为 *Vertex，即便 Abs 并不需要修改其接收者。\n通常来说，所有给定类型的方法都应该有值或指针接收者，但并不应该二者混用。 （我们会在接下来几页中明白为什么。）\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Vertex struct { 9 X, Y float64 10} 11 12func (v *Vertex) Scale(f float64) { 13 v.X = v.X * f 14 v.Y = v.Y * f 15} 16 17func (v *Vertex) Abs() float64 { 18 return math.Sqrt(v.X*v.X + v.Y*v.Y) 19} 20 21func main() { 22 v := \u0026amp;Vertex{3, 4} 23 fmt.Printf(\u0026#34;缩放前：%+v，绝对值：%v\\n\u0026#34;, v, v.Abs()) 24 v.Scale(5) 25 fmt.Printf(\u0026#34;缩放后：%+v，绝对值：%v\\n\u0026#34;, v, v.Abs()) 26} 输出 缩放前：\u0026amp;{X:3 Y:4}，绝对值：5\n缩放后：\u0026amp;{X:15 Y:20}，绝对值：25 接口 接口类型是由一组方法签名定义的集合。 简单来说，接口就像是一份\u0026quot;合同\u0026quot;，它只规定了\u0026quot;要做什么\u0026quot;，但不关心\u0026quot;怎么做\u0026quot;。\n任何类型只要实现了接口中定义的所有方法，就被认为满足了这个接口。在 Go 中，不需要显式声明某个类型实现了某个接口，只要一个类型实现了接口要求的所有方法，它就自动满足了这个接口。这与其他语言如 Java 不同，Java 需要明确声明实现关系。\n警告 示例代码的第 22 行存在一个错误。由于 Abs 方法只为 *Vertex （指针类型）定义，因此 Vertex（值类型）并未实现 Abser。 1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type Abser interface { 9 Abs() float64 10} 11 12func main() { 13 var a Abser 14 f := MyFloat(-math.Sqrt2) 15 v := Vertex{3, 4} 16 17 a = f // a MyFloat 实现了 Abser 18 a = \u0026amp;v // a *Vertex 实现了 Abser 19 20 // 下面一行，v 是一个 Vertex（而不是 *Vertex） 21 // 所以没有实现 Abser。 22 // a = v 23 24 fmt.Println(a.Abs()) 25} 26 27type MyFloat float64 28 29func (f MyFloat) Abs() float64 { 30 if f \u0026lt; 0 { 31 return float64(-f) 32 } 33 return float64(f) 34} 35 36type Vertex struct { 37 X, Y float64 38} 39 40func (v *Vertex) Abs() float64 { 41 return math.Sqrt(v.X*v.X + v.Y*v.Y) 42} 输出 5 Abser接口只定义了一个方法：Abs() float64，意思是任何想要实现这个接口的类型都必须有一个名为Abs的方法，该方法不接受参数且返回一个float64值.\n在代码中，有两个类型与这个接口相关：\nMyFloat类型 - 直接实现了Abs()方法 1func (f MyFloat) Abs() float64 { 2 // 方法实现... 3} Vertex结构体 - 但这里有个关键点：Abs()方法是为*Vertex(指针类型)定义的，而不是Vertex(值类型) 1func (v *Vertex) Abs() float64 { 2 return math.Sqrt(v.X*v.X + v.Y*v.Y) 3} 那为什么 Vertex 值类型没有实现 Abser 接口，而 *Vertex 指针类型实现了呢？因为 Abs() 方法的接收者是 *Vertex（指针类型），而不是 Vertex（值类型）。这意味着：\n指针接收者：当 Abs() 方法定义在 *Vertex 上时，只有 *Vertex 类型实现了 Abser 接口。 值不能自动转为指针：Go 语言不会自动将 Vertex 值转换为 *Vertex 指针来调用方法。 在示例代码中：\na = f 有效，因为 MyFloat 类型实现了 Abs() 方法； a = \u0026amp;v 有效，因为 *Vertex 类型实现了 Abs() 方法； a = v 无效，因为 Vertex 类型没有实现 Abs() 方法 - 这行被注释掉了，因为它会导致编译错误。 可以这样理解：\n假设接口 Abser 是一个\u0026quot;会计算绝对值的东西\u0026quot;的规范 MyFloat 类型本身就知道如何计算自己的绝对值 但对于 Vertex 结构体，只有拿到它的地址(*Vertex)才能计算绝对值，而值本身(Vertex)不具备这个能力 这就是为什么 Go 的接口被称为\u0026quot;隐式接口\u0026quot;，它通过实现方法而不是声明来满足接口要求。\n运行逻辑 首先，8~10 行定义了一个接口 Abser：\ntype Abser interface { Abs() float64 }\n还有两个实现该接口的类型：MyFloat 和 *Vertex：\n1. MyFloat 类型及其方法\n1type MyFloat float64 2 3func (f MyFloat) Abs() float64 { 4 if f \u0026lt; 0 { 5 return float64(-f) 6 } 7 return float64(f) 8} 这里 MyFloat 是一个基于 float64 的自定义类型，它实现了 Abs() 方法，返回该浮点数的绝对值。在 Go 中，这被称为值接收者的方法。\n2. Vertex 类型及其方法\n1type Vertex struct { 2 X, Y float64 3} 4 5func (v *Vertex) Abs() float64 { 6 return math.Sqrt(v.X*v.X + v.Y*v.Y) 7} Vertex 是一个包含 X 和 Y 坐标的结构体，它的 Abs() 方法使用指针接收者，计算从原点到该点的欧几里得距离。\n1func main() { 2 var a Abser 3 f := MyFloat(-math.Sqrt2) 4 v := Vertex{3, 4} 5 6 a = f // a MyFloat 实现了 Abser 7 a = \u0026amp;v // a *Vertex 实现了 Abser 8 9 fmt.Println(a.Abs()) 10} 执行过程：\n变量声明： var a Abser 声明一个 Abser 接口类型的变量 a f := MyFloat(-math.Sqrt2) 创建一个值为 -√2 的 MyFloat 变量 v := Vertex{3, 4} 创建一个坐标为 (3,4) 的 Vertex 结构体 接口赋值： a = f -\u0026gt; 将 MyFloat 类型的变量赋值给接口变量 a。这是合法的，因为 MyFloat 类型实现了 Abs() 方法，满足了 Abser 接口的要求。 a = \u0026amp;v -\u0026gt; 将 Vertex 的指针赋值给接口变量 a。注意这里必须是指针，因为 Abs() 方法是定义在 *Vertex 上的，而不是 Vertex 上。换句话说，*Vertex 类型实现了 Abser 接口，但 Vertex 类型没有。 方法调用： fmt.Println(a.Abs()) -\u0026gt; 通过接口调用 Abs() 方法。虽然 a 是接口类型，但实际上它指向的是一个 *Vertex 类型的值（因为最后一次赋值是 a = \u0026amp;v），所以调用的是 *Vertex 的 Abs() 方法。 这将输出 5（即 √(3² + 4²)），这是点 (3,4) 到原点的距离。 这个简短的程序展示了 Go 语言面向接口编程的核心概念。\n接口的隐式实现 类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明，也就没有 implements 关键字。隐式接口从接口的实现中解耦了定义，这样接口的实现可以出现在任何包中，无需提前准备。因此，也就无需在每一个实现上增加新的接口名称，这样同时也鼓励了明确的接口定义。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type I interface { 6 M() 7} 8 9type T struct { 10 S string 11} 12 13// 此方法表示类型 T 实现了接口 I，不过我们并不需要显式声明这一点。 14func (t T) M() { 15 fmt.Println(t.S) 16} 17 18func main() { 19 var i I = T{\u0026#34;hello\u0026#34;} 20 i.M() 21} 答案 hello 接口是一种特殊的类型，它定义了一组方法的集合，但不提供具体实现。与其他编程语言不同的是，Go 采用了隐式接口实现的方式。这意味着类型通过实现一个接口的所有方法来自动实现该接口，而无需显式声明 。你的代码示例中:\n1type I interface { 2 M() 3} 4 5type T struct { 6 S string 7} 8 9func (t T) M() { 10 fmt.Println(t.S) 11} 这里，结构体 T 实现了方法 M()，因此它自动实现了接口 I，尽管代码中没有明确声明这一点。\n\u0026ldquo;implements\u0026rdquo; 关键字是什么？ 在 Java 等其他面向对象语言中，当一个类实现一个接口时需要使用 implements 关键字来显式声明。例如在 Java 中:\n1interface I { 2 void M(); 3} 4 5class T implements I { 6 public void M() { 7 // 实现 8 } 9} 但 Go 没有 \u0026ldquo;implements\u0026rdquo; 关键字，只要一个类型实现了接口中定义的所有方法，它就自动实现了该接口，无需显式声明。\n通俗理解：可以把 Go 的接口理解为一份\u0026quot;能力清单\u0026quot;。只要一个类型具备了这份清单上所列的所有\u0026quot;能力\u0026quot;(方法)，它就自动获得了这个接口的\u0026quot;资格证\u0026quot;，而不需要特意去申请或声明。\n在上面的例子中，接口 I 要求有一个 M() 方法，结构体 T 正好有这个方法，所以 T 自动满足了接口 I 的要求，可以被当作接口 I 类型来使用，如 var i I = T{\u0026quot;hello\u0026quot;} 所示。\n这种设计使得代码更加简洁灵活，并且允许开发者为现有类型\u0026quot;追加\u0026quot;接口实现，即使是对标准库或第三方库中的类型。\n接口值 接口也是值，它们可以像其它值一样传递；接口值可以用作函数的参数或返回值。\n在内部，接口值可以看做包含值和具体类型的元组：(value, type)。\n接口值保存了一个具体底层类型的具体值，接口值调用方法时会执行其底层类型的同名方法。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8type I interface { 9 M() 10} 11 12type T struct { 13 S string 14} 15 16func (t *T) M() { 17 fmt.Println(t.S) 18} 19 20type F float64 21 22func (f F) M() { 23 fmt.Println(f) 24} 25 26func main() { 27 var i I 28 29 i = \u0026amp;T{\u0026#34;Hello\u0026#34;} 30 describe(i) 31 i.M() 32 33 i = F(math.Pi) 34 describe(i) 35 i.M() 36} 37 38func describe(i I) { 39 fmt.Printf(\u0026#34;(%v, %T)\\n\u0026#34;, i, i) 40} 输出 (\u0026amp;{Hello}, *main.T)\nHello\n(3.141592653589793, main.F)\n3.141592653589793 底层值为 nil 的接口值 即便接口内的具体值为 nil，方法仍然会被 nil 接收者调用。\n在一些语言中，这会触发一个空指针异常，但在 Go 中通常会写一些方法来优雅地处理它（如本例中的 M 方法）。\n注意: 保存了 nil 具体值的接口其自身并不为 nil。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type I interface { 6 M() 7} 8 9type T struct { 10 S string 11} 12 13func (t *T) M() { 14 if t == nil { 15 fmt.Println(\u0026#34;\u0026lt;nil\u0026gt;\u0026#34;) 16 return 17 } 18 fmt.Println(t.S) 19} 20 21func main() { 22 var i I 23 24 var t *T 25 i = t 26 describe(i) 27 i.M() 28 29 i = \u0026amp;T{\u0026#34;hello\u0026#34;} 30 describe(i) 31 i.M() 32} 33 34func describe(i I) { 35 fmt.Printf(\u0026#34;(%v, %T)\\n\u0026#34;, i, i) 36} 输出 (\u0026lt;nil\u0026gt;, *main.T)\n\u0026lt;nil\u0026gt;\n(\u0026amp;{hello}, *main.T)\nhello nil 接口值 nil 接口值既不保存值也不保存具体类型。但为 nil 接口调用方法会产生运行时错误，因为接口的元组内并未包含能够指明该调用哪个具体方法的类型。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type I interface { 6 M() 7} 8 9func main() { 10 var i I 11 describe(i) 12 i.M() 13} 14 15func describe(i I) { 16 fmt.Printf(\u0026#34;(%v, %T)\\n\u0026#34;, i, i) 17} 输出 (\u0026lt;nil\u0026gt;, \u0026lt;nil\u0026gt;)\npanic: runtime error: invalid memory address or nil pointer dereference\n[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x492b99]\ngoroutine 1 [running]:\nmain.main()\n/tmp/sandbox1833746970/prog.go:12 +0x19\n空接口 指定了零个方法的接口值被称为空接口：interface{}。\n空接口可保存任何类型的值。（因为每个类型都至少实现了零个方法。）\n空接口被用来处理未知类型的值。例如，fmt.Print 可接受类型为 interface{} 的任意数量的参数。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 var i interface{} 7 describe(i) 8 9 i = 42 10 describe(i) 11 12 i = \u0026#34;hello\u0026#34; 13 describe(i) 14} 15 16func describe(i interface{}) { 17 fmt.Printf(\u0026#34;(%v, %T)\\n\u0026#34;, i, i) 18} 输出 (\u0026lt;nil\u0026gt;, \u0026lt;nil\u0026gt;)\n(42, int)\n(hello, string) 类型断言 总结 类型断言提供了访问接口值底层具体值的方式：t := i.(T)\n该语句断言接口值 i 保存了具体类型 T，并将其底层类型为 T 的值赋予变量 t。若 i 并未保存 T 类型的值，该语句就会触发一个 panic。\n为了判断一个接口值是否保存了一个特定的类型，类型断言可返回两个值：其底层值以及一个报告断言是否成功的布尔值：\nt, ok := i.(T)\n若 i 保存了一个 T，那么 t 将会是其底层值，而 ok 为 true。否则，ok 将为 false 而 t 将为 T 类型的零值，程序并不会产生 panic。\n请注意这种语法和读取一个映射时的相同之处。\n这说的什么玩意。。\n通俗点说，类型断言是 Go 语言中的一种机制，它允许我们检查并访问接口值中存储的具体类型值。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 var i interface{} = \u0026#34;hello\u0026#34; 7 8 s := i.(string) 9 fmt.Println(s) 10 11 s, ok := i.(string) 12 fmt.Println(s, ok) 13 14 f, ok := i.(float64) 15 fmt.Println(f, ok) 16 17 f = i.(float64) // panic 18 fmt.Println(f) 19} 输出 hello\nhello true\n0 false\npanic: interface conversion: interface {} is string, not float64\ngoroutine 1 [running]:\nmain.main()\n/tmp/sandbox2560958058/prog.go:17 +0x13f\n类型断言有两种主要形式：\n简单形式：t := i.(T) 这种形式直接断言接口值i中保存的是T类型的值。 如果断言正确，t将获得i中存储的值。 如果断言错误（即i中不是T类型的值），程序会触发panic。 安全形式：t, ok := i.(T) 这种形式返回两个值：可能的转换结果和一个布尔值。 如果i中确实保存了T类型的值，那么t会获得该值，ok为true。 如果i中不是T类型的值，t会是T类型的零值，ok为false，但程序不会panic。 类型断言就像是打开盒子检查里面到底是什么。\n简单形式(t := i.(T))就像是你确信盒子里装的是某样东西，直接拿出来用 安全形式(t, ok := i.(T))就像是你不确定盒子里是什么，所以先小心地看一眼，确认是你想要的东西再使用 以上面提供的代码示例：\n1var i interface{} = \u0026#34;hello\u0026#34; // 创建一个接口变量i，存入字符串\u0026#34;hello\u0026#34; 2 3s := i.(string) // 断言i中存储的是字符串，获取该字符串值 4fmt.Println(s) // 输出: \u0026#34;hello\u0026#34; 5 6s, ok := i.(string) // 安全方式断言i中存储的是字符串 7fmt.Println(s, ok) // 输出: \u0026#34;hello true\u0026#34; 8 9f, ok := i.(float64) // 安全方式断言i中存储的是float64 10fmt.Println(f, ok) // 输出: \u0026#34;0 false\u0026#34;（断言失败，f得到float64的零值0，ok为false） 11 12f = i.(float64) // 直接断言i中存储的是float64（但实际上不是） 13fmt.Println(f) // 不会执行，因为上一行会触发panic 类型断言的安全形式（t, ok := i.(T)）语法与Go中读取映射的语法非常相似：\n1value, exists := someMap[key] 两者都是返回两个值，第一个值是获取到的实际值（如果存在/断言成功），第二个值是一个布尔值，表示操作是否成功。\n当我们需要访问接口值中存储的具体类型特有的方法或属性时，就需要使用类型断言将接口值\u0026quot;还原\u0026quot;为其原始类型。\n类型选择 类型选择是一种按顺序从几个类型断言中选择分支的结构。与一般的 switch 语句相似，不过类型选择中的 case 为类型（而非值），它们针对给定接口值所存储的值的类型进行比较。\n1switch v := i.(type) { 2 case T: 3 // v 的类型为 T 4 case S: 5 // v 的类型为 S 6 default: 7 // 没有匹配，v 与 i 的类型相同 8} 说人话：它可以查明一个接口变量里面藏着什么具体类型的值，然后根据不同类型执行不同的代码。 类型选择中的声明与类型断言 i.(T) 的语法相同，只是具体类型 T 被替换成了关键字 type。\n此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 T 或 S 的情况下，变量 v 会分别按 T 或 S 类型保存 i 拥有的值。在默认（即没有匹配）的情况下，变量 v 与 i 的接口类型和值相同。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func do(i interface{}) { 6 switch v := i.(type) { 7 case int: 8 fmt.Printf(\u0026#34;二倍的 %v 是 %v\\n\u0026#34;, v, v*2) 9 case string: 10 fmt.Printf(\u0026#34;%q 长度为 %v 字节\\n\u0026#34;, v, len(v)) 11 default: 12 fmt.Printf(\u0026#34;我不知道类型 %T!\\n\u0026#34;, v) 13 } 14} 15 16func main() { 17 do(21) 18 do(\u0026#34;hello\u0026#34;) 19 do(true) 20} 输出 二倍的 21 是 42\n\u0026ldquo;hello\u0026rdquo; 长度为 5 字节\n我不知道类型 bool! 普通的 switch：根据值来选择执行哪个分支 类型选择：根据类型来选择执行哪个分支 类型选择只能用于接口值，因为只有接口值才能容纳不同类型的数据。\nStringer fmt 包中定义的 Stringer 是最普遍的接口之一。\n1type Stringer interface { 2 String() string 3} Stringer 是一个可以用字符串描述自己的类型。fmt 包（还有很多包）都通过此接口来打印值。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Person struct { 6 Name string 7 Age int 8} 9 10func (p Person) String() string { 11 return fmt.Sprintf(\u0026#34;%v (%v years)\u0026#34;, p.Name, p.Age) 12} 13 14func main() { 15 a := Person{\u0026#34;Arthur Dent\u0026#34;, 42} 16 z := Person{\u0026#34;Zaphod Beeblebrox\u0026#34;, 9001} 17 fmt.Println(a, z) // output: Arthur Dent (42 years) Zaphod Beeblebrox (9001 years) 18} 练习：Stringer 通过让 IPAddr 类型实现 fmt.Stringer 来打印点号分隔的地址。\n例如，IPAddr{1, 2, 3, 4} 应当打印为 \u0026quot;1.2.3.4\u0026quot;。\n例题 1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type IPAddr [4]byte 6 7// TODO: 为 IPAddr 添加一个 \u0026#34;String() string\u0026#34; 方法。 8 9func main() { 10 hosts := map[string]IPAddr{ 11 \u0026#34;loopback\u0026#34;: {127, 0, 0, 1}, 12 \u0026#34;googleDNS\u0026#34;: {8, 8, 8, 8}, 13 } 14 for name, ip := range hosts { 15 fmt.Printf(\u0026#34;%v: %v\\n\u0026#34;, name, ip) 16 } 17} loopback: [127 0 0 1]\ngoogleDNS: [8 8 8 8] 为 String() 类型添加了 IPAddr 方法，该方法可将 IP 地址格式化为以点分隔的字符串； 使用 fmt.Sprintf 将每个字节转换为十进制数，并用点将它们连接起来；\n就能将 IPAddr{1, 2, 3, 4} 打印为 1.2.3.4，以标准点阵格式显示 IP 地址。 答案 1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5) 6 7type IPAddr [4]byte 8 9// Add String method to implement fmt.Stringer interface 10func (ip IPAddr) String() string { 11 return fmt.Sprintf(\u0026#34;%d.%d.%d.%d\u0026#34;, ip[0], ip[1], ip[2], ip[3]) 12} 13 14func main() { 15 hosts := map[string]IPAddr{ 16 \u0026#34;loopback\u0026#34;: {127, 0, 0, 1}, 17 \u0026#34;googleDNS\u0026#34;: {8, 8, 8, 8}, 18 } 19 for name, ip := range hosts { 20 fmt.Printf(\u0026#34;%v: %v\\n\u0026#34;, name, ip) 21 } 22} loopback: 127.0.0.1\ngoogleDNS: 8.8.8.8 错误 Go 程序使用 error 值来表示错误状态。与 fmt.Stringer 类似，error 类型是一个内建接口：\n1type error interface { 2 Error() string 3} 与 fmt.Stringer 类似，fmt 包也会根据对 error 的实现来打印值。通常函数会返回一个 error 值，调用它的代码应当判断这个错误是否等于 nil 来进行错误处理。\n1i, err := strconv.Atoi(\u0026#34;42\u0026#34;) 2if err != nil { 3 fmt.Printf(\u0026#34;couldn\u0026#39;t convert number: %v\\n\u0026#34;, err) 4 return 5} 6fmt.Println(\u0026#34;Converted integer:\u0026#34;, i) error 为 nil 时表示成功；非 nil 的 error 表示失败。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;time\u0026#34; 6) 7 8type MyError struct { 9 When time.Time 10 What string 11} 12 13func (e *MyError) Error() string { 14 return fmt.Sprintf(\u0026#34;at %v, %s\u0026#34;, 15 e.When, e.What) 16} 17 18func run() error { 19 return \u0026amp;MyError{ 20 time.Now(), 21 \u0026#34;it didn\u0026#39;t work\u0026#34;, 22 } 23} 24 25func main() { 26 if err := run(); err != nil { 27 fmt.Println(err) 28 } 29} 输出 at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn\u0026rsquo;t work Readers io 包指定了 io.Reader 接口，它表示数据流的读取端。Go 标准库包含了该接口的许多实现，包括文件、网络连接、压缩和加密等等。\nio.Reader 接口有一个 Read 方法：func (T) Read(b []byte) (n int, err error)。Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时，它会返回一个 io.EOF 错误。\n示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;io\u0026#34; 6 \u0026#34;strings\u0026#34; 7) 8 9func main() { 10 r := strings.NewReader(\u0026#34;Hello, Reader!\u0026#34;) 11 12 b := make([]byte, 8) 13 for { 14 n, err := r.Read(b) 15 fmt.Printf(\u0026#34;n = %v err = %v b = %v\\n\u0026#34;, n, err, b) 16 fmt.Printf(\u0026#34;b[:n] = %q\\n\u0026#34;, b[:n]) 17 if err == io.EOF { 18 break 19 } 20 } 21} 输出 n = 8 err = b = [72 101 108 108 111 44 32 82]\nb[:n] = \u0026ldquo;Hello, R\u0026rdquo;\nn = 6 err = b = [101 97 100 101 114 33 32 82]\nb[:n] = \u0026ldquo;eader!\u0026rdquo;\nn = 0 err = EOF b = [101 97 100 101 114 33 32 82]\nb[:n] = \u0026quot;\u0026quot; 练习：Reader 实现一个 Reader 类型，它产生一个 ASCII 字符 'A' 的无限流。io.Reader 要求实现如下方法：\n1Read(p []byte) (n int, err error) 例题 1package main 2 3import \u0026#34;golang.org/x/tour/reader\u0026#34; 4 5type MyReader struct{} 6 7// TODO: 为 MyReader 添加一个 Read([]byte) (int, error) 方法。 8 9func main() { 10\treader.Validate(MyReader{}) 11} 此方法要将数据写入切片 p，返回写入的字节数，和可能的错误。这里题目的要求是产生无限个字符 'A' 的流，所以你写多少就返回多少，全写成 'A' 就可以了，不需要返回 io.EOF。\n答案 1package main 2 3import \u0026#34;golang.org/x/tour/reader\u0026#34; 4 5type MyReader struct{} 6 7// 实现 Read 方法 8func (r MyReader) Read(p []byte) (n int, err error) { 9 // 填满 p，全部赋值为 \u0026#39;A\u0026#39; 10 for i := range p { 11 p[i] = \u0026#39;A\u0026#39; 12 } 13 // 返回填充的长度，不报错 14 return len(p), nil 15} 16 17func main() { 18 reader.Validate(MyReader{}) 19} OK! 要点说明：\nfor i := range p { p[i] = 'A' } 保证每个元素都写成 'A' 返回值 len(p), nil，说明全部填充，没有出错 MyReader 是结构体，实现了 Read 方法，所以它就是 io.Reader 运行这段代码，reader.Validate 会测试你的实现是否正确。OK!\n练习：rot13Reader 有种常见的模式是一个 io.Reader 包装另一个 io.Reader，然后通过某种方式修改其数据流。例如，gzip.NewReader 函数接受一个 io.Reader（已压缩的数据流）并返回一个同样实现了 io.Reader 的 *gzip.Reader（解压后的数据流）。\n编写一个实现了 io.Reader 并从另一个 io.Reader 中读取数据的 rot13Reader，通过应用 rot13 代换密码对数据流进行修改。rot13Reader 类型已经提供。实现 Read 方法以满足 io.Reader。\nROT13 是一个简单的加密算法，它将每个字母向前移动13个位置。由于英文字母表有26个字母，所以应用 ROT13 两次会得到原文。这使得它既是加密算法也是解密算法。\n例题 1package main 2 3import ( 4\t\u0026#34;io\u0026#34; 5\t\u0026#34;os\u0026#34; 6\t\u0026#34;strings\u0026#34; 7) 8 9type rot13Reader struct { 10\tr io.Reader 11} 12 13func main() { 14\ts := strings.NewReader(\u0026#34;Lbh penpxrq gur pbqr!\u0026#34;) 15\tr := rot13Reader{s} 16\tio.Copy(os.Stdout, \u0026amp;r) 17} 其实就是要实现一个 rot13Reader 类型，包装另一个 io.Reader 并应用 ROT13 加密算法处理读取的数据。ROT13 是一种简单的字母替换密码，它将字母表中的每个字母替换为其后的第 13 个字母。\n答案 1package main 2 3import ( 4 \u0026#34;io\u0026#34; 5 \u0026#34;os\u0026#34; 6 \u0026#34;strings\u0026#34; 7) 8 9type rot13Reader struct { 10 r io.Reader 11} 12 13func (r *rot13Reader) Read(p []byte) (n int, err error) { 14 n, err = r.r.Read(p) 15 for i := 0; i \u0026lt; n; i++ { 16 if p[i] \u0026gt;= \u0026#39;A\u0026#39; \u0026amp;\u0026amp; p[i] \u0026lt;= \u0026#39;Z\u0026#39; { 17 p[i] = \u0026#39;A\u0026#39; + (p[i]-\u0026#39;A\u0026#39;+13)%26 18 } else if p[i] \u0026gt;= \u0026#39;a\u0026#39; \u0026amp;\u0026amp; p[i] \u0026lt;= \u0026#39;z\u0026#39; { 19 p[i] = \u0026#39;a\u0026#39; + (p[i]-\u0026#39;a\u0026#39;+13)%26 20 } 21 } 22 return 23} 24 25func main() { 26 s := strings.NewReader(\u0026#34;Lbh penpxrq gur pbqr!\u0026#34;) 27 r := rot13Reader{s} 28 io.Copy(os.Stdout, \u0026amp;r) 29} You cracked the code! 首先从被包装的 io.Reader 中读取数据到缓冲区 p 然后对读取到的每个字节应用 ROT13 变换: 对于大写字母 ('A'-'Z')，应用 ROT13 变换 对于小写字母 ('a'-'z')，应用 ROT13 变换 其他字符（如空格、标点符号等）保持不变 当程序运行时，它会将 Lbh penpxrq gur pbqr! 通过 ROT13 解码后输出 You cracked the code!\n图像 image包定义了 Image 接口：\n1package image 2type Image interface { 3 ColorModel() color.Model 4 Bounds() Rectangle 5 At(x, y int) color.Color 6} 注意: Bounds 方法的返回值 Rectangle 实际上是一个image.Rectangle，它在 image 包中声明。（请参阅文档了解全部信息。）\ncolor.Color 和 color.Model 类型也是接口，但是通常因为直接使用预定义的实现 image.RGBA 和 image.RGBAModel 而被忽视了。这些接口和类型由 image/color 包定义。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;image\u0026#34; 6) 7 8func main() { 9 m := image.NewRGBA(image.Rect(0, 0, 100, 100)) 10 fmt.Println(m.Bounds()) 11 fmt.Println(m.At(0, 0).RGBA()) 12} 输出 (0,0)-(100,100)\n0 0 0 0 练习：图像 还记得之前编写的图片生成器吗？我们再来编写另外一个，不过这次它将会返回一个 image.Image 的实现而非一个数据切片。定义你自己的 Image 类型，实现必要的方法并调用 pic.ShowImage。\nBounds 应当返回一个 image.Rectangle ，例如 image.Rect(0, 0, w, h)；ColorModel 应当返回 color.RGBAModel；At 应当返回一个颜色。上一个图片生成器的值 v 对应于此次的 color.RGBA{v, v, 255, 255}。\n例题 1package main 2 3import \u0026#34;golang.org/x/tour/pic\u0026#34; 4 5type Image struct{} 6 7func main() { 8\tm := Image{} 9\tpic.ShowImage(m) 10} 三步走：\n定义一个 Image 类型 实现必要的 image.Image 接口方法 调用 pic.ShowImage 来展示图像 image.Image 接口需要实现三个方法:\nBounds() - 应当返回 image.Rectangle，例如 image.Rect(0, 0, w, h) ColorModel() - 应当返回 color.RGBAModel At(x, y int) - 应当返回一个颜色，将上一个图像生成器的值 v 转换为 color.RGBA{v, v, 255, 255} 答案 1package main 2 3import ( 4 \u0026#34;image\u0026#34; 5 \u0026#34;image/color\u0026#34; 6 \u0026#34;golang.org/x/tour/pic\u0026#34; 7) 8 9type Image struct{ 10 width, height int 11} 12 13func (img Image) Bounds() image.Rectangle { 14 return image.Rect(0, 0, img.width, img.height) 15} 16 17func (img Image) ColorModel() color.Model { 18 return color.RGBAModel 19} 20 21func (img Image) At(x, y int) color.Color { 22 // 这里我们使用之前练习中类似的函数来生成颜色值 23 v := uint8((x + y) / 2) 24 // 返回颜色，将值v转换为color.RGBA{v, v, 255, 255} 25 return color.RGBA{v, v, 255, 255} 26} 27 28func main() { 29 m := Image{256, 256} // 创建一个256x256的图像 30 pic.ShowImage(m) 31} Bounds() 方法返回了图像的矩形边界，使用 image.Rect(0, 0, img.width, img.height)； ColorModel() 方法返回了 color.RGBAModel，表示我们使用RGBA颜色模型； At(x, y int) 方法根据坐标计算颜色值，将之前图像生成器的值 v 映射到 color.RGBA{v, v, 255, 255}，创建一个蓝色调的图像。 这个代码会生成一个渐变的蓝色图像，因为我们将 RGB 中的蓝色通道值设为了最大值 255，同时根据坐标计算的值来控制红色和绿色通道。你可以尝试不同的函数来计算 v 的值，例如 x*y、(x+y)/2、x^y 等，这会产生不同的图案效果。\n泛型 类型参数 可以使用类型参数编写 Go 函数来处理多种类型，可以让你编写更加通用和可复用的代码。函数的类型参数出现在函数参数之前的方括号之间：func Index[T comparable](s []T, x T) int。此声明意味着 s 是满足内置约束 comparable 的任何类型 T 的切片。 x 也是相同类型的值。\ncomparable 是一个有用的约束，它能让我们对任意满足该类型的值使用 == 和 != 运算符。在此示例中，我们使用它将值与所有切片元素进行比较，直到找到匹配项。 该 Index 函数适用于任何支持比较的类型。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5// Index 返回 x 在 s 中的下标，未找到则返回 -1。 6func Index[T comparable](s []T, x T) int { 7\tfor i, v := range s { 8\t// v 和 x 的类型为 T，它拥有 comparable 可比较的约束， 9\t// 因此我们可以使用 ==。 10\tif v == x { 11\treturn i 12\t} 13\t} 14\treturn -1 15} 16 17func main() { 18\t// Index 可以在整数切片上使用 19\tsi := []int{10, 20, 15, -10} 20\tfmt.Println(Index(si, 15)) 21 22\t// Index 也可以在字符串切片上使用 23\tss := []string{\u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;, \u0026#34;baz\u0026#34;} 24\tfmt.Println(Index(ss, \u0026#34;hello\u0026#34;)) 25} 输出 2\n-1 类型参数就像是给函数的\u0026quot;类型\u0026quot;留了一个空位，等到真正使用时再填入具体的类型。这样一个函数就能适用于多种数据类型，避免了对于每种不同类型，都需要复制并修改相似的代码的情况。\nfunc Index[T comparable](s []T, x T) int 里的 [T comparable] 可以理解为：\nT 是一个\u0026quot;占位符\u0026quot;，表示某种具体的类型 comparable 是一个约束，表示 T 必须是可以用 == 比较的类型 [[2]] 这个 Index 函数可以接受任何类型的切片和相同类型的单个值，并返回该值在切片中的位置（找不到则返回 -1）。\n当你调用 Index([]int{10, 20, 15}, 15) 时，Go 会自动将 T 理解为 int 类型。 当你调用 Index([]string{\u0026quot;foo\u0026quot;, \u0026quot;bar\u0026quot;}, \u0026quot;bar\u0026quot;) 时，Go 会自动将 T 理解为 string 类型。\n泛型类型 除了泛型函数之外，Go 还支持泛型类型。类型可以使用类型参数进行参数化，这对于实现通用数据结构非常有用，能够保存任意类型值的单链表的简单类型声明。\n下面是一个练习，请为此链表的实现添加一些功能。\n例题 1package main 2 3// List 表示一个可以保存任何类型的值的单链表。 4type List[T any] struct { 5\tnext *List[T] 6\tval T 7} 8 9func main() { 10} Go 从 1.18 版开始增加了对泛型（类型参数）的支持。通过泛型，可以编写适用于多种类型的代码，这对于实现像链接列表这样的泛型数据结构尤其有用。\n答案 1package main 2 3import \u0026#34;fmt\u0026#34; 4 5// List represents a single linked list that can store values of any type. 6type List[T any] struct { 7\tnext *List[T] 8\tval T 9} 10 11// NewList creates a new list with the given value. 12func NewList[T any](val T) *List[T] { 13\treturn \u0026amp;List[T]{val: val} 14} 15 16// PushFront adds a new element to the beginning of the list. 17func (l *List[T]) PushFront(val T) *List[T] { 18\tnewHead := \u0026amp;List[T]{ 19\tnext: l, 20\tval: val, 21\t} 22\treturn newHead 23} 24 25// Append adds a new element to the end of the list. 26func (l *List[T]) Append(val T) *List[T] { 27\tif l == nil { 28\treturn NewList(val) 29\t} 30\t31\tcurrent := l 32\tfor current.next != nil { 33\tcurrent = current.next 34\t} 35\tcurrent.next = \u0026amp;List[T]{val: val} 36\treturn l 37} 38 39// Print displays all elements in the list. 40func (l *List[T]) Print() { 41\tif l == nil { 42\tfmt.Println(\u0026#34;Empty list\u0026#34;) 43\treturn 44\t} 45\t46\tcurrent := l 47\tfor current != nil { 48\tfmt.Printf(\u0026#34;%v -\u0026gt; \u0026#34;, current.val) 49\tcurrent = current.next 50\t} 51\tfmt.Println(\u0026#34;nil\u0026#34;) 52} 53 54// Length returns the number of elements in the list. 55func (l *List[T]) Length() int { 56\tcount := 0 57\tcurrent := l 58\tfor current != nil { 59\tcount++ 60\tcurrent = current.next 61\t} 62\treturn count 63} 64 65// Contains checks if a value exists in the list. 66// This requires that T supports the comparable constraint. 67func Contains[T comparable](l *List[T], val T) bool { 68\tcurrent := l 69\tfor current != nil { 70\tif current.val == val { 71\treturn true 72\t} 73\tcurrent = current.next 74\t} 75\treturn false 76} 77 78// RemoveFirst removes the first occurrence of a value. 79// This also requires that T supports the comparable constraint. 80func RemoveFirst[T comparable](l *List[T], val T) *List[T] { 81\tif l == nil { 82\treturn nil 83\t} 84\t85\t// If head has the value to remove 86\tif l.val == val { 87\treturn l.next 88\t} 89\t90\t// Search for the value in subsequent nodes 91\tcurrent := l 92\tfor current.next != nil { 93\tif current.next.val == val { 94\tcurrent.next = current.next.next 95\tbreak 96\t} 97\tcurrent = current.next 98\t} 99\t100\treturn l 101} 102 103func main() { 104\t// Create a list of integers 105\tintList := NewList(1) 106\tintList = intList.Append(2) 107\tintList = intList.Append(3) 108\tintList = intList.PushFront(0) 109\t110\tfmt.Println(\u0026#34;Integer list:\u0026#34;) 111\tintList.Print() 112\tfmt.Printf(\u0026#34;List length: %d\\n\u0026#34;, intList.Length()) 113\tfmt.Printf(\u0026#34;Contains 2: %v\\n\u0026#34;, Contains(intList, 2)) 114\t115\t// Remove an element 116\tintList = RemoveFirst(intList, 2) 117\tfmt.Println(\u0026#34;After removing 2:\u0026#34;) 118\tintList.Print() 119\t120\t// Create a list of strings 121\tstrList := NewList(\u0026#34;hello\u0026#34;) 122\tstrList = strList.Append(\u0026#34;world\u0026#34;) 123\tstrList = strList.Append(\u0026#34;go\u0026#34;) 124\t125\tfmt.Println(\u0026#34;\\nString list:\u0026#34;) 126\tstrList.Print() 127} Integer list:\n0 -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; nil\nList length: 4\nContains 2: true\nAfter removing 2:\n0 -\u0026gt; 1 -\u0026gt; 3 -\u0026gt; nil\nString list:\nhello -\u0026gt; world -\u0026gt; go -\u0026gt; nil\nNewList 用初始值创建一个新列表， PushFront 将元素添加到开头（O(1) 运算）， Append 在末尾添加元素（O(n) 运算）， Print 显示列表中的所有元素， Length 计算元素个数， Contains 检查列表中是否存在值， RemoveFirst 移除数值的第一次出现。\nContains 和 RemoveFirst 函数要求类型参数 T 是 可比较的 ，这意味着可以使用 == 和 !=。有些操作是作为独立函数而不是方法实现的，因为它们需要额外的类型约束，而这些约束并不是原始结构体定义的一部分。该实现维护了链表结构，每次操作都会在需要时返回新的表头，这是链表操作的常见模式。\n小记 终于快把基础的学完了。当然，仅仅看代码没有用，还是要自己写点东西或者找点项目搞一下，不然始终就是纸上谈兵。\n","date":"2025-04-18T11:18:57+08:00","image":"https://langminjeii.ifantic.de/post/go-3.png","permalink":"https://langminjeii.ifantic.de/post/go-%E8%90%8C%E6%96%B0%E7%9A%84%E5%88%9D%E5%AD%A6%E4%B9%8B%E8%B7%AF-iii/","title":"Go 萌新的初学之路 III"},{"content":"合集墙详见这里。如果没有特别说明，css样式都是在 assets/scss/custom.scss 文件里修改，短代码的html文件放在 layouts/shortcodes 文件夹里。\n重点标记 重点标记 新建 mark.html，内容为： 1\u0026lt;mark\u0026gt;{{ .Get \u0026#34;text\u0026#34; }}\u0026lt;/mark\u0026gt; 在 custom.scss 中添加： 1//重点标记 2mark{ 3 background: hsla(199, 64%, 63%, 0.696); 4} 使用： 1{{\u0026lt; mark text=\u0026#34;标记\u0026#34; \u0026gt;}} 指定颜色文本 红色文本 橙色文本 黄色文本 绿色文本 深青色文本 蓝色文本 紫色文本\n使用： 1\u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;红色文本\u0026lt;/span\u0026gt; 147种标准颜色 基础颜色 (16种) 序号 颜色名称 效果 1 black ; #000080 黑色文本 2 silver ; #C0C0C0 银色文本 3 gray / grey ; #808080 灰色文本 4 white ; #FFFFFF 白色文本 5 maroon ; #800000 栗色/褐红色文本 6 red ; #FF0000 红色文本 7 purple ; #800080 紫色文本 8 fuchsia (等同于 magenta) ; #FF00FF 紫红色/洋红色文本 9 green ; #008000 绿色文本 10 lime ; #00FF00 酸橙绿文本 11 olive ; #808000 橄榄绿文本 12 yellow ; #FFFF00 黄色文本 13 navy ; #000080 海军蓝文本 14 blue ; #0000FF 蓝色文本 15 teal ; #008080 水鸭色/蓝绿色文本 16 aqua (等同于 cyan) ; #00FFFF 水色/浅青色文本 扩展颜色 (131种) 红色系 红色系\n序号 颜色名称 效果 1 indianred 红色文本 2 lightcoral 红色文本 3 salmon 红色文本 4 darksalmon 红色文本 5 lightsalmon 红色文本 6 crimson 红色文本 7 firebrick 红色文本 8 darkred 红色文本 粉红色系 粉红色系\n序号 颜色名称 效果 1 pink 粉色文本 2 lightpink 浅粉色文本 3 hotpink 亮粉色文本 4 deeppink 深粉色文本 5 mediumvioletred 中紫红色文本 6 palevioletred 淡紫红色文本 橙色系 橙色系\n序号 颜色名称 效果 1 coral 珊瑚色文本 2 tomato 番茄色文本 3 orangered 橙红色文本 4 darkorange 暗橙色文本 5 orange 橙色文本 黄色系 黄色系\n序号 颜色名称 效果 1 gold 金色文本 2 yellow 黄色文本 3 lightyellow 浅黄色文本 4 lemonchiffon 柠檬绸色文本 5 lightgoldenrodyellow 浅金黄色文本 6 papayawhip 番木瓜色文本 7 moccasin 鹿皮色文本 8 peachpuff 桃肉色文本 9 palegoldenrod 苍金棒色文本 10 khaki 卡其色文本 11 darkkhaki 深卡其色文本 紫色系 紫色系\n序号 颜色名称 效果 1 lavender 淡紫色文本 2 thistle 蓟色文本 3 plum 李子色文本 4 violet 紫罗兰色文本 5 orchid 兰花色文本 6 fuchsia (magenta) 洋红色文本 7 mediumorchid 中兰花色文本 8 mediumpurple 中紫色文本 9 rebeccapurple 丽贝卡紫色文本 10 blueviolet 蓝紫色文本 11 darkviolet 深紫罗兰色文本 12 darkorchid 深兰花色文本 13 darkmagenta 深洋红色文本 14 purple 紫色文本 15 indigo 靛蓝色文本 16 slateblue 板岩蓝文本 17 darkslateblue 深板岩蓝文本 18 mediumslateblue 中板岩蓝文本 绿色系 绿色系\n序号 颜色名称 效果 1 greenyellow 绿黄色文本 2 chartreuse 黄绿色文本 3 lawngreen 草绿色文本 4 lime 亮绿色文本 5 limegreen 酸橙绿文本 6 palegreen 浅绿色文本 7 lightgreen 亮绿色文本 8 mediumspringgreen 中春绿色文本 9 springgreen 春绿色文本 10 mediumseagreen 中海绿色文本 11 seagreen 海绿色文本 12 forestgreen 森林绿文本 13 green 绿色文本 14 darkgreen 深绿色文本 15 yellowgreen 黄绿色文本 16 olivedrab 橄榄褐文本 17 olive 橄榄色文本 18 darkolivegreen 深橄榄绿文本 19 mediumaquamarine 中碧绿色文本 20 darkseagreen 深海绿色文本 21 lightseagreen 亮海绿色文本 22 darkcyan 深青色文本 23 teal 凫蓝文本 蓝色系 蓝色系\n序号 颜色名称 效果 1 aqua (cyan) 水蓝色文本 2 lightcyan 浅青色文本 3 paleturquoise 苍绿色文本 4 aquamarine 碧绿色文本 5 turquoise 青绿色文本 6 mediumturquoise 中青绿色文本 7 darkturquoise 深青绿色文本 8 cadetblue 军蓝色文本 9 steelblue 钢蓝色文本 10 lightsteelblue 亮钢蓝色文本 11 powderblue 粉蓝色文本 12 lightblue 浅蓝色文本 13 skyblue 天蓝色文本 14 lightskyblue 亮天蓝色文本 15 deepskyblue 深天蓝色文本 16 dodgerblue 闪蓝色文本 17 cornflowerblue 矢车菊蓝色文本 18 royalblue 皇家蓝色文本 19 blue 纯蓝色文本 20 mediumblue 中蓝色文本 21 darkblue 深蓝色文本 22 navy 海军蓝色文本 23 midnightblue 午夜蓝色文本 棕色系 棕色系\n序号 颜色名称 效果 1 cornsilk 浅玉米丝色文本 2 blanchedalmond 漂白杏仁色文本 3 bisque 乳脂色文本 4 navajowhite 纳瓦白文本 5 wheat 小麦色文本 6 burlywood 硬木色文本 7 tan 棕褐色文本 8 rosybrown 玫瑰棕色文本 9 sandybrown 沙棕色文本 10 goldenrod 金菊色文本 11 darkgoldenrod 深金菊色文本 12 peru 秘鲁色文本 13 chocolate 巧克力色文本 14 saddlebrown 鞍棕色文本 15 sienna 赭色文本 16 brown 棕色文本 17 darkbrown 深棕色文本 白色系 白色系\n序号 颜色名称 效果 1 white 白色文本 2 snow 雪白文本 3 honeydew 蜜露色文本 4 mintcream 薄荷奶油色文本 5 azure 天蓝色文本 6 aliceblue 爱丽丝蓝文本 7 ghostwhite 幽灵白文本 8 whitesmoke 烟白文本 9 seashell 贝壳白文本 10 beige 米色文本 11 oldlace 旧蕾丝色文本 12 floralwhite 花卉白文本 13 ivory 象牙白文本 14 antiquewhite 古董白文本 15 linen 亚麻色文本 灰色系 灰色系\n序号 颜色名称 效果 1 gainsboro 浅灰文本 2 lightgray / lightgrey 亮灰文本 3 silver 银灰文本 4 darkgray / darkgrey 暗灰文本 5 gray / grey 标准灰文本 6 dimgray / dimgrey 昏灰文本 7 lightslategray / lightslategrey 亮岩灰文本 8 slategray / slategrey 岩灰文本 9 darkslategray / darkslategrey 暗岩灰文本 10 black 纯黑文本 文本缩写 这个短代码只在电脑端生效 新建 abbr.html，内容为： 1\u0026lt;abbr title=\u0026#34;{{ .Get \u0026#34;title\u0026#34; }}\u0026#34;\u0026gt;{{ .Get \u0026#34;text\u0026#34; }}\u0026lt;/abbr\u0026gt; 使用： 1{{\u0026lt; abbr title=\u0026#34;感觉很神秘\u0026#34; text=\u0026#34;这个短代码只在电脑端生效\u0026#34; \u0026gt;}} 文本折叠 点击显示 让我看看！ 新建 detail.html，内容为： 1\u0026lt;details\u0026gt; 2 \u0026lt;summary\u0026gt;{{ (.Get 0) | markdownify }}\u0026lt;/summary\u0026gt; 3 {{ .Inner | markdownify }} 4\u0026lt;/details\u0026gt; 使用： 1{{\u0026lt; spoiler \u0026#34;点击显示\u0026#34; \u0026gt;}} 2让我看看！ 3{{\u0026lt; /spoiler \u0026gt;}} 高斯模糊 手动打码效果\n在 custom.scss 中添加： 1//文本高斯模糊 2.blur { 3 color: transparent; 4 text-shadow:0px 0px 8px var(--card-text-color-main) 5} 6 7.blur:hover { 8 color: transparent; 9 text-shadow:0px 0px 0px var(--card-text-color-main) 10 11} 使用： 1\u0026lt;span class=\u0026#34;blur\u0026#34;\u0026gt;手动打码效果\u0026lt;/span\u0026gt; 文本黑幕 数据删除！\n在 custom.scss 中添加： 1//文本黑幕效果 2.shady { 3 color:#000; 4 font-weight: bold; 5 box-shadow: 0px -20px 0px rgba(0,0,0,1) inset; 6 transition: all 0.3s ease; 7} 8.shady:hover{ 9 font-weight: bold; 10 color:#FFF; 11 box-shadow: 0px -20px 0px rgba(0,0,0,1) inset; 12} 使用： 1\u0026lt;span class=\u0026#34;shady\u0026#34;\u0026gt;数据删除！\u0026lt;/span\u0026gt; 文字抖动 这是基本的摇晃效果。 这个段落持续摇晃。 新建shake.html，内容为： 1\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/csshake/1.5.3/csshake.min.css\u0026#34; /\u0026gt; 2 3\u0026lt;div class=\u0026#34;shake {{ .Get \u0026#34;effect\u0026#34; }}\u0026#34;\u0026gt;{{ .Inner }}\u0026lt;/div\u0026gt; 4 5\u0026lt;div class=\u0026#34;shake\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 6\u0026lt;div class=\u0026#34;shake shake-hard\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 7\u0026lt;div class=\u0026#34;shake shake-slow\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 8\u0026lt;div class=\u0026#34;shake shake-little\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 9\u0026lt;div class=\u0026#34;shake shake-horizontal\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 10\u0026lt;div class=\u0026#34;shake shake-vertical\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 11\u0026lt;div class=\u0026#34;shake shake-rotate\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 12\u0026lt;div class=\u0026#34;shake shake-opacity\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 13\u0026lt;div class=\u0026#34;shake shake-crazy\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 14\u0026lt;!-- Freeze the animation at that point when :hover --\u0026gt; 15\u0026lt;div class=\u0026#34;shake shake-freeze\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 16\u0026lt;!-- Continuous animation instead on :hover --\u0026gt; 17\u0026lt;div class=\u0026#34;shake shake-constant\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 使用： 1{{\u0026lt; shake effect=\u0026#34;shake\u0026#34; \u0026gt;}}这是基本的摇晃效果。{{\u0026lt; /shake \u0026gt;}} 文字渐变 我挑的配色好看吧！\n在 custom.scss 中添加： 1//文字颜色渐变 2.colorfulfont { 3 background: linear-gradient(to right, rgb(25, 221, 238), #ed4588); //第一个颜色代码是渐变起始色，第二个颜色代码是渐变结束色； 4 -webkit-background-clip: text; 5 background-clip: text; 6 color: transparent; 7} 使用： 1\u0026lt;font class=\u0026#34;colorfulfont\u0026#34;\u0026gt; 我挑的配色好看吧！\u0026lt;/font\u0026gt; 文本位置 文字居左\n文字居中\n文字居右\n新建 align.html，内容为： 1\u0026lt;p style=\u0026#34;text-align:{{ index .Params 0 }}\u0026#34;\u0026gt;{{ index .Params 1 | markdownify }}\u0026lt;/p\u0026gt; 使用： 1{{\u0026lt; align left \u0026#34;文字居左\u0026#34; \u0026gt;}} 2{{\u0026lt; align center \u0026#34;文字居中\u0026#34; \u0026gt;}} 3{{\u0026lt; align right \u0026#34;文字居右\u0026#34; \u0026gt;}} 摘录引用 “年复一年，创作的冲动随年衰减，创作的能力逐渐消失——也许两者根本上是一回事，我们常把自己的写作冲动误认为自己的写作才能，自以为要写就意味着会写。”\n钱钟书 《围城》重印前记 新建 blockquote.html，内容为： 1\u0026lt;!-- reset scratch variables at the start --\u0026gt; 2{{ $.Scratch.Set \u0026#34;bl_author\u0026#34; false }} 3{{ $.Scratch.Set \u0026#34;bl_source\u0026#34; false }} 4{{ $.Scratch.Set \u0026#34;bl_link\u0026#34; false }} 5{{ $.Scratch.Set \u0026#34;bl_title\u0026#34; false }} 6 7{{ if .IsNamedParams }} 8 {{ $.Scratch.Set \u0026#34;bl_author\u0026#34; (.Get \u0026#34;author\u0026#34;) }} 9 {{ $.Scratch.Set \u0026#34;bl_source\u0026#34; (.Get \u0026#34;source\u0026#34;) }} 10 {{ $.Scratch.Set \u0026#34;bl_link\u0026#34; (.Get \u0026#34;link\u0026#34;) }} 11 {{ $.Scratch.Set \u0026#34;bl_title\u0026#34; (.Get \u0026#34;title\u0026#34;) }} 12{{ else }} 13 \u0026lt;!-- for the positional version if any --\u0026gt; 14{{ end }} 15 16\u0026lt;!-- if title is not set explicitly then we need to beautify the link 17 if length of link is more than 32 chars, we will cut it off by 32 and 18 then drop everything after the last / if any and put it in into title --\u0026gt; 19 20{{ with $.Scratch.Get \u0026#34;bl_title\u0026#34; }} 21 \u0026lt;!-- do nothing --\u0026gt; 22{{ else }} 23 {{ with $.Scratch.Get \u0026#34;bl_link\u0026#34; }} \u0026lt;!-- if link is given --\u0026gt; 24 {{ range last 1 (split ($.Scratch.Get \u0026#34;bl_link\u0026#34; ) \u0026#34;://\u0026#34;) }} \u0026lt;!-- split by :// and then only take the items after it to remove protocol:// --\u0026gt; 25 {{ $.Scratch.Set \u0026#34;title_without_protocol\u0026#34; . }} 26 {{ end }} 27 {{ range last 1 (split ($.Scratch.Get \u0026#34;title_without_protocol\u0026#34; ) \u0026#34;www.\u0026#34;) }} \u0026lt;!-- also remove the www. at the start if any. we are using a second split because all URLS may not start with it --\u0026gt; 28 {{ $.Scratch.Set \u0026#34;title_without_protocol\u0026#34; . }} 29 {{ end }} 30 {{ $.Scratch.Set \u0026#34;bl_title\u0026#34; ($.Scratch.Get \u0026#34;title_without_protocol\u0026#34;) }} 31 32 \u0026lt;!-- if link is longer than 32 bytes we should trim it --\u0026gt; 33 {{ if (gt (len ($.Scratch.Get \u0026#34;title_without_protocol\u0026#34;) ) 32) }} 34 {{ $title := (slicestr ($.Scratch.Get \u0026#34;title_without_protocol\u0026#34;) 0 32) }} \u0026lt;!-- get the first 32 characters of title_without_protocol --\u0026gt; 35 {{ $split_by_fw_slash := split $title \u0026#34;/\u0026#34; }} \u0026lt;!-- now split on / because we want to stop after the last forward slash --\u0026gt; 36 {{ $count := (sub (len $split_by_fw_slash) 1) }} \u0026lt;!-- we want everything but the last part so we adjust the count accordingly --\u0026gt; 37 38 {{ $.Scratch.Set \u0026#34;tempstring\u0026#34; \u0026#34;\u0026#34; }} \u0026lt;!-- temp variable to hold the concatinated string --\u0026gt; 39 {{ range first $count $split_by_fw_slash }} \u0026lt;!-- loop through all parts except last and concat them (add / between halves) --\u0026gt; 40 {{ $.Scratch.Set \u0026#34;tempstring\u0026#34; ( . | printf \u0026#34;%s%s/\u0026#34; ($.Scratch.Get \u0026#34;tempstring\u0026#34;) | printf \u0026#34;%s\u0026#34; ) }} 41 {{ end }} 42 {{ $.Scratch.Set \u0026#34;bl_title\u0026#34; ( printf \u0026#34;%s...\u0026#34; ($.Scratch.Get \u0026#34;tempstring\u0026#34;) | printf \u0026#34;%s\u0026#34; ) }} 43 {{ end }} 44 {{ end }} 45{{ end }} 46 47\u0026lt;blockquote\u0026gt; 48 \u0026lt;p\u0026gt;{{ .Inner | markdownify }}\u0026lt;/p\u0026gt; 49 \u0026lt;footer style=\u0026#34;text-align:right\u0026#34;\u0026gt; 50 \u0026lt;strong\u0026gt;{{ with $.Scratch.Get \u0026#34;bl_author\u0026#34; }}{{ . }}{{ end }}\u0026lt;/strong\u0026gt; 51 {{ with $.Scratch.Get \u0026#34;bl_source\u0026#34; }} 52 \u0026lt;cite\u0026gt;{{ . }}\u0026lt;/cite\u0026gt; 53 {{ else }} 54 {{ with $.Scratch.Get \u0026#34;bl_link\u0026#34; }} 55 \u0026lt;cite\u0026gt; 56 \u0026lt;a href=\u0026#34;{{ . }}\u0026#34; title=\u0026#34;{{ . }}\u0026#34; rel=\u0026#34;noopener noreferrer\u0026#34;\u0026gt;{{ $.Scratch.Get \u0026#34;bl_title\u0026#34; }}\u0026lt;/a\u0026gt; \u0026lt;!-- can\u0026#39;t have new lines here --\u0026gt; 57 \u0026lt;/cite\u0026gt; 58 {{ else }} 59 {{ with $.Scratch.Get \u0026#34;bl_title\u0026#34; }} 60 \u0026lt;cite\u0026gt; 61 {{ $.Scratch.Get \u0026#34;bl_title\u0026#34; }}\u0026lt;/a\u0026gt; 62 \u0026lt;/cite\u0026gt; 63 {{ end }} 64 {{ end }} 65 {{ end }} 66 \u0026lt;/footer\u0026gt; 67\u0026lt;/blockquote\u0026gt; 使用： 1{{\u0026lt; blockquote author=\u0026#34;作者\u0026#34; link=\u0026#34;#\u0026#34; title=\u0026#34;作品名\u0026#34; \u0026gt;}} 2这里写引用内容 3{{\u0026lt; /blockquote \u0026gt;}} 评分样式 新建 rating.html，内容为： 1\u0026lt;span class=\u0026#34;star-rating\u0026#34;\u0026gt; 2 {{- if ge (.Get 0) (.Get 1) -}} 3 {{- $star_outline := sub (int (.Get 0)) (int (.Get 1)) -}} 4 {{- range $i, $sequence := (seq (.Get 1)) -}} 5 \u0026lt;i class=\u0026#34;star\u0026#34;\u0026gt;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; viewBox=\u0026#34;0 0 576 512\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M381.2 150.3L524.9 171.5C536.8 173.2 546.8 181.6 550.6 193.1C554.4 204.7 551.3 217.3 542.7 225.9L438.5 328.1L463.1 474.7C465.1 486.7 460.2 498.9 450.2 506C440.3 513.1 427.2 514 416.5 508.3L288.1 439.8L159.8 508.3C149 514 135.9 513.1 126 506C116.1 498.9 111.1 486.7 113.2 474.7L137.8 328.1L33.58 225.9C24.97 217.3 21.91 204.7 25.69 193.1C29.46 181.6 39.43 173.2 51.42 171.5L195 150.3L259.4 17.97C264.7 6.954 275.9-.0391 288.1-.0391C300.4-.0391 311.6 6.954 316.9 17.97L381.2 150.3z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026lt;/i\u0026gt; 6 {{- end -}} 7 {{- range $i, $sequence := (seq $star_outline) -}} 8 \u0026lt;i class=\u0026#34;star-outline\u0026#34;\u0026gt;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; viewBox=\u0026#34;0 0 576 512\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M381.2 150.3L524.9 171.5C536.8 173.2 546.8 181.6 550.6 193.1C554.4 204.7 551.3 217.3 542.7 225.9L438.5 328.1L463.1 474.7C465.1 486.7 460.2 498.9 450.2 506C440.3 513.1 427.2 514 416.5 508.3L288.1 439.8L159.8 508.3C149 514 135.9 513.1 126 506C116.1 498.9 111.1 486.7 113.2 474.7L137.8 328.1L33.58 225.9C24.97 217.3 21.91 204.7 25.69 193.1C29.46 181.6 39.43 173.2 51.42 171.5L195 150.3L259.4 17.97C264.7 6.954 275.9-.0391 288.1-.0391C300.4-.0391 311.6 6.954 316.9 17.97L381.2 150.3z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026lt;/i\u0026gt; 9 {{- end -}} 10 {{- end -}} 11\u0026lt;/span\u0026gt; 在 custom.scss 中添加： 1//星星评级 2i.star{ 3 color: hsl(61, 79%, 63%); /*星星的颜色*/ 4 } 5 6 i.star-outline{ 7 color: hsl(211, 31%, 74%);/*空星星的颜色*/ 8 } 9 10 i.star svg, i.star-outline svg{ 11 width: 20px; 12 fill: currentColor; 13} 使用： 1{{\u0026lt; rating 10 7 \u0026gt;}} 键盘样式 Ctrl+Alt+Del\n在 custom.scss 中添加： 1//键盘标签样式 2nkbd { 3 margin: 0 .1em; 4 padding: .1em .6em; 5 font-size: .8em; 6 color: #242729; 7 background: #fff; 8 border: 1px solid #adb3b9; 9 border-radius: 3px; 10 box-shadow: 0px 1px 0 rgba(12, 13, 14, 0.2), 0 0 0 2px #fff inset; 11 white-space: nowrap; 12 vertical-align: middle; 13 font-family: SourceHanSerifSC; 14} 使用： 1\u0026lt;nkbd\u0026gt;Ctrl\u0026lt;/nkbd\u0026gt;+\u0026lt;nkbd\u0026gt;Alt\u0026lt;/nkbd\u0026gt;+\u0026lt;nkbd\u0026gt;Del\u0026lt;/nkbd\u0026gt; 卡片样式 卡片样式 新建 card.html，内容为： 1{{- $raw := (markdownify .Inner | chomp) -}} {{- $block := findRE 2 \u0026#34;(?is)^\u0026lt;(?:address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h(?:1|2|3|4|5|6)|header|hgroup|hr|li|main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video)\\\\b\u0026#34; 3 $raw 1 -}} 4 \u0026lt;div class=\u0026#34;mycard\u0026#34;\u0026gt; 5 \u0026lt;div class=\u0026#34;content\u0026#34;\u0026gt;{{- if or $block (not $raw) }}{{ $raw }}{{ else }} {{ $raw }} {{ end -}}\u0026lt;/div\u0026gt; 6 \u0026lt;/div\u0026gt; 在 custom.scss 中添加： 1// 卡片样式 2.mycard { 3 padding: 10px 20px; 4 margin: 20px 0; 5 border-radius: 4px; 6 word-break: break-all; 7 background: #d2e5eb14; 8 box-shadow: 0 6px 10px 0 #00000033; 9 .content { 10 line-height: 30px; 11 } 12} 使用： 1{{\u0026lt; card \u0026gt;}}卡片样式{{\u0026lt; /card \u0026gt;}} 仿Github样式 ifanticの小屋 鸳鸯帐里暖芙蓉 此生何处不相逢 Hugo 新建 github.html，内容为： 1\u0026lt;div class=\u0026#34;github\u0026#34;\u0026gt; 2 \u0026lt;div class=\u0026#34;logo\u0026#34;\u0026gt; 3 {{ replace $.Site.Data.SVG.repository \u0026#34;icon\u0026#34; \u0026#34;icon github-icon\u0026#34; | safeHTML }} 4 \u0026lt;a class=\u0026#34;name\u0026#34; href={{ .Get \u0026#34;link\u0026#34; }} target=\u0026#34;_blank\u0026#34;\u0026gt;{{ .Get \u0026#34;name\u0026#34; }}\u0026lt;/a\u0026gt; 5 \u0026lt;/div\u0026gt; 6 \u0026lt;div class=\u0026#34;description\u0026#34;\u0026gt;{{ .Get \u0026#34;description\u0026#34; }}\u0026lt;/div\u0026gt; 7 \u0026lt;div class=\u0026#34;language\u0026#34;\u0026gt; 8 \u0026lt;span class=\u0026#34;language-color\u0026#34; style=\u0026#34;background-color: {{ .Get \u0026#34;color\u0026#34; }}\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 9 \u0026lt;span class=\u0026#34;language-name\u0026#34;\u0026gt;{{ .Get \u0026#34;language\u0026#34; }}\u0026lt;/span\u0026gt; 10 \u0026lt;/div\u0026gt; 11\u0026lt;/div\u0026gt; 在 custom.scss 中添加： 1//github样式 2.github { 3 border: 1px solid var(--card-text-color-main); 4 border-radius: 3px; 5 margin: 0 auto; 6 margin-bottom: 1em; 7 padding: 1em; 8 .github-icon { 9 width: 1.2em; 10 height: 1.2em; 11 margin-right: 0.5em; 12 margin-bottom: 0.2em; 13 fill: var(--card-text-color-main); 14 transition: all 0.5s; 15 } 16 .name { 17 font-weight: bold; 18 color: #2e97d9; 19 text-decoration: underline; 20 margin-left: 0.5em; 21 position: relative; 22 top: -5px; //加这个属性是因为我的图标和名称无法对齐，实际使用的时候要根据自己的网站修改 23 } 24 .description { 25 margin-top: 0.5em; 26 margin-bottom: 0.5em; 27 color: var(--card-text-color-main); 28 text-align: justify; 29 font-size: 90%; 30 transition: all 0.5s; 31 } 32 .language-color { 33 position: relative; 34 top: 1px; 35 display: inline-block; 36 width: 0.75em; 37 height: 0.75em; 38 border-radius: 50%; 39 } 40 .language-name { 41 color: var(--card-text-color-main); 42 font-size: 90%; 43 margin-left: 0.5em; 44 transition: all 0.5s; 45 } 46} 在 data/SVG.toml 中加入图标（给出的代码是Github自己的图标，我已经换成了自定义的）： 1repository = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 16 16\u0026#34;\u0026gt;\u0026lt;path fill-rule=\u0026#34;evenodd\u0026#34; clip-rule=\u0026#34;evenodd\u0026#34; d=\u0026#34;M2 2.5C2 1.83696 2.26339 1.20107 2.73223 0.732233C3.20108 0.263392 3.83696 0 4.5 0L13.25 0C13.4489 0 13.6397 0.0790176 13.7803 0.21967C13.921 0.360322 14 0.551088 14 0.75V13.25C14 13.4489 13.921 13.6397 13.7803 13.7803C13.6397 13.921 13.4489 14 13.25 14H10.75C10.5511 14 10.3603 13.921 10.2197 13.7803C10.079 13.6397 10 13.4489 10 13.25C10 13.0511 10.079 12.8603 10.2197 12.7197C10.3603 12.579 10.5511 12.5 10.75 12.5H12.5V10.5H4.5C4.30308 10.5 4.11056 10.5582 3.94657 10.6672C3.78257 10.7762 3.65442 10.9312 3.57816 11.1128C3.50191 11.2943 3.48096 11.4943 3.51793 11.6878C3.5549 11.8812 3.64816 12.0594 3.786 12.2C3.92524 12.3422 4.0023 12.5338 4.00024 12.7328C3.99818 12.9318 3.91716 13.1218 3.775 13.261C3.63285 13.4002 3.4412 13.4773 3.24222 13.4752C3.04325 13.4732 2.85324 13.3922 2.714 13.25C2.25571 12.7829 1.99929 12.1544 2 11.5V2.5ZM12.5 1.5V9H4.5C4.144 9 3.806 9.074 3.5 9.208V2.5C3.5 2.23478 3.60536 1.98043 3.79289 1.79289C3.98043 1.60536 4.23478 1.5 4.5 1.5H12.5ZM5 12.25V15.5C5 15.5464 5.01293 15.5919 5.03734 15.6314C5.06175 15.6709 5.09667 15.7028 5.1382 15.7236C5.17972 15.7444 5.22621 15.7532 5.27245 15.749C5.31869 15.7448 5.36286 15.7279 5.4 15.7L6.85 14.613C6.89328 14.5805 6.94591 14.563 7 14.563C7.05409 14.563 7.10673 14.5805 7.15 14.613L8.6 15.7C8.63714 15.7279 8.68131 15.7448 8.72755 15.749C8.77379 15.7532 8.82028 15.7444 8.8618 15.7236C8.90333 15.7028 8.93826 15.6709 8.96266 15.6314C8.98707 15.5919 9 15.5464 9 15.5V12.25C9 12.1837 8.97366 12.1201 8.92678 12.0732C8.87989 12.0263 8.81631 12 8.75 12H5.25C5.1837 12 5.12011 12.0263 5.07322 12.0732C5.02634 12.1201 5 12.1837 5 12.25Z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 使用： 1{{\u0026lt; github 2 name=\u0026#34;链接标题\u0026#34; 3 link=\u0026#34;#\u0026#34; 4 description=\u0026#34;内容描述\u0026#34; 5 color=\u0026#34;#F48201\u0026#34; 6 language=\u0026#34;Go\u0026#34; 7\u0026gt;}} 标签块样式 Warning\ninfo\nnote\ntips\n新建 notice.html，内容为： 1\u0026lt;!-- 文件位置：~/layouts/shortcodes/notice.html --\u0026gt; 2 3\u0026lt;!--https://github.com/martignoni/hugo-notice--\u0026gt; 4{{- $noticeType := .Get 0 -}} 5 6{{- $raw := (markdownify .Inner | chomp) -}} 7 8{{- $block := findRE \u0026#34;(?is)^\u0026lt;(?:address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h(?:1|2|3|4|5|6)|header|hgroup|hr|li|main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video)\\\\b\u0026#34; $raw 1 -}} 9 10{{ $icon := (replace (index $.Site.Data.SVG $noticeType) \u0026#34;icon\u0026#34; \u0026#34;icon notice-icon\u0026#34;) }} 11\u0026lt;div class=\u0026#34;notice {{ $noticeType }}\u0026#34; {{ if len .Params | eq 2 }} id=\u0026#34;{{ .Get 1 }}\u0026#34; {{ end }}\u0026gt; 12 \u0026lt;div class=\u0026#34;notice-title\u0026#34;\u0026gt;{{ $icon | safeHTML }}\u0026lt;/div\u0026gt; 13 {{- if or $block (not $raw) }}{{ $raw }}{{ else }}\u0026lt;p\u0026gt;{{ $raw }}\u0026lt;/p\u0026gt;{{ end -}} 14\u0026lt;/div\u0026gt; 在 custom.scss 中添加： 1//增加notice短代码 2.notice { 3 position:relative; 4 padding: 1em 1em 1em 2.5em; 5 margin-bottom: 1em; 6 border-radius: 4px; 7 p:last-child { 8 margin-bottom: 0; 9 } 10 .notice-title { 11 position: absolute; 12 left: 0.8em; 13 .notice-icon { 14 width: 1.2em; 15 height: 1.2em; 16 } 17 } 18 \u0026amp;.notice-warning { 19 background: hsla(0, 65%, 65%, 0.15); 20 border-left: 5px solid hsl(0, 65%, 65%); 21 .notice-title { 22 color: hsl(0, 65%, 65%); 23 } 24 } 25 \u0026amp;.notice-info { 26 background: hsla(30, 80%, 70%, 0.15); 27 border-left: 5px solid hsl(30, 80%, 70%); 28 .notice-title { 29 color: hsl(30, 80%, 70%); 30 } 31 } 32 \u0026amp;.notice-note { 33 background: hsla(200, 65%, 65%, 0.15); 34 border-left: 5px solid hsl(200, 65%, 65%); 35 .notice-title { 36 color: hsl(200, 65%, 65%); 37 } 38 } 39 \u0026amp;.notice-tip { 40 background: hsla(140, 65%, 65%, 0.15); 41 border-left: 5px solid hsl(140, 65%, 65%); 42 .notice-title { 43 color: hsl(140, 65%, 65%); 44 } 45 } 46} 47 48[data-theme=\u0026#34;dark\u0026#34;] .notice { 49 \u0026amp;.notice-warning { 50 background: hsla(0, 25%, 35%, 0.15); 51 border-left: 5px solid hsl(0, 25%, 35%); 52 .notice-title { 53 color: rgba(224, 108, 108, 0.5); 54 } 55 } 56 \u0026amp;.notice-info { 57 background: hsla(30, 25%, 35%, 0.15); 58 border-left: 5px solid hsl(30, 25%, 35%); 59 .notice-title { 60 color: rgba(240, 178, 117, 0.5); 61 } 62 } 63 \u0026amp;.notice-note { 64 background: hsla(200, 25%, 35%, 0.15); 65 border-left: 5px solid hsl(200, 25%, 35%); 66 .notice-title { 67 color: rgba(108, 185, 224, 0.5); 68 } 69 } 70 \u0026amp;.notice-tip { 71 background: hsla(140, 25%, 35%, 0.15); 72 border-left: 5px solid hsl(140, 25%, 35%); 73 .notice-title { 74 color: rgba(108, 224, 147, 0.5); 75 } 76 } 77} 在 data/SVG.toml 中加入： 1# Notice Icon 2notice-warning = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 576 512\u0026#34; fill=\u0026#34;hsl(0, 65%, 65%)\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zM124 296c-6.6.0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6.0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H124z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 3notice-info = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 512 512\u0026#34; fill=\u0026#34;hsl(30, 80%, 70%)\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M256 8a248 248 0 100 496 248 248 0 000-496zm0 110a42 42 0 110 84 42 42 0 010-84zm56 254c0 7-5 12-12 12h-88c-7 0-12-5-12-12v-24c0-7 5-12 12-12h12v-64h-12c-7 0-12-5-12-12v-24c0-7 5-12 12-12h64c7 0 12 5 12 12v100h12c7 0 12 5 12 12v24z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 4notice-note = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 512 512\u0026#34; fill=\u0026#34;hsl(200, 65%, 65%)\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M504 256a248 248 0 11-496 0 248 248 0 01496 0zm-248 50a46 46 0 100 92 46 46 0 000-92zm-44-165l8 136c0 6 5 11 12 11h48c7 0 12-5 12-11l8-136c0-7-5-13-12-13h-64c-7 0-12 6-12 13z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 5notice-tip = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 512 512\u0026#34; fill=\u0026#34;hsl(140, 65%, 65%)\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M504 256a248 248 0 11-496 0 248 248 0 01496 0zM227 387l184-184c7-6 7-16 0-22l-22-23c-7-6-17-6-23 0L216 308l-70-70c-6-6-16-6-23 0l-22 23c-7 6-7 16 0 22l104 104c6 7 16 7 22 0z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 使用： 1{{\u0026lt; notice notice-warning \u0026gt;}} 2Warning 3{{\u0026lt; /notice \u0026gt;}} 时间轴 2024 past First Time 我是萌新 2025 now Second Time 我还是萌新 新建 timeline.html，内容为： 1{{- $date := .Get \u0026#34;date\u0026#34; -}} {{- $title := .Get \u0026#34;title\u0026#34; -}} {{- $description := .Get \u0026#34;description\u0026#34; -}} {{- $tags := .Get \u0026#34;tags\u0026#34; -}} 2 3\u0026lt;div class=\u0026#34;timeline__row\u0026#34;\u0026gt; 4 \u0026lt;div class=\u0026#34;timeline__time\u0026#34;\u0026gt; 5 \u0026lt;div class=\u0026#34;timeline__time\u0026#34;\u0026gt;{{ $date }}\u0026lt;/div\u0026gt; 6 \u0026lt;div class=\u0026#34;timeline__split-line\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 7 \u0026lt;/div\u0026gt; 8 \u0026lt;div class=\u0026#34;timeline__content\u0026#34;\u0026gt; 9 \u0026lt;div class=\u0026#34;timeline__tags\u0026#34;\u0026gt; 10 {{- with split $tags \u0026#34;, \u0026#34; -}} {{- range . }}{{- if eq . \u0026#34;样式\u0026#34; }} 11 \u0026lt;span class=\u0026#34;timeline__tag timeline__tag-style\u0026#34;\u0026gt;{{ . }}\u0026lt;/span\u0026gt; {{- else if eq . \u0026#34;文章\u0026#34; }} 12 \u0026lt;span class=\u0026#34;timeline__tag timeline__tag-article\u0026#34;\u0026gt;{{ . }}\u0026lt;/span\u0026gt; {{- else if eq . \u0026#34;页面\u0026#34; }} 13 \u0026lt;span class=\u0026#34;timeline__tag timeline__tag-page\u0026#34;\u0026gt;{{ . }}\u0026lt;/span\u0026gt; {{- else }} 14 \u0026lt;span class=\u0026#34;timeline__tag\u0026#34;\u0026gt;{{ . }}\u0026lt;/span\u0026gt; {{- end }} {{- end }} {{- end }} 15 \u0026lt;/div\u0026gt; 16 \u0026lt;div class=\u0026#34;timeline__title\u0026#34;\u0026gt;{{ $title }}\u0026lt;/div\u0026gt; 17 \u0026lt;div class=\u0026#34;timeline__description\u0026#34;\u0026gt; 18 {{ $description }} 19 \u0026lt;/div\u0026gt; 20 \u0026lt;/div\u0026gt; 21\u0026lt;/div\u0026gt; 22 23\u0026lt;style\u0026gt; 24 .timeline { 25 display: flex; 26 flex-direction: column; 27 } 28 29 .timeline__row { 30 display: flex; 31 padding-left: 4%; 32 height: 90px; 33 } 34 35 .timeline__time { 36 flex: 0 0 110px; 37 color: #5d5d5d; 38 font-size: 17px; 39 text-transform: uppercase; 40 position: relative; 41 display: flex; 42 flex-direction: column; 43 align-items: center; 44 padding: 0.5rem 0; 45 } 46 47 .timeline__time-text { 48 margin: 0; 49 } 50 51 .timeline__split-line { 52 position: absolute; 53 top: 0.5rem; 54 right: -20px; 55 height: 100%; 56 width: 2px; 57 background-color: #84c4e240; 58 z-index: 0; 59 } 60 61 .timeline__split-line:before { 62 content: \u0026#34;\u0026#34;; 63 position: absolute; 64 top: 24%; 65 right: -4px; 66 transform: translateY(-50%); 67 width: 10px; 68 height: 10px; 69 background-color: #c9e5f2; 70 box-shadow: 0 0 0 4px var(--theme); 71 border-radius: 50%; 72 border: 0px solid #84c4e2; 73 z-index: -1; 74 } 75 76 .timeline__content { 77 flex: 1; 78 margin-left: 4.5rem; 79 padding: 0.5rem 0 1.2rem 0; 80 } 81 82 .timeline__title { 83 margin: 0; 84 margin-bottom: 2px; 85 padding-top: 3px; 86 margin-left: 0.5rem; 87 color: var(--content); 88 font-family: var(--font-family-teshu); 89 font-size: 19px; 90 font-weight: 600; 91 width: fit-content; 92 display: inline-block; 93 vertical-align: middle; 94 /* 垂直居中对齐 */ 95 } 96 97 .timeline__tags { 98 display: inline-block; 99 padding: 0; 100 margin-left: 0.3rem; 101 align-items: center; 102 gap: 0.3rem; 103 } 104 105 .timeline__tag { 106 display: inline-block; 107 color: var(--secondary); 108 background-color: #84c4e230; 109 border: 1.5px solid #84c4e230; 110 border-radius: 999px; 111 padding: 0rem 0.5rem; 112 font-size: 12px; 113 white-space: nowrap; 114 line-height: 1.4rem; 115 opacity: 0.8; 116 vertical-align: middle; 117 /* 垂直居中对齐 */ 118 } 119 120 .timeline__description { 121 font-size: 15px; 122 line-height: 1.6; 123 color: #5d5d5d; 124 overflow: hidden; 125 text-overflow: ellipsis; 126 margin: 0.5rem 0 0.4rem 1.5rem; 127 /* 添加 1.5rem 的左侧内边距 */ 128 } 129 /* 为类名为 \u0026#34;timeline__tag-style\u0026#34; 的标签定义颜色 */ 130 131 .timeline__tag-style { 132 background-color: #c581da; 133 /* 替换为你希望的颜色 */ 134 border-color: #c581da; 135 /* 与背景色相同或不同，根据需要自定义 */ 136 color: #FFFFFF; 137 /* 根据需要选择文本颜色 */ 138 } 139 /* 为类名为 \u0026#34;timeline__tag-article\u0026#34; 的标签定义颜色 */ 140 141 .timeline__tag-article { 142 background-color: #92d392; 143 /* 替换为你希望的颜色 */ 144 border-color: #92d392; 145 /* 与背景色相同或不同，根据需要自定义 */ 146 color: #000000; 147 /* 根据需要选择文本颜色 */ 148 } 149 /* 为类名为 \u0026#34;timeline__tag-page\u0026#34; 的标签定义颜色 */ 150 151 .timeline__tag-page { 152 background-color: #707070; 153 /* 替换为你希望的颜色 */ 154 border-color: #707070; 155 /* 与背景色相同或不同，根据需要自定义 */ 156 color: #FFFFFF; 157 /* 根据需要选择文本颜色 */ 158 } 159 160 @media screen and (max-width: 768px) { 161 .timeline__time { 162 font-size: 14px; 163 /* 在小屏幕上使用较小的字体大小 */ 164 } 165 .timeline__title { 166 font-size: 16px; 167 /* 在小屏幕上使用较小的字体大小 */ 168 } 169 .timeline__description { 170 font-size: 14px; 171 /* 在小屏幕上使用较小的字体大小 */ 172 } 173 } 174\u0026lt;/style\u0026gt; 使用： 1{{\u0026lt; timeline date=\u0026#34;2024\u0026#34; title=\u0026#34;First Time\u0026#34; description=\u0026#34;我是萌新\u0026#34; tags=\u0026#34;past\u0026#34; \u0026gt;}}{{\u0026lt; timeline date=\u0026#34;2025\u0026#34; title=\u0026#34;Second Time\u0026#34; description=\u0026#34;我还是萌新\u0026#34; tags=\u0026#34;now\u0026#34; \u0026gt;}} 图片轮播 新建 imgloop.html，内容为： 1{{ if .Site.Params.enableimgloop }} 2 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/css/swiper.min.css\u0026#34;\u0026gt; 3 \u0026lt;!-- Swiper --\u0026gt; 4 \u0026lt;div class=\u0026#34;swiper-container\u0026#34;\u0026gt; 5 \u0026lt;div class=\u0026#34;swiper-wrapper\u0026#34;\u0026gt; 6 {{$itItems := split (.Get 0) \u0026#34;,\u0026#34;}} 7 {{range $itItems }} 8 \u0026lt;div class=\u0026#34;swiper-slide\u0026#34;\u0026gt; 9 \u0026lt;img src=\u0026#34;{{.}}\u0026#34; alt=\u0026#34;\u0026#34;\u0026gt; 10 \u0026lt;/div\u0026gt; 11 {{end}} 12 \u0026lt;/div\u0026gt; 13 \u0026lt;!-- Add Pagination --\u0026gt; 14 \u0026lt;div class=\u0026#34;swiper-pagination\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 15 \u0026lt;/div\u0026gt; 16 17 \u0026lt;script src=\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/js/swiper.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 18 \u0026lt;!-- Initialize Swiper --\u0026gt; 19 \u0026lt;script\u0026gt; 20 var swiper = new Swiper(\u0026#39;.swiper-container\u0026#39;, { 21 pagination: \u0026#39;.swiper-pagination\u0026#39;, 22 paginationClickable: true, 23 //自动调节高度 24 autoHeight: true, 25 //键盘左右方向键控制 26 keyboardControl : true, 27 //鼠标滑轮控制 28 mousewheelControl : true, 29 //自动切换 30 //autoplay : 5000, 31 //懒加载 32 lazyLoading : true, 33\tlazyLoadingInPrevNext : true, 34\t//无限循环 35\tloop : true, 36 }); 37 \u0026lt;/script\u0026gt; 38{{ end }} 在 custom.scss 中添加： 1//图片轮播 2.swiper-container { 3 max-width: 820px; 4 margin: 2em auto; 5} 6.swiper-slide { 7 text-align: center; 8 font-size: 18px; 9 background-color: #fff; 10 /* Center slide text vertically */ 11 display: flex; 12 justify-content: center; 13 align-items: center; 14 img { 15 margin: 0 !important; 16 } 17} 在 config.yaml 找到 params:，添加 enableimgloop: true。\n使用：\n1{{\u0026lt; imgloop \u0026#34;1.jpg,2.jpg,3.jpg\u0026#34; \u0026gt;}} 瀑布流相册 新建 gallery.html，内容为： 1{{ $baseURL := .Site.BaseURL }} 2{{- with (.Get 0) -}} 3{{- $files := readDir (print \u0026#34;/static/\u0026#34; .) }} 4\u0026lt;div class=\u0026#34;gallery-photos\u0026#34;\u0026gt; 5 {{- range (sort $files \u0026#34;Name\u0026#34; \u0026#34;asc\u0026#34;) -}} 6 {{- if ( .Name | findRE \u0026#34;\\\\.(gif|jpg|jpeg|tiff|png|bmp|webp|avif|jxl)\u0026#34;) }} 7 {{- $linkURL := print $baseURL \u0026#34;/\u0026#34; ($.Get 0) \u0026#34;/\u0026#34; .Name | absURL }} 8 \u0026lt;div class=\u0026#34;gallery-photo\u0026#34;\u0026gt; 9 \u0026lt;img class=\u0026#34;photo-img\u0026#34; loading=\u0026#39;lazy\u0026#39; decoding=\u0026#34;async\u0026#34; src=\u0026#34;{{ $linkURL }}\u0026#34; alt=\u0026#34;{{ .Name }}\u0026#34; /\u0026gt; 10 \u0026lt;span class=\u0026#34;photo-title\u0026#34;\u0026gt;{{ .Name | replaceRE \u0026#34;\\\\..*\u0026#34; \u0026#34;\u0026#34;}}\u0026lt;/span\u0026gt; 11 \u0026lt;/div\u0026gt; 12 {{- end }} 13 {{- end }} 14\u0026lt;/div\u0026gt; 15{{- end }} 在 layouts/partials/footer/custom.html 中插入： 1\u0026lt;!-- 瀑布流相册 --\u0026gt; 2\u0026lt;script src=\u0026#34;https://immmmm.com/waterfall.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 3\u0026lt;script src=\u0026#34;https://immmmm.com/imgStatus.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 4\u0026lt;script\u0026gt; 5document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, () =\u0026gt; { 6 //外链 gallery 标签相册瀑布流 7 var photosAll = document.getElementsByTagName(\u0026#39;gallery\u0026#39;) || \u0026#39;\u0026#39;; 8 if(photosAll){ 9 for(var i=0;i \u0026lt; photosAll.length;i++){ 10 photosAll[i].innerHTML = \u0026#39;\u0026lt;div class=\u0026#34;gallery-photos\u0026#34;\u0026gt;\u0026#39;+photosAll[i].innerHTML+\u0026#39;\u0026lt;/div\u0026gt;\u0026#39; 11 var photosIMG = photosAll[i].getElementsByTagName(\u0026#39;img\u0026#39;) 12 for(var j=0;j \u0026lt; photosIMG.length;j++){ 13 wrap(photosIMG[j], document.createElement(\u0026#39;div\u0026#39;)); 14 } 15 } 16 } 17 function wrap(el, wrapper) { 18 wrapper.className = \u0026#34;gallery-photo\u0026#34;; 19 el.parentNode.insertBefore(wrapper, el); 20 wrapper.appendChild(el); 21 } 22 //相册瀑布流 23 let galleryPhotos = document.querySelectorAll(\u0026#39;.gallery-photos\u0026#39;) || \u0026#39;\u0026#39; 24 if(galleryPhotos){ 25 imgStatus.watch(\u0026#39;.gallery-photo img\u0026#39;, function(imgs) { 26 if(imgs.isDone()){ 27 for(var i=0;i \u0026lt; galleryPhotos.length;i++){ 28 waterfall(galleryPhotos[i]); 29 let pagePhoto = galleryPhotos[i].querySelectorAll(\u0026#39;.gallery-photo\u0026#39;); 30 for(var j=0;j \u0026lt; pagePhoto.length;j++){pagePhoto[j].className += \u0026#34; visible\u0026#34;}; 31 } 32 } 33 }); 34 window.addEventListener(\u0026#39;resize\u0026#39;, function () { 35 for(var i=0;i \u0026lt; galleryPhotos.length;i++){ 36 waterfall(galleryPhotos[i]); 37 } 38 }); 39 } 40}); 41\u0026lt;/script\u0026gt; 在 layouts/partials/head/custom.html 中插入： 1\u0026lt;style\u0026gt; 2 .gallery-photos{width:100%;break-inside: avoid;margin-bottom: 10px;} 3 .gallery-photo{width:33.3%;position: relative;padding:0px 4px 0px 4px;visibility: hidden;overflow: hidden;} 4 .gallery-photo.visible{visibility: visible;} 5 .gallery-photo img{width: 100%;height: 100%;object-fit: cover;border-radius: 5px;box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);} 6 @media screen and (min-width: 1200x) { 7 .gallery-photo{ 8 width: 100%; 9 columns: 3; 10 column-gap: 20px; 11 } 12 } 13 @media screen and (min-width: 860px) and (max-width: 1323.98px) { 14 .gallery-photo{ 15 width: 100%; 16 columns: 2; 17 column-gap: 20px;} 18 } 19 @media screen and (max-width: 860px) { 20 .gallery-photo{ 21 width: 100%; 22 columns: 1;} 23 } 24 @media (max-width: 767.98px){ 25 .photo-time{display: none;} 26 } 27\u0026lt;/style\u0026gt; 使用： 1\u0026lt;gallery\u0026gt;![](https://www.sleepymoon.cyou/2023/hugo-shortcodes/2.jpg)![](https://www.sleepymoon.cyou/2023/hugo-shortcodes/1.jpg)\u0026lt;/gallery\u0026gt; PDF/PPT插入 嵌入PDF 新建 pdf.html，内容为： 1\u0026lt;div class=\u0026#34;pdf\u0026#34; style=\u0026#34; 2 padding-bottom: 66%; 3 position: relative; 4 display: block; 5 width: 100%; 6 border-bottom: 5px solid; 7\u0026#34;\u0026gt; 8 \u0026lt;iframe 9 width=\u0026#34;100%\u0026#34; 10 height=\u0026#34;100%\u0026#34; 11 src=\u0026#34;{{ .Get \u0026#34;src\u0026#34; }}\u0026#34; 12 frameborder=\u0026#34;0\u0026#34; 13 allowfullscreen=\u0026#34;\u0026#34; 14 style=\u0026#34; 15 position: absolute; 16 top: 0; 17 left: 0\u0026#34; \u0026gt; 18 \u0026lt;/iframe\u0026gt; 19\u0026lt;/div\u0026gt; 使用： 1{{\u0026lt; pdf src=\u0026#34;https://www.blatchingtonmill.org.uk/assets/Uploads/All-Of-Me-Sheet-Music-John-Legend-All-Of-Me-Piano-Sheet-Music-Medium-Vocals-Piano-Guitar.pdf\u0026#34; \u0026gt;}} 手动输入NeoDB条目 《八度空间》 [台] 周杰伦 \u0026lt; 9.2 \u003e 歌曲可以拥有与电影媲美的叙事性吗？在短短四分钟左右的歌曲里，43 分钟的专辑《八度空间》中，周杰伦用旺盛的创作力和狂想尝试构建出这样的“空间”。“八度空间”意指西方的八度音阶，在看似局限的音乐空间中，周杰伦用他平时从电影和 MV 得来的灵感，给每首歌曲赋予了情节，打造没有维度限制的音乐异想世界。 Album 新建 book.html，内容为： 1\u0026lt;div class=\u0026#34;db-card\u0026#34;\u0026gt; 2 \u0026lt;div class=\u0026#34;db-card-subject\u0026#34;\u0026gt; 3 \u0026lt;div class=\u0026#34;db-card-post\u0026#34;\u0026gt;\u0026lt;img loading=\u0026#34;lazy\u0026#34; decoding=\u0026#34;async\u0026#34; referrerpolicy=\u0026#34;no-referrer\u0026#34; src=\u0026#34;{{ .Get \u0026#34;image\u0026#34; }}\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 4 \u0026lt;div class=\u0026#34;db-card-content\u0026#34;\u0026gt; 5 \u0026lt;div class=\u0026#34;db-card-title\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;{{ .Get \u0026#34;url\u0026#34; }}\u0026#34; class=\u0026#34;cute\u0026#34; target=\u0026#34;_blank\u0026#34; rel=\u0026#34;noreferrer\u0026#34;\u0026gt;{{ .Get \u0026#34;title\u0026#34; }}\u0026lt;/a\u0026gt;\u0026lt;/div\u0026gt; 6 \u0026lt;div class=\u0026#34;rating\u0026#34;\u0026gt; 7 \u0026lt;span class=\u0026#34;rating_nums\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;span class=\u0026#34;allstardark\u0026#34;\u0026gt;\u0026lt;span class=\u0026#34;allstarlight\u0026#34; style=\u0026#34;width:{{ .Get \u0026#34;rate\u0026#34; }}0%\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;span class=\u0026#34;rating_float\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;/span\u0026gt; 8 \u0026lt;span class=\u0026#34;rating_float\u0026#34;\u0026gt;\u0026lt; {{ .Get \u0026#34;rate_float\u0026#34; }} \u0026gt;\u0026lt;/span\u0026gt; 9 \u0026lt;/div\u0026gt; 10 \u0026lt;div class=\u0026#34;db-card-abstract\u0026#34;\u0026gt;{{ .Get \u0026#34;brief\u0026#34; }}\u0026lt;/div\u0026gt; 11 \u0026lt;/div\u0026gt; 12 \u0026lt;div class=\u0026#34;db-card-cate\u0026#34;\u0026gt;{{ .Get \u0026#34;tag\u0026#34; }}\u0026lt;/div\u0026gt; 13 \u0026lt;/div\u0026gt; 14\u0026lt;/div\u0026gt; 在 custom.scss 中添加： 1/* db-card -------- start*/ 2.db-card{margin:2.5rem 3rem;background:var(--card-background);border-radius: 7px;box-shadow: 0 6px 10px 0 #00000053;margin-left:auto;margin-right:auto;} 3.db-card-subject{display: flex;align-items:flex-start;line-height:1.6;padding:12px;position:relative;} 4.dark .db-card{background:var(--card-background);} 5.db-card-content {flex:1 1 auto;} 6.db-card-post {width: 100px;margin-right: 15px;display: flex;flex: 0 0 auto;} 7.db-card-title {margin-bottom: 3px;font-size: 14px;color: var(--card-text-color-main);;} 8.db-card-title a{text-decoration: none!important} 9.db-card-abstract,.db-card-comment{font-size:13px;overflow: auto;max-height:10rem;color: var(--card-text-color-main);;} 10.db-card-cate{position: absolute;top:0;right:0;background:#f99b01;padding:1px 8px;font-size:small;font-style:italic;border-radius:0 8px 0 8px;text-transform:capitalize;} 11.db-card-post img{width: 100px!important;height: 150px!important;border-radius: 4px;-o-object-fit: cover;object-fit: cover;} 12.rating{margin: 0 0 5px;font-size:13px;line-height: 1;display: flex;align-items: center;} 13.rating .allstardark{position:relative;color: #f99b01;height: 16px;width: 80px;background-size: auto 100%;margin-right: 8px;background-repeat: repeat;background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==); 14} 15.rating .allstarlight{position: absolute;left: 0;color: #f99b01;height:16px;overflow: hidden;background-size: auto 100%;background-repeat: repeat;background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);} 16@media (max-width:550px) { 17\t.db-card{margin:0.8rem 1rem;} 18\t.db-card-comment{display: none;} 19} 20/* db-card -------- end */ 使用： 1{{\u0026lt; book url=\u0026#34;#\u0026#34; image=\u0026#34;1.png\u0026#34; title=\u0026#34;《JAY》 [台] 周杰倫\u0026#34; rate=\u0026#34;9\u0026#34; rate_float=\u0026#34;9.2\u0026#34; brief=\u0026#34;text\u0026#34; tag=\u0026#34;Album\u0026#34; \u0026gt;}} 居中引用 人生代代无穷已，江月年年望相似。\n不知江月待何人，但见长江送流水。\n新建 quote-center.html，内容为： 1\u0026lt;div class=\u0026#34;quote-center\u0026#34;\u0026gt; 2 {{- $content := .Inner | markdownify -}} 3 {{- if not (strings.HasPrefix $content \u0026#34;\u0026lt;p\u0026gt;\u0026#34;) }} 4 {{ printf `\u0026lt;/p\u0026gt;\u0026lt;p\u0026gt;%s\u0026lt;/p\u0026gt;` $content | safeHTML }} 5 {{- else }} 6 {{- $content }} 7 {{- end -}} 8\u0026lt;/div\u0026gt; 在 custom.scss 中添加： 1.quote-center { 2 background-color:#84c4e230!important; 3 position: relative; 4 border-left: none; 5 padding-left: 0; 6 padding: 20px!important; 7 border-radius: 5px!important; 8 border-top: 0px solid var(--color-markdown-blockquote-border); 9 border-bottom: 0px solid var(--color-markdown-blockquote-border); 10 p { 11 text-align: center !important; 12 margin-top: 1.5em!important; 13 margin-bottom: 1.5em!important; 14 font-weight: bold; 15 } 16 \u0026amp;::before { 17 position: absolute; 18 left: 0; 19 content: \u0026#39;“---\u0026#39;; 20 color: #8aa2d3; 21 font-size: 2.5em; 22 font-weight: normal; 23 margin-top: -5%; 24 margin-left: 1%; 25 height: 90%; 26 } 27 \u0026amp;::after { 28 position: absolute; 29 right: 0; 30 content: \u0026#39;---”\u0026#39;; 31 color: #8aa2d3; 32 font-size: 2.5em; 33 font-weight: normal; 34 margin-top: -6%; 35 margin-right: 1%; 36 height: 90%; 37 } 38} 使用： 1{{\u0026lt; quote-center \u0026gt;}} 2人生代代无穷已，江月年年望相似。\u0026lt;br\u0026gt;不知江月待何人，但见长江送流水。 3{{\u0026lt; /quote-center \u0026gt;}} 古书排版 沧海一声笑\n滔滔两岸潮\n浮 沉 随 浪\n只记今朝\n苍天笑\n纷纷世上潮\n谁负谁胜出\n天知晓\n江山笑\n烟雨遥\n涛浪淘尽红尘俗世\n几多娇\n清风笑\n竟惹寂寥\n豪情还剩了一襟晚照\n苍生笑\n不再寂寥\n豪情仍在痴痴笑笑 新建 verticaltext.html，内容为： 1\u0026lt;style\u0026gt; 2.mainvlr { 3 margin:20px; 4 font-size: 20px; 5 letter-spacing: 2px; 6} 7 8.vlr{ 9 writing-mode: vertical-lr; 10} 11 12.vrl{ 13 writing-mode: vertical-rl; 14 unicode-bidi: bidi-override; 15 border: 1px solid; 16 padding: 5px; 17 margin: 5px; 18 margin-right: 5px; 19 word-wrap: break-word; 20 overflow-y:auto; 21 height: {{ .Get \u0026#34;height\u0026#34; | default \u0026#34;500\u0026#34; }}px; 22 width: 100%; 23} 24\u0026lt;/style\u0026gt; 25 26\u0026lt;div class=\u0026#34;vrl\u0026#34; id=\u0026#34;vrlmain\u0026#34;\u0026gt; 27 28\u0026lt;div class=\u0026#34;mainvlr\u0026#34;\u0026gt; 29 30{{ .Inner | markdownify }} 31 32\u0026lt;/div\u0026gt; 33 34\u0026lt;/div\u0026gt; 35 36\u0026lt;script\u0026gt; 37window.scrollTo(document.body.scrollWidth - 500, 0); 38\u0026lt;/script\u0026gt; 使用：（默认高度为500px，可以自定义） 1{{\u0026lt; verticaltext height=\u0026#34;300px\u0026#34; \u0026gt;}} 2沧海一声笑 3滔滔两岸潮 4{{\u0026lt; /verticaltext \u0026gt;}} 对话样式 John Doe\u0026nbsp;\u0026nbsp;\u0026nbsp;2023-09-12 14:30 这是左边的消息内容。 2023-09-12 14:45\u0026nbsp;\u0026nbsp;\u0026nbsp;Alice 这是右边的消息内容。你好呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀呀 新建 chat.html，内容为： 1{{ if eq (.Get \u0026#34;position\u0026#34;) \u0026#34;left\u0026#34; }} 2\u0026lt;div class=\u0026#34;chat --other\u0026#34;\u0026gt; 3 \u0026lt;div class=\u0026#34;chat__inner\u0026#34;\u0026gt; 4 \u0026lt;div class=\u0026#34;chat__meta\u0026#34;\u0026gt;{{ .Get \u0026#34;name\u0026#34; }}\u0026amp;nbsp;\u0026amp;nbsp;\u0026amp;nbsp;{{ .Get \u0026#34;timestamp\u0026#34; }}\u0026lt;/div\u0026gt; 5 \u0026lt;div class=\u0026#34;chat__text\u0026#34;\u0026gt; 6 {{ .Inner }} 7 \u0026lt;/div\u0026gt; 8 \u0026lt;/div\u0026gt; 9\u0026lt;/div\u0026gt; 10{{ else if eq (.Get \u0026#34;position\u0026#34;) \u0026#34;right\u0026#34; }} 11\u0026lt;div class=\u0026#34;chat --self\u0026#34;\u0026gt; 12 \u0026lt;div class=\u0026#34;chat__inner\u0026#34;\u0026gt; 13 \u0026lt;div class=\u0026#34;chat__meta\u0026#34; style=\u0026#34;text-align: right;\u0026#34;\u0026gt;{{ .Get \u0026#34;timestamp\u0026#34; }}\u0026amp;nbsp;\u0026amp;nbsp;\u0026amp;nbsp;{{ .Get \u0026#34;name\u0026#34; }}\u0026lt;/div\u0026gt; 14 \u0026lt;div class=\u0026#34;chat__text\u0026#34;\u0026gt; 15 {{ .Inner }} 16 \u0026lt;/div\u0026gt; 17 \u0026lt;/div\u0026gt; 18\u0026lt;/div\u0026gt; 19{{ end }} 20 21\u0026lt;style\u0026gt; 22 .chat { 23 margin: 10px; 24 padding: 10px; 25 position: relative; 26 /* 添加相对定位，以便定位尖角箭头 */ 27 transition: transform 0.2s; 28 /* 添加过渡效果，使放大平滑 */ 29 max-width: 80%; 30 min-width: 15%; 31 } 32 33 .chat:hover { 34 transform: scale(1.05); 35 } 36 37 .chat.--self { 38 text-align: left; 39 background-color: #ecf5ff; 40 color: #000000; 41 border-radius: 15px; 42 width: fit-content; 43 margin-left: auto; 44 } 45 /* 尖角箭头 */ 46 47 .chat.--self::before { 48 content: \u0026#34;\u0026#34;; 49 position: absolute; 50 right: -18px; 51 /* 调整箭头位置 */ 52 bottom: 5px; 53 transform: translateY(-50%); 54 border-width: 15px 0 0 20px; 55 border-style: solid; 56 border-color: transparent transparent transparent #ecf5ff; 57 /* 箭头颜色与对话框背景颜色一致 */ 58 } 59 /* 左边对话框样式 */ 60 61 .chat.--other { 62 text-align: left; 63 background-color: #ffecec; 64 color: #333; 65 border-radius: 15px; 66 position: relative; 67 width: fit-content; 68 } 69 /* 左边对话框的尖角箭头 */ 70 71 .chat.--other::before { 72 content: \u0026#34;\u0026#34;; 73 position: absolute; 74 left: -18px; 75 bottom: 5px; 76 transform: translateY(-50%); 77 border-width: 15px 20px 0 0; 78 border-style: solid; 79 border-color: transparent #ffecec transparent transparent; 80 } 81 /* 消息元数据样式（名称和时间戳） */ 82 83 .chat__meta { 84 font-weight: bold; 85 font-size: 0.67em; 86 color: #707070; 87 margin-bottom: 5px; 88 } 89 /* 消息文本样式 */ 90 91 .chat__text { 92 font-size: 0.9em; 93 margin-left: 10px; 94 word-break: break-all; 95 } 96 97 [data-scheme=\u0026#34;dark\u0026#34;] { 98 .chat.--self { 99 color: #fefefe; 100 background-color: #253958; 101 } 102 .chat.--self::before { 103 border-color: transparent transparent transparent #253958; 104 } 105 .chat.--other { 106 color: #fefefe; 107 background-color: #1a1a1a; 108 } 109 .chat.--other::before { 110 border-color: transparent #1a1a1a transparent transparent; 111 } 112 .chat__meta { 113 color: #b1b1b1; 114 } 115 } 116\u0026lt;/style\u0026gt; 使用： 1{{\u0026lt; chat position=\u0026#34;left\u0026#34; name=\u0026#34;John Doe\u0026#34; timestamp=\u0026#34;2023-09-12 14:30\u0026#34; \u0026gt;}}这是左边的消息内容。{{\u0026lt; /chat \u0026gt;}} 2{{\u0026lt; chat position=\u0026#34;right\u0026#34; name=\u0026#34;Alice\u0026#34; timestamp=\u0026#34;2023-09-12 14:45\u0026#34; \u0026gt;}}这是右边的消息内容。{{\u0026lt; /chat \u0026gt;}} 脚注 第一层引用1\n第二层引用2\n第三层引用3\n单独引用2\n新建 layouts/shortcodes/inner.html，内容为： 1{{ .Inner }} 新建 layouts/shortcodes/outer.html，内容为： 1{{ .Inner }} 使用：（outer 是外层引用或用作单独脚注，inner仅用作内部嵌套的脚注） 1{{% outer %}} 2footnote[^1] 3{{\u0026lt; inner \u0026gt;}} 4footnote[^2] 5{{\u0026lt; /inner \u0026gt;}} 6{{% /outer %}} 7 8... 9... 10 11[^1]: footnote 1 12[^2]: footnote 2 简化版引用 十里青山远，潮平路带沙。数声啼鸟怨年华。又是凄凉时候，在天涯。白露收残月，清风散晓霞。绿杨堤畔问荷花。记得年时沽酒，那人家。\n十里青山远，潮平路带沙。数声啼鸟怨年华。又是凄凉时候，在天涯。白露收残月，清风散晓霞。绿杨堤畔问荷花。记得年时沽酒，那人家。\n十里青山远，潮平路带沙。数声啼鸟怨年华。又是凄凉时候，在天涯。白露收残月，清风散晓霞。绿杨堤畔问荷花。记得年时沽酒，那人家。\n十里青山远，潮平路带沙。数声啼鸟怨年华。又是凄凉时候，在天涯。白露收残月，清风散晓霞。绿杨堤畔问荷花。记得年时沽酒，那人家。\n新建 simple-notice.html，内容为： 1\u0026lt;!-- 文件位置：~/layouts/shortcodes/notice.html --\u0026gt; 2 3\u0026lt;!--https://github.com/martignoni/hugo-notice--\u0026gt; 4{{- $noticeType := .Get 0 -}} 5 6{{- $raw := (markdownify .Inner | chomp) -}} 7 8{{- $block := findRE \u0026#34;(?is)^\u0026lt;(?:address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h(?:1|2|3|4|5|6)|header|hgroup|hr|li|main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video)\\\\b\u0026#34; $raw 1 -}} 9 10{{ $icon := (replace (index $.Site.Data.SVG $noticeType) \u0026#34;icon\u0026#34; \u0026#34;icon simple-notice-icon\u0026#34;) }} 11\u0026lt;div class=\u0026#34;simple-notice {{ $noticeType }}\u0026#34; {{ if len .Params | eq 2 }} id=\u0026#34;{{ .Get 1 }}\u0026#34; {{ end }}\u0026gt; 12 \u0026lt;div class=\u0026#34;simple-notice-title\u0026#34;\u0026gt;{{ $icon | safeHTML }}\u0026lt;/div\u0026gt; 13 {{- if or $block (not $raw) }}{{ $raw }}{{ else }}\u0026lt;p\u0026gt;{{ $raw }}\u0026lt;/p\u0026gt;{{ end -}} 14\u0026lt;/div\u0026gt; 在 custom.scss 中添加： 1// 文件位置：~/assets/scss/custom.scss 2 3.simple-notice { 4 position:relative; 5 padding: 1em 0 1em 2em; 6 margin-bottom: 1em; 7 transition: all .5s; 8 p:last-child { 9 margin-bottom: 0; 10 } 11 .simple-notice-title { 12 position: absolute; 13 left: 0.8em; 14 .simple-notice-icon { 15 width: 1em; 16 height: 1em; 17 margin-left: -0.8em; 18 } 19 } 20 \u0026amp;.simple-notice-warning { 21 border-top: 2px solid hsl(0, 100%, 35%); 22 color: hsl(0, 100%, 35%); 23 .simple-notice-title { 24 color: hsl(0, 100%, 35%); 25 } 26 a { 27 color: hsl(0, 100%, 35%); 28 text-decoration-color: hsl(0, 100%, 35%); 29 } 30 } 31 \u0026amp;.simple-notice-info { 32 border-top: 2px solid hsl(40, 80%, 45%); 33 color: hsl(40, 80%, 45%); 34 .simple-notice-title { 35 color: hsl(40, 80%, 45%); 36 } 37 a { 38 color: hsl(40, 80%, 45%); 39 text-decoration-color: hsl(40, 80%, 45%); 40 } 41 } 42 \u0026amp;.simple-notice-note { 43 border-top: 2px solid hsl(210, 100%, 25%); 44 color: hsl(210, 100%, 25%); 45 .simple-notice-title { 46 color: hsl(210, 100%, 25%); 47 } 48 a { 49 color: hsl(210, 100%, 25%); 50 text-decoration-color: hsl(210, 100%, 25%); 51 } 52 } 53 \u0026amp;.simple-notice-tip { 54 border-top: 2px solid hsl(150, 100%, 25%); 55 color: hsl(150, 100%, 25%); 56 .simple-notice-title { 57 color: hsl(150, 100%, 25%); 58 } 59 a { 60 color: hsl(150, 100%, 25%); 61 text-decoration-color: hsl(150, 100%, 25%); 62 } 63 } 64} 65 66[data-theme=\u0026#34;dark\u0026#34;] .simple-notice { 67 \u0026amp;.simple-notice-warning { 68 border-top: 2px solid hsl(0, 65%, 65%); 69 color: hsl(0, 65%, 65%); 70 .simple-notice-title { 71 color: hsl(0, 65%, 65%); 72 } 73 a { 74 color: hsl(0, 65%, 65%); 75 text-decoration-color: hsl(0, 65%, 65%); 76 } 77 } 78 \u0026amp;.simple-notice-info { 79 border-top: 2px solid hsl(30, 80%, 70%); 80 color: hsl(30, 80%, 70%); 81 .simple-notice-title { 82 color: hsl(30, 80%, 70%); 83 } 84 a { 85 color: hsl(30, 80%, 70%); 86 text-decoration-color: hsl(30, 80%, 70%); 87 } 88 } 89 \u0026amp;.simple-notice-note { 90 border-top: 2px solid hsl(200, 65%, 65%); 91 color: hsl(200, 65%, 65%); 92 .simple-notice-title { 93 color: hsl(200, 65%, 65%); 94 } 95 a { 96 color: hsl(200, 65%, 65%); 97 text-decoration-color: hsl(200, 65%, 65%); 98 } 99 } 100 \u0026amp;.simple-notice-tip { 101 border-top: 2px solid hsl(140, 65%, 65%); 102 color: hsl(140, 65%, 65%); 103 .simple-notice-title { 104 color: hsl(140, 65%, 65%); 105 } 106 a { 107 color: hsl(140, 65%, 65%); 108 text-decoration-color: hsl(140, 65%, 65%); 109 } 110 } 111} 在 data/SVG.toml 中插入图标： 1# 文件位置：~/data/SVG.toml 2 3# Notice Icon 4simple-notice-warning = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 512 512\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M114.57 76.07a45.71 45.71 0 00-67.51-6.41c-17.58 16.18-19 43.52-4.75 62.77l91.78 123-92.33 124.15c-14.23 19.25-13.11 46.59 4.74 62.77a45.71 45.71 0 0067.5-6.41L242.89 262.7a12.14 12.14 0 000-14.23zm355.67 303.51l-92.33-124.13 91.78-123c14.22-19.25 12.83-46.59-4.75-62.77a45.71 45.71 0 00-67.51 6.41l-128 172.12a12.14 12.14 0 000 14.23L398 435.94a45.71 45.71 0 0067.51 6.41c17.84-16.18 18.96-43.52 4.73-62.77z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 5simple-notice-note = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 192 512\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M176 432c0 44.11-35.89 80-80 80s-80-35.89-80-80 35.89-80 80-80 80 35.89 80 80zM25.26 25.2l13.6 272A24 24 0 0062.83 320h66.34a24 24 0 0023.97-22.8l13.6-272A24 24 0 00142.77 0H49.23a24 24 0 00-23.97 25.2z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 6simple-notice-info = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 192 512\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M20 424.23h20V279.77H20a20 20 0 01-20-20V212a20 20 0 0120-20h112a20 20 0 0120 20v212.23h20a20 20 0 0120 20V492a20 20 0 01-20 20H20a20 20 0 01-20-20v-47.77a20 20 0 0120-20zM96 0a72 72 0 100 144A72 72 0 0096 0z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 7simple-notice-tip = \u0026#39;\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; class=\u0026#34;icon\u0026#34; viewBox=\u0026#34;0 0 512 512\u0026#34;\u0026gt;\u0026lt;path d=\u0026#34;M173.9 439.4L7.5 273c-10-10-10-26.2 0-36.2l36.2-36.2c10-10 26.2-10 36.2 0L192 312.69l240.1-240.1c10-10 26.2-10 36.2 0l36.2 36.21c10 10 10 26.2 0 36.2L210.1 439.4c-10 10-26.2 10-36.2 0z\u0026#34;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#39; 使用： 1{{\u0026lt; simple-notice simple-notice-warning \u0026gt;}} 2十里青山远，潮平路带沙。数声啼鸟怨年华。又是凄凉时候，在天涯。白露收残月，清风散晓霞。绿杨堤畔问荷花。记得年时沽酒，那人家。 3{{\u0026lt; /simple-notice \u0026gt;}} 荧光标注 红色萤光笔 橙色萤光笔 黄色萤光笔 绿色萤光笔 蓝色萤光笔 紫色萤光笔 新建 setSwlColor.html，内容为： 1{{ $color := default \u0026#34;mark_blue\u0026#34; (.Get \u0026#34;color\u0026#34;) }}\u0026lt;span class=\u0026#39;swl-marker {{ $color }}\u0026#39;\u0026gt;{{ .Inner }}\u0026lt;/span\u0026gt; 在 custom.scss 中添加： 1.swl-marker { 2 display: inline; 3 padding: 2px; 4} 5 6.mark_blue { 7 background: -webkit-linear-gradient(transparent 64%, #b7e3ff 0%); 8 background: linear-gradient(transparent 64%, #b7e3ff 0%) 9} 10 11.mark_green { 12 background: -webkit-linear-gradient(transparent 64%, #bdf9c3 0%); 13 background: linear-gradient(transparent 64%, #bdf9c3 0%); 14} 15 16.mark_yellow { 17 background: -webkit-linear-gradient(transparent 64%, #fcf69f 0%); 18 background: linear-gradient(transparent 64%, #fcf69f 0%) 19} 20 21.mark_orange { 22 background: -webkit-linear-gradient(transparent 64%, #ffddbc 0%); 23 background: linear-gradient(transparent 64%, #ffddbc 0%) 24} 25 26.mark_red { 27 background: -webkit-linear-gradient(transparent 64%, #dfa1a1 0%); 28 background: linear-gradient(transparent 64%, #dfa1a1 0%) 29} 使用： 1{{\u0026lt; setSwlColor color=\u0026#34;mark_blue\u0026#34; \u0026gt;}}蓝色萤光笔{{\u0026lt; /setSwlColor \u0026gt;}} 警告框 标注 标注 摘要 摘要 信息 信息 提示 提示 成功 成功 疑问 疑问 警告 警告 失败 失败 危险 危险 漏洞 漏洞 范例 范例 引用 引用 新建 admonition.html，内容为： 1{{- /* 使用方法 {{\u0026lt; admonition type=tip title=\u0026#34;小标题\u0026#34; open=false \u0026gt;}}内容{{\u0026lt; /admonition \u0026gt;}}，其中type支持note, abstract, info, tip, success, question, warning, failure, danger, bug, example, quote， open表示默认是否为展开状态 */ -}} 2{{- $inner := .Inner | .Page.RenderString -}} 3 4{{- $iconMap := dict \u0026#34;note\u0026#34; \u0026#34;fas fa-pencil-alt fa-fw\u0026#34; -}} 5{{- $iconMap = dict \u0026#34;abstract\u0026#34; \u0026#34;fas fa-list-ul fa-fw\u0026#34; | merge $iconMap -}} 6{{- $iconMap = dict \u0026#34;info\u0026#34; \u0026#34;fas fa-info-circle fa-fw\u0026#34; | merge $iconMap -}} 7{{- $iconMap = dict \u0026#34;tip\u0026#34; \u0026#34;fas fa-lightbulb fa-fw\u0026#34; | merge $iconMap -}} 8{{- $iconMap = dict \u0026#34;success\u0026#34; \u0026#34;fas fa-check-circle fa-fw\u0026#34; | merge $iconMap -}} 9{{- $iconMap = dict \u0026#34;question\u0026#34; \u0026#34;fas fa-question-circle fa-fw\u0026#34; | merge $iconMap -}} 10{{- $iconMap = dict \u0026#34;warning\u0026#34; \u0026#34;fas fa-exclamation-triangle fa-fw\u0026#34; | merge $iconMap -}} 11{{- $iconMap = dict \u0026#34;failure\u0026#34; \u0026#34;fas fa-times-circle fa-fw\u0026#34; | merge $iconMap -}} 12{{- $iconMap = dict \u0026#34;danger\u0026#34; \u0026#34;fas fa-skull-crossbones fa-fw\u0026#34; | merge $iconMap -}} 13{{- $iconMap = dict \u0026#34;bug\u0026#34; \u0026#34;fas fa-bug fa-fw\u0026#34; | merge $iconMap -}} 14{{- $iconMap = dict \u0026#34;example\u0026#34; \u0026#34;fas fa-list-ol fa-fw\u0026#34; | merge $iconMap -}} 15{{- $iconMap = dict \u0026#34;quote\u0026#34; \u0026#34;fas fa-quote-right fa-fw\u0026#34; | merge $iconMap -}} 16{{- $iconDetails := \u0026#34;fas fa-angle-right fa-fw\u0026#34; -}} 17 18{{- if .IsNamedParams -}} 19 {{- $type := .Get \u0026#34;type\u0026#34; | default \u0026#34;note\u0026#34; -}} 20 \u0026lt;div class=\u0026#34;details admonition {{ $type }}{{ if .Get `open` | ne false }} open{{ end }}\u0026#34;\u0026gt; 21 \u0026lt;div class=\u0026#34;details-summary admonition-title\u0026#34;\u0026gt; 22 \u0026lt;i class=\u0026#34;icon {{ index $iconMap $type | default (index $iconMap \u0026#34;note\u0026#34;) }}\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;{{ .Get \u0026#34;title\u0026#34; | default (T $type) }}\u0026lt;i class=\u0026#34;details-icon {{ $iconDetails }}\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; 23 \u0026lt;/div\u0026gt; 24 \u0026lt;div class=\u0026#34;details-content\u0026#34;\u0026gt; 25 \u0026lt;div class=\u0026#34;admonition-content\u0026#34;\u0026gt; 26 {{- $inner -}} 27 \u0026lt;/div\u0026gt; 28 \u0026lt;/div\u0026gt; 29 \u0026lt;/div\u0026gt; 30{{- else -}} 31 {{- $type := .Get 0 | default \u0026#34;note\u0026#34; -}} 32 \u0026lt;div class=\u0026#34;details admonition {{ $type }}{{ if .Get 2 | ne false }} open{{ end }}\u0026#34;\u0026gt; 33 \u0026lt;div class=\u0026#34;details-summary admonition-title\u0026#34;\u0026gt; 34 \u0026lt;i class=\u0026#34;icon {{ index $iconMap $type | default (index $iconMap \u0026#34;note\u0026#34;) }}\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;{{ .Get 1 | default (T $type) }}\u0026lt;i class=\u0026#34;details-icon {{ $iconDetails }}\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; 35 \u0026lt;/div\u0026gt; 36 \u0026lt;div class=\u0026#34;details-content\u0026#34;\u0026gt; 37 \u0026lt;div class=\u0026#34;admonition-content\u0026#34;\u0026gt; 38 {{- $inner -}} 39 \u0026lt;/div\u0026gt; 40 \u0026lt;/div\u0026gt; 41 \u0026lt;/div\u0026gt; 42{{- end -}} 在 custom.scss 中添加： 1/*! 2 * Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com 3 * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 */ 5 .fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul\u0026gt;li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:\u0026#34;progid:DXImageTransform.Microsoft.BasicImage(rotation=1)\u0026#34;;-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:\u0026#34;progid:DXImageTransform.Microsoft.BasicImage(rotation=2)\u0026#34;;-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:\u0026#34;progid:DXImageTransform.Microsoft.BasicImage(rotation=3)\u0026#34;;-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:\u0026#34;progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)\u0026#34;;-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:\u0026#34;progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)\u0026#34;}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:\u0026#34;\\f26e\u0026#34;}.fa-accessible-icon:before{content:\u0026#34;\\f368\u0026#34;}.fa-accusoft:before{content:\u0026#34;\\f369\u0026#34;}.fa-acquisitions-incorporated:before{content:\u0026#34;\\f6af\u0026#34;}.fa-ad:before{content:\u0026#34;\\f641\u0026#34;}.fa-address-book:before{content:\u0026#34;\\f2b9\u0026#34;}.fa-address-card:before{content:\u0026#34;\\f2bb\u0026#34;}.fa-adjust:before{content:\u0026#34;\\f042\u0026#34;}.fa-adn:before{content:\u0026#34;\\f170\u0026#34;}.fa-adversal:before{content:\u0026#34;\\f36a\u0026#34;}.fa-affiliatetheme:before{content:\u0026#34;\\f36b\u0026#34;}.fa-air-freshener:before{content:\u0026#34;\\f5d0\u0026#34;}.fa-airbnb:before{content:\u0026#34;\\f834\u0026#34;}.fa-algolia:before{content:\u0026#34;\\f36c\u0026#34;}.fa-align-center:before{content:\u0026#34;\\f037\u0026#34;}.fa-align-justify:before{content:\u0026#34;\\f039\u0026#34;}.fa-align-left:before{content:\u0026#34;\\f036\u0026#34;}.fa-align-right:before{content:\u0026#34;\\f038\u0026#34;}.fa-alipay:before{content:\u0026#34;\\f642\u0026#34;}.fa-allergies:before{content:\u0026#34;\\f461\u0026#34;}.fa-amazon:before{content:\u0026#34;\\f270\u0026#34;}.fa-amazon-pay:before{content:\u0026#34;\\f42c\u0026#34;}.fa-ambulance:before{content:\u0026#34;\\f0f9\u0026#34;}.fa-american-sign-language-interpreting:before{content:\u0026#34;\\f2a3\u0026#34;}.fa-amilia:before{content:\u0026#34;\\f36d\u0026#34;}.fa-anchor:before{content:\u0026#34;\\f13d\u0026#34;}.fa-android:before{content:\u0026#34;\\f17b\u0026#34;}.fa-angellist:before{content:\u0026#34;\\f209\u0026#34;}.fa-angle-double-down:before{content:\u0026#34;\\f103\u0026#34;}.fa-angle-double-left:before{content:\u0026#34;\\f100\u0026#34;}.fa-angle-double-right:before{content:\u0026#34;\\f101\u0026#34;}.fa-angle-double-up:before{content:\u0026#34;\\f102\u0026#34;}.fa-angle-down:before{content:\u0026#34;\\f107\u0026#34;}.fa-angle-left:before{content:\u0026#34;\\f104\u0026#34;}.fa-angle-right:before{content:\u0026#34;\\f105\u0026#34;}.fa-angle-up:before{content:\u0026#34;\\f106\u0026#34;}.fa-angry:before{content:\u0026#34;\\f556\u0026#34;}.fa-angrycreative:before{content:\u0026#34;\\f36e\u0026#34;}.fa-angular:before{content:\u0026#34;\\f420\u0026#34;}.fa-ankh:before{content:\u0026#34;\\f644\u0026#34;}.fa-app-store:before{content:\u0026#34;\\f36f\u0026#34;}.fa-app-store-ios:before{content:\u0026#34;\\f370\u0026#34;}.fa-apper:before{content:\u0026#34;\\f371\u0026#34;}.fa-apple:before{content:\u0026#34;\\f179\u0026#34;}.fa-apple-alt:before{content:\u0026#34;\\f5d1\u0026#34;}.fa-apple-pay:before{content:\u0026#34;\\f415\u0026#34;}.fa-archive:before{content:\u0026#34;\\f187\u0026#34;}.fa-archway:before{content:\u0026#34;\\f557\u0026#34;}.fa-arrow-alt-circle-down:before{content:\u0026#34;\\f358\u0026#34;}.fa-arrow-alt-circle-left:before{content:\u0026#34;\\f359\u0026#34;}.fa-arrow-alt-circle-right:before{content:\u0026#34;\\f35a\u0026#34;}.fa-arrow-alt-circle-up:before{content:\u0026#34;\\f35b\u0026#34;}.fa-arrow-circle-down:before{content:\u0026#34;\\f0ab\u0026#34;}.fa-arrow-circle-left:before{content:\u0026#34;\\f0a8\u0026#34;}.fa-arrow-circle-right:before{content:\u0026#34;\\f0a9\u0026#34;}.fa-arrow-circle-up:before{content:\u0026#34;\\f0aa\u0026#34;}.fa-arrow-down:before{content:\u0026#34;\\f063\u0026#34;}.fa-arrow-left:before{content:\u0026#34;\\f060\u0026#34;}.fa-arrow-right:before{content:\u0026#34;\\f061\u0026#34;}.fa-arrow-up:before{content:\u0026#34;\\f062\u0026#34;}.fa-arrows-alt:before{content:\u0026#34;\\f0b2\u0026#34;}.fa-arrows-alt-h:before{content:\u0026#34;\\f337\u0026#34;}.fa-arrows-alt-v:before{content:\u0026#34;\\f338\u0026#34;}.fa-artstation:before{content:\u0026#34;\\f77a\u0026#34;}.fa-assistive-listening-systems:before{content:\u0026#34;\\f2a2\u0026#34;}.fa-asterisk:before{content:\u0026#34;\\f069\u0026#34;}.fa-asymmetrik:before{content:\u0026#34;\\f372\u0026#34;}.fa-at:before{content:\u0026#34;\\f1fa\u0026#34;}.fa-atlas:before{content:\u0026#34;\\f558\u0026#34;}.fa-atlassian:before{content:\u0026#34;\\f77b\u0026#34;}.fa-atom:before{content:\u0026#34;\\f5d2\u0026#34;}.fa-audible:before{content:\u0026#34;\\f373\u0026#34;}.fa-audio-description:before{content:\u0026#34;\\f29e\u0026#34;}.fa-autoprefixer:before{content:\u0026#34;\\f41c\u0026#34;}.fa-avianex:before{content:\u0026#34;\\f374\u0026#34;}.fa-aviato:before{content:\u0026#34;\\f421\u0026#34;}.fa-award:before{content:\u0026#34;\\f559\u0026#34;}.fa-aws:before{content:\u0026#34;\\f375\u0026#34;}.fa-baby:before{content:\u0026#34;\\f77c\u0026#34;}.fa-baby-carriage:before{content:\u0026#34;\\f77d\u0026#34;}.fa-backspace:before{content:\u0026#34;\\f55a\u0026#34;}.fa-backward:before{content:\u0026#34;\\f04a\u0026#34;}.fa-bacon:before{content:\u0026#34;\\f7e5\u0026#34;}.fa-bacteria:before{content:\u0026#34;\\e059\u0026#34;}.fa-bacterium:before{content:\u0026#34;\\e05a\u0026#34;}.fa-bahai:before{content:\u0026#34;\\f666\u0026#34;}.fa-balance-scale:before{content:\u0026#34;\\f24e\u0026#34;}.fa-balance-scale-left:before{content:\u0026#34;\\f515\u0026#34;}.fa-balance-scale-right:before{content:\u0026#34;\\f516\u0026#34;}.fa-ban:before{content:\u0026#34;\\f05e\u0026#34;}.fa-band-aid:before{content:\u0026#34;\\f462\u0026#34;}.fa-bandcamp:before{content:\u0026#34;\\f2d5\u0026#34;}.fa-barcode:before{content:\u0026#34;\\f02a\u0026#34;}.fa-bars:before{content:\u0026#34;\\f0c9\u0026#34;}.fa-baseball-ball:before{content:\u0026#34;\\f433\u0026#34;}.fa-basketball-ball:before{content:\u0026#34;\\f434\u0026#34;}.fa-bath:before{content:\u0026#34;\\f2cd\u0026#34;}.fa-battery-empty:before{content:\u0026#34;\\f244\u0026#34;}.fa-battery-full:before{content:\u0026#34;\\f240\u0026#34;}.fa-battery-half:before{content:\u0026#34;\\f242\u0026#34;}.fa-battery-quarter:before{content:\u0026#34;\\f243\u0026#34;}.fa-battery-three-quarters:before{content:\u0026#34;\\f241\u0026#34;}.fa-battle-net:before{content:\u0026#34;\\f835\u0026#34;}.fa-bed:before{content:\u0026#34;\\f236\u0026#34;}.fa-beer:before{content:\u0026#34;\\f0fc\u0026#34;}.fa-behance:before{content:\u0026#34;\\f1b4\u0026#34;}.fa-behance-square:before{content:\u0026#34;\\f1b5\u0026#34;}.fa-bell:before{content:\u0026#34;\\f0f3\u0026#34;}.fa-bell-slash:before{content:\u0026#34;\\f1f6\u0026#34;}.fa-bezier-curve:before{content:\u0026#34;\\f55b\u0026#34;}.fa-bible:before{content:\u0026#34;\\f647\u0026#34;}.fa-bicycle:before{content:\u0026#34;\\f206\u0026#34;}.fa-biking:before{content:\u0026#34;\\f84a\u0026#34;}.fa-bimobject:before{content:\u0026#34;\\f378\u0026#34;}.fa-binoculars:before{content:\u0026#34;\\f1e5\u0026#34;}.fa-biohazard:before{content:\u0026#34;\\f780\u0026#34;}.fa-birthday-cake:before{content:\u0026#34;\\f1fd\u0026#34;}.fa-bitbucket:before{content:\u0026#34;\\f171\u0026#34;}.fa-bitcoin:before{content:\u0026#34;\\f379\u0026#34;}.fa-bity:before{content:\u0026#34;\\f37a\u0026#34;}.fa-black-tie:before{content:\u0026#34;\\f27e\u0026#34;}.fa-blackberry:before{content:\u0026#34;\\f37b\u0026#34;}.fa-blender:before{content:\u0026#34;\\f517\u0026#34;}.fa-blender-phone:before{content:\u0026#34;\\f6b6\u0026#34;}.fa-blind:before{content:\u0026#34;\\f29d\u0026#34;}.fa-blog:before{content:\u0026#34;\\f781\u0026#34;}.fa-blogger:before{content:\u0026#34;\\f37c\u0026#34;}.fa-blogger-b:before{content:\u0026#34;\\f37d\u0026#34;}.fa-bluetooth:before{content:\u0026#34;\\f293\u0026#34;}.fa-bluetooth-b:before{content:\u0026#34;\\f294\u0026#34;}.fa-bold:before{content:\u0026#34;\\f032\u0026#34;}.fa-bolt:before{content:\u0026#34;\\f0e7\u0026#34;}.fa-bomb:before{content:\u0026#34;\\f1e2\u0026#34;}.fa-bone:before{content:\u0026#34;\\f5d7\u0026#34;}.fa-bong:before{content:\u0026#34;\\f55c\u0026#34;}.fa-book:before{content:\u0026#34;\\f02d\u0026#34;}.fa-book-dead:before{content:\u0026#34;\\f6b7\u0026#34;}.fa-book-medical:before{content:\u0026#34;\\f7e6\u0026#34;}.fa-book-open:before{content:\u0026#34;\\f518\u0026#34;}.fa-book-reader:before{content:\u0026#34;\\f5da\u0026#34;}.fa-bookmark:before{content:\u0026#34;\\f02e\u0026#34;}.fa-bootstrap:before{content:\u0026#34;\\f836\u0026#34;}.fa-border-all:before{content:\u0026#34;\\f84c\u0026#34;}.fa-border-none:before{content:\u0026#34;\\f850\u0026#34;}.fa-border-style:before{content:\u0026#34;\\f853\u0026#34;}.fa-bowling-ball:before{content:\u0026#34;\\f436\u0026#34;}.fa-box:before{content:\u0026#34;\\f466\u0026#34;}.fa-box-open:before{content:\u0026#34;\\f49e\u0026#34;}.fa-box-tissue:before{content:\u0026#34;\\e05b\u0026#34;}.fa-boxes:before{content:\u0026#34;\\f468\u0026#34;}.fa-braille:before{content:\u0026#34;\\f2a1\u0026#34;}.fa-brain:before{content:\u0026#34;\\f5dc\u0026#34;}.fa-bread-slice:before{content:\u0026#34;\\f7ec\u0026#34;}.fa-briefcase:before{content:\u0026#34;\\f0b1\u0026#34;}.fa-briefcase-medical:before{content:\u0026#34;\\f469\u0026#34;}.fa-broadcast-tower:before{content:\u0026#34;\\f519\u0026#34;}.fa-broom:before{content:\u0026#34;\\f51a\u0026#34;}.fa-brush:before{content:\u0026#34;\\f55d\u0026#34;}.fa-btc:before{content:\u0026#34;\\f15a\u0026#34;}.fa-buffer:before{content:\u0026#34;\\f837\u0026#34;}.fa-bug:before{content:\u0026#34;\\f188\u0026#34;}.fa-building:before{content:\u0026#34;\\f1ad\u0026#34;}.fa-bullhorn:before{content:\u0026#34;\\f0a1\u0026#34;}.fa-bullseye:before{content:\u0026#34;\\f140\u0026#34;}.fa-burn:before{content:\u0026#34;\\f46a\u0026#34;}.fa-buromobelexperte:before{content:\u0026#34;\\f37f\u0026#34;}.fa-bus:before{content:\u0026#34;\\f207\u0026#34;}.fa-bus-alt:before{content:\u0026#34;\\f55e\u0026#34;}.fa-business-time:before{content:\u0026#34;\\f64a\u0026#34;}.fa-buy-n-large:before{content:\u0026#34;\\f8a6\u0026#34;}.fa-buysellads:before{content:\u0026#34;\\f20d\u0026#34;}.fa-calculator:before{content:\u0026#34;\\f1ec\u0026#34;}.fa-calendar:before{content:\u0026#34;\\f133\u0026#34;}.fa-calendar-alt:before{content:\u0026#34;\\f073\u0026#34;}.fa-calendar-check:before{content:\u0026#34;\\f274\u0026#34;}.fa-calendar-day:before{content:\u0026#34;\\f783\u0026#34;}.fa-calendar-minus:before{content:\u0026#34;\\f272\u0026#34;}.fa-calendar-plus:before{content:\u0026#34;\\f271\u0026#34;}.fa-calendar-times:before{content:\u0026#34;\\f273\u0026#34;}.fa-calendar-week:before{content:\u0026#34;\\f784\u0026#34;}.fa-camera:before{content:\u0026#34;\\f030\u0026#34;}.fa-camera-retro:before{content:\u0026#34;\\f083\u0026#34;}.fa-campground:before{content:\u0026#34;\\f6bb\u0026#34;}.fa-canadian-maple-leaf:before{content:\u0026#34;\\f785\u0026#34;}.fa-candy-cane:before{content:\u0026#34;\\f786\u0026#34;}.fa-cannabis:before{content:\u0026#34;\\f55f\u0026#34;}.fa-capsules:before{content:\u0026#34;\\f46b\u0026#34;}.fa-car:before{content:\u0026#34;\\f1b9\u0026#34;}.fa-car-alt:before{content:\u0026#34;\\f5de\u0026#34;}.fa-car-battery:before{content:\u0026#34;\\f5df\u0026#34;}.fa-car-crash:before{content:\u0026#34;\\f5e1\u0026#34;}.fa-car-side:before{content:\u0026#34;\\f5e4\u0026#34;}.fa-caravan:before{content:\u0026#34;\\f8ff\u0026#34;}.fa-caret-down:before{content:\u0026#34;\\f0d7\u0026#34;}.fa-caret-left:before{content:\u0026#34;\\f0d9\u0026#34;}.fa-caret-right:before{content:\u0026#34;\\f0da\u0026#34;}.fa-caret-square-down:before{content:\u0026#34;\\f150\u0026#34;}.fa-caret-square-left:before{content:\u0026#34;\\f191\u0026#34;}.fa-caret-square-right:before{content:\u0026#34;\\f152\u0026#34;}.fa-caret-square-up:before{content:\u0026#34;\\f151\u0026#34;}.fa-caret-up:before{content:\u0026#34;\\f0d8\u0026#34;}.fa-carrot:before{content:\u0026#34;\\f787\u0026#34;}.fa-cart-arrow-down:before{content:\u0026#34;\\f218\u0026#34;}.fa-cart-plus:before{content:\u0026#34;\\f217\u0026#34;}.fa-cash-register:before{content:\u0026#34;\\f788\u0026#34;}.fa-cat:before{content:\u0026#34;\\f6be\u0026#34;}.fa-cc-amazon-pay:before{content:\u0026#34;\\f42d\u0026#34;}.fa-cc-amex:before{content:\u0026#34;\\f1f3\u0026#34;}.fa-cc-apple-pay:before{content:\u0026#34;\\f416\u0026#34;}.fa-cc-diners-club:before{content:\u0026#34;\\f24c\u0026#34;}.fa-cc-discover:before{content:\u0026#34;\\f1f2\u0026#34;}.fa-cc-jcb:before{content:\u0026#34;\\f24b\u0026#34;}.fa-cc-mastercard:before{content:\u0026#34;\\f1f1\u0026#34;}.fa-cc-paypal:before{content:\u0026#34;\\f1f4\u0026#34;}.fa-cc-stripe:before{content:\u0026#34;\\f1f5\u0026#34;}.fa-cc-visa:before{content:\u0026#34;\\f1f0\u0026#34;}.fa-centercode:before{content:\u0026#34;\\f380\u0026#34;}.fa-centos:before{content:\u0026#34;\\f789\u0026#34;}.fa-certificate:before{content:\u0026#34;\\f0a3\u0026#34;}.fa-chair:before{content:\u0026#34;\\f6c0\u0026#34;}.fa-chalkboard:before{content:\u0026#34;\\f51b\u0026#34;}.fa-chalkboard-teacher:before{content:\u0026#34;\\f51c\u0026#34;}.fa-charging-station:before{content:\u0026#34;\\f5e7\u0026#34;}.fa-chart-area:before{content:\u0026#34;\\f1fe\u0026#34;}.fa-chart-bar:before{content:\u0026#34;\\f080\u0026#34;}.fa-chart-line:before{content:\u0026#34;\\f201\u0026#34;}.fa-chart-pie:before{content:\u0026#34;\\f200\u0026#34;}.fa-check:before{content:\u0026#34;\\f00c\u0026#34;}.fa-check-circle:before{content:\u0026#34;\\f058\u0026#34;}.fa-check-double:before{content:\u0026#34;\\f560\u0026#34;}.fa-check-square:before{content:\u0026#34;\\f14a\u0026#34;}.fa-cheese:before{content:\u0026#34;\\f7ef\u0026#34;}.fa-chess:before{content:\u0026#34;\\f439\u0026#34;}.fa-chess-bishop:before{content:\u0026#34;\\f43a\u0026#34;}.fa-chess-board:before{content:\u0026#34;\\f43c\u0026#34;}.fa-chess-king:before{content:\u0026#34;\\f43f\u0026#34;}.fa-chess-knight:before{content:\u0026#34;\\f441\u0026#34;}.fa-chess-pawn:before{content:\u0026#34;\\f443\u0026#34;}.fa-chess-queen:before{content:\u0026#34;\\f445\u0026#34;}.fa-chess-rook:before{content:\u0026#34;\\f447\u0026#34;}.fa-chevron-circle-down:before{content:\u0026#34;\\f13a\u0026#34;}.fa-chevron-circle-left:before{content:\u0026#34;\\f137\u0026#34;}.fa-chevron-circle-right:before{content:\u0026#34;\\f138\u0026#34;}.fa-chevron-circle-up:before{content:\u0026#34;\\f139\u0026#34;}.fa-chevron-down:before{content:\u0026#34;\\f078\u0026#34;}.fa-chevron-left:before{content:\u0026#34;\\f053\u0026#34;}.fa-chevron-right:before{content:\u0026#34;\\f054\u0026#34;}.fa-chevron-up:before{content:\u0026#34;\\f077\u0026#34;}.fa-child:before{content:\u0026#34;\\f1ae\u0026#34;}.fa-chrome:before{content:\u0026#34;\\f268\u0026#34;}.fa-chromecast:before{content:\u0026#34;\\f838\u0026#34;}.fa-church:before{content:\u0026#34;\\f51d\u0026#34;}.fa-circle:before{content:\u0026#34;\\f111\u0026#34;}.fa-circle-notch:before{content:\u0026#34;\\f1ce\u0026#34;}.fa-city:before{content:\u0026#34;\\f64f\u0026#34;}.fa-clinic-medical:before{content:\u0026#34;\\f7f2\u0026#34;}.fa-clipboard:before{content:\u0026#34;\\f328\u0026#34;}.fa-clipboard-check:before{content:\u0026#34;\\f46c\u0026#34;}.fa-clipboard-list:before{content:\u0026#34;\\f46d\u0026#34;}.fa-clock:before{content:\u0026#34;\\f017\u0026#34;}.fa-clone:before{content:\u0026#34;\\f24d\u0026#34;}.fa-closed-captioning:before{content:\u0026#34;\\f20a\u0026#34;}.fa-cloud:before{content:\u0026#34;\\f0c2\u0026#34;}.fa-cloud-download-alt:before{content:\u0026#34;\\f381\u0026#34;}.fa-cloud-meatball:before{content:\u0026#34;\\f73b\u0026#34;}.fa-cloud-moon:before{content:\u0026#34;\\f6c3\u0026#34;}.fa-cloud-moon-rain:before{content:\u0026#34;\\f73c\u0026#34;}.fa-cloud-rain:before{content:\u0026#34;\\f73d\u0026#34;}.fa-cloud-showers-heavy:before{content:\u0026#34;\\f740\u0026#34;}.fa-cloud-sun:before{content:\u0026#34;\\f6c4\u0026#34;}.fa-cloud-sun-rain:before{content:\u0026#34;\\f743\u0026#34;}.fa-cloud-upload-alt:before{content:\u0026#34;\\f382\u0026#34;}.fa-cloudflare:before{content:\u0026#34;\\e07d\u0026#34;}.fa-cloudscale:before{content:\u0026#34;\\f383\u0026#34;}.fa-cloudsmith:before{content:\u0026#34;\\f384\u0026#34;}.fa-cloudversify:before{content:\u0026#34;\\f385\u0026#34;}.fa-cocktail:before{content:\u0026#34;\\f561\u0026#34;}.fa-code:before{content:\u0026#34;\\f121\u0026#34;}.fa-code-branch:before{content:\u0026#34;\\f126\u0026#34;}.fa-codepen:before{content:\u0026#34;\\f1cb\u0026#34;}.fa-codiepie:before{content:\u0026#34;\\f284\u0026#34;}.fa-coffee:before{content:\u0026#34;\\f0f4\u0026#34;}.fa-cog:before{content:\u0026#34;\\f013\u0026#34;}.fa-cogs:before{content:\u0026#34;\\f085\u0026#34;}.fa-coins:before{content:\u0026#34;\\f51e\u0026#34;}.fa-columns:before{content:\u0026#34;\\f0db\u0026#34;}.fa-comment:before{content:\u0026#34;\\f075\u0026#34;}.fa-comment-alt:before{content:\u0026#34;\\f27a\u0026#34;}.fa-comment-dollar:before{content:\u0026#34;\\f651\u0026#34;}.fa-comment-dots:before{content:\u0026#34;\\f4ad\u0026#34;}.fa-comment-medical:before{content:\u0026#34;\\f7f5\u0026#34;}.fa-comment-slash:before{content:\u0026#34;\\f4b3\u0026#34;}.fa-comments:before{content:\u0026#34;\\f086\u0026#34;}.fa-comments-dollar:before{content:\u0026#34;\\f653\u0026#34;}.fa-compact-disc:before{content:\u0026#34;\\f51f\u0026#34;}.fa-compass:before{content:\u0026#34;\\f14e\u0026#34;}.fa-compress:before{content:\u0026#34;\\f066\u0026#34;}.fa-compress-alt:before{content:\u0026#34;\\f422\u0026#34;}.fa-compress-arrows-alt:before{content:\u0026#34;\\f78c\u0026#34;}.fa-concierge-bell:before{content:\u0026#34;\\f562\u0026#34;}.fa-confluence:before{content:\u0026#34;\\f78d\u0026#34;}.fa-connectdevelop:before{content:\u0026#34;\\f20e\u0026#34;}.fa-contao:before{content:\u0026#34;\\f26d\u0026#34;}.fa-cookie:before{content:\u0026#34;\\f563\u0026#34;}.fa-cookie-bite:before{content:\u0026#34;\\f564\u0026#34;}.fa-copy:before{content:\u0026#34;\\f0c5\u0026#34;}.fa-copyright:before{content:\u0026#34;\\f1f9\u0026#34;}.fa-cotton-bureau:before{content:\u0026#34;\\f89e\u0026#34;}.fa-couch:before{content:\u0026#34;\\f4b8\u0026#34;}.fa-cpanel:before{content:\u0026#34;\\f388\u0026#34;}.fa-creative-commons:before{content:\u0026#34;\\f25e\u0026#34;}.fa-creative-commons-by:before{content:\u0026#34;\\f4e7\u0026#34;}.fa-creative-commons-nc:before{content:\u0026#34;\\f4e8\u0026#34;}.fa-creative-commons-nc-eu:before{content:\u0026#34;\\f4e9\u0026#34;}.fa-creative-commons-nc-jp:before{content:\u0026#34;\\f4ea\u0026#34;}.fa-creative-commons-nd:before{content:\u0026#34;\\f4eb\u0026#34;}.fa-creative-commons-pd:before{content:\u0026#34;\\f4ec\u0026#34;}.fa-creative-commons-pd-alt:before{content:\u0026#34;\\f4ed\u0026#34;}.fa-creative-commons-remix:before{content:\u0026#34;\\f4ee\u0026#34;}.fa-creative-commons-sa:before{content:\u0026#34;\\f4ef\u0026#34;}.fa-creative-commons-sampling:before{content:\u0026#34;\\f4f0\u0026#34;}.fa-creative-commons-sampling-plus:before{content:\u0026#34;\\f4f1\u0026#34;}.fa-creative-commons-share:before{content:\u0026#34;\\f4f2\u0026#34;}.fa-creative-commons-zero:before{content:\u0026#34;\\f4f3\u0026#34;}.fa-credit-card:before{content:\u0026#34;\\f09d\u0026#34;}.fa-critical-role:before{content:\u0026#34;\\f6c9\u0026#34;}.fa-crop:before{content:\u0026#34;\\f125\u0026#34;}.fa-crop-alt:before{content:\u0026#34;\\f565\u0026#34;}.fa-cross:before{content:\u0026#34;\\f654\u0026#34;}.fa-crosshairs:before{content:\u0026#34;\\f05b\u0026#34;}.fa-crow:before{content:\u0026#34;\\f520\u0026#34;}.fa-crown:before{content:\u0026#34;\\f521\u0026#34;}.fa-crutch:before{content:\u0026#34;\\f7f7\u0026#34;}.fa-css3:before{content:\u0026#34;\\f13c\u0026#34;}.fa-css3-alt:before{content:\u0026#34;\\f38b\u0026#34;}.fa-cube:before{content:\u0026#34;\\f1b2\u0026#34;}.fa-cubes:before{content:\u0026#34;\\f1b3\u0026#34;}.fa-cut:before{content:\u0026#34;\\f0c4\u0026#34;}.fa-cuttlefish:before{content:\u0026#34;\\f38c\u0026#34;}.fa-d-and-d:before{content:\u0026#34;\\f38d\u0026#34;}.fa-d-and-d-beyond:before{content:\u0026#34;\\f6ca\u0026#34;}.fa-dailymotion:before{content:\u0026#34;\\e052\u0026#34;}.fa-dashcube:before{content:\u0026#34;\\f210\u0026#34;}.fa-database:before{content:\u0026#34;\\f1c0\u0026#34;}.fa-deaf:before{content:\u0026#34;\\f2a4\u0026#34;}.fa-deezer:before{content:\u0026#34;\\e077\u0026#34;}.fa-delicious:before{content:\u0026#34;\\f1a5\u0026#34;}.fa-democrat:before{content:\u0026#34;\\f747\u0026#34;}.fa-deploydog:before{content:\u0026#34;\\f38e\u0026#34;}.fa-deskpro:before{content:\u0026#34;\\f38f\u0026#34;}.fa-desktop:before{content:\u0026#34;\\f108\u0026#34;}.fa-dev:before{content:\u0026#34;\\f6cc\u0026#34;}.fa-deviantart:before{content:\u0026#34;\\f1bd\u0026#34;}.fa-dharmachakra:before{content:\u0026#34;\\f655\u0026#34;}.fa-dhl:before{content:\u0026#34;\\f790\u0026#34;}.fa-diagnoses:before{content:\u0026#34;\\f470\u0026#34;}.fa-diaspora:before{content:\u0026#34;\\f791\u0026#34;}.fa-dice:before{content:\u0026#34;\\f522\u0026#34;}.fa-dice-d20:before{content:\u0026#34;\\f6cf\u0026#34;}.fa-dice-d6:before{content:\u0026#34;\\f6d1\u0026#34;}.fa-dice-five:before{content:\u0026#34;\\f523\u0026#34;}.fa-dice-four:before{content:\u0026#34;\\f524\u0026#34;}.fa-dice-one:before{content:\u0026#34;\\f525\u0026#34;}.fa-dice-six:before{content:\u0026#34;\\f526\u0026#34;}.fa-dice-three:before{content:\u0026#34;\\f527\u0026#34;}.fa-dice-two:before{content:\u0026#34;\\f528\u0026#34;}.fa-digg:before{content:\u0026#34;\\f1a6\u0026#34;}.fa-digital-ocean:before{content:\u0026#34;\\f391\u0026#34;}.fa-digital-tachograph:before{content:\u0026#34;\\f566\u0026#34;}.fa-directions:before{content:\u0026#34;\\f5eb\u0026#34;}.fa-discord:before{content:\u0026#34;\\f392\u0026#34;}.fa-discourse:before{content:\u0026#34;\\f393\u0026#34;}.fa-disease:before{content:\u0026#34;\\f7fa\u0026#34;}.fa-divide:before{content:\u0026#34;\\f529\u0026#34;}.fa-dizzy:before{content:\u0026#34;\\f567\u0026#34;}.fa-dna:before{content:\u0026#34;\\f471\u0026#34;}.fa-dochub:before{content:\u0026#34;\\f394\u0026#34;}.fa-docker:before{content:\u0026#34;\\f395\u0026#34;}.fa-dog:before{content:\u0026#34;\\f6d3\u0026#34;}.fa-dollar-sign:before{content:\u0026#34;\\f155\u0026#34;}.fa-dolly:before{content:\u0026#34;\\f472\u0026#34;}.fa-dolly-flatbed:before{content:\u0026#34;\\f474\u0026#34;}.fa-donate:before{content:\u0026#34;\\f4b9\u0026#34;}.fa-door-closed:before{content:\u0026#34;\\f52a\u0026#34;}.fa-door-open:before{content:\u0026#34;\\f52b\u0026#34;}.fa-dot-circle:before{content:\u0026#34;\\f192\u0026#34;}.fa-dove:before{content:\u0026#34;\\f4ba\u0026#34;}.fa-download:before{content:\u0026#34;\\f019\u0026#34;}.fa-draft2digital:before{content:\u0026#34;\\f396\u0026#34;}.fa-drafting-compass:before{content:\u0026#34;\\f568\u0026#34;}.fa-dragon:before{content:\u0026#34;\\f6d5\u0026#34;}.fa-draw-polygon:before{content:\u0026#34;\\f5ee\u0026#34;}.fa-dribbble:before{content:\u0026#34;\\f17d\u0026#34;}.fa-dribbble-square:before{content:\u0026#34;\\f397\u0026#34;}.fa-dropbox:before{content:\u0026#34;\\f16b\u0026#34;}.fa-drum:before{content:\u0026#34;\\f569\u0026#34;}.fa-drum-steelpan:before{content:\u0026#34;\\f56a\u0026#34;}.fa-drumstick-bite:before{content:\u0026#34;\\f6d7\u0026#34;}.fa-drupal:before{content:\u0026#34;\\f1a9\u0026#34;}.fa-dumbbell:before{content:\u0026#34;\\f44b\u0026#34;}.fa-dumpster:before{content:\u0026#34;\\f793\u0026#34;}.fa-dumpster-fire:before{content:\u0026#34;\\f794\u0026#34;}.fa-dungeon:before{content:\u0026#34;\\f6d9\u0026#34;}.fa-dyalog:before{content:\u0026#34;\\f399\u0026#34;}.fa-earlybirds:before{content:\u0026#34;\\f39a\u0026#34;}.fa-ebay:before{content:\u0026#34;\\f4f4\u0026#34;}.fa-edge:before{content:\u0026#34;\\f282\u0026#34;}.fa-edge-legacy:before{content:\u0026#34;\\e078\u0026#34;}.fa-edit:before{content:\u0026#34;\\f044\u0026#34;}.fa-egg:before{content:\u0026#34;\\f7fb\u0026#34;}.fa-eject:before{content:\u0026#34;\\f052\u0026#34;}.fa-elementor:before{content:\u0026#34;\\f430\u0026#34;}.fa-ellipsis-h:before{content:\u0026#34;\\f141\u0026#34;}.fa-ellipsis-v:before{content:\u0026#34;\\f142\u0026#34;}.fa-ello:before{content:\u0026#34;\\f5f1\u0026#34;}.fa-ember:before{content:\u0026#34;\\f423\u0026#34;}.fa-empire:before{content:\u0026#34;\\f1d1\u0026#34;}.fa-envelope:before{content:\u0026#34;\\f0e0\u0026#34;}.fa-envelope-open:before{content:\u0026#34;\\f2b6\u0026#34;}.fa-envelope-open-text:before{content:\u0026#34;\\f658\u0026#34;}.fa-envelope-square:before{content:\u0026#34;\\f199\u0026#34;}.fa-envira:before{content:\u0026#34;\\f299\u0026#34;}.fa-equals:before{content:\u0026#34;\\f52c\u0026#34;}.fa-eraser:before{content:\u0026#34;\\f12d\u0026#34;}.fa-erlang:before{content:\u0026#34;\\f39d\u0026#34;}.fa-ethereum:before{content:\u0026#34;\\f42e\u0026#34;}.fa-ethernet:before{content:\u0026#34;\\f796\u0026#34;}.fa-etsy:before{content:\u0026#34;\\f2d7\u0026#34;}.fa-euro-sign:before{content:\u0026#34;\\f153\u0026#34;}.fa-evernote:before{content:\u0026#34;\\f839\u0026#34;}.fa-exchange-alt:before{content:\u0026#34;\\f362\u0026#34;}.fa-exclamation:before{content:\u0026#34;\\f12a\u0026#34;}.fa-exclamation-circle:before{content:\u0026#34;\\f06a\u0026#34;}.fa-exclamation-triangle:before{content:\u0026#34;\\f071\u0026#34;}.fa-expand:before{content:\u0026#34;\\f065\u0026#34;}.fa-expand-alt:before{content:\u0026#34;\\f424\u0026#34;}.fa-expand-arrows-alt:before{content:\u0026#34;\\f31e\u0026#34;}.fa-expeditedssl:before{content:\u0026#34;\\f23e\u0026#34;}.fa-external-link-alt:before{content:\u0026#34;\\f35d\u0026#34;}.fa-external-link-square-alt:before{content:\u0026#34;\\f360\u0026#34;}.fa-eye:before{content:\u0026#34;\\f06e\u0026#34;}.fa-eye-dropper:before{content:\u0026#34;\\f1fb\u0026#34;}.fa-eye-slash:before{content:\u0026#34;\\f070\u0026#34;}.fa-facebook:before{content:\u0026#34;\\f09a\u0026#34;}.fa-facebook-f:before{content:\u0026#34;\\f39e\u0026#34;}.fa-facebook-messenger:before{content:\u0026#34;\\f39f\u0026#34;}.fa-facebook-square:before{content:\u0026#34;\\f082\u0026#34;}.fa-fan:before{content:\u0026#34;\\f863\u0026#34;}.fa-fantasy-flight-games:before{content:\u0026#34;\\f6dc\u0026#34;}.fa-fast-backward:before{content:\u0026#34;\\f049\u0026#34;}.fa-fast-forward:before{content:\u0026#34;\\f050\u0026#34;}.fa-faucet:before{content:\u0026#34;\\e005\u0026#34;}.fa-fax:before{content:\u0026#34;\\f1ac\u0026#34;}.fa-feather:before{content:\u0026#34;\\f52d\u0026#34;}.fa-feather-alt:before{content:\u0026#34;\\f56b\u0026#34;}.fa-fedex:before{content:\u0026#34;\\f797\u0026#34;}.fa-fedora:before{content:\u0026#34;\\f798\u0026#34;}.fa-female:before{content:\u0026#34;\\f182\u0026#34;}.fa-fighter-jet:before{content:\u0026#34;\\f0fb\u0026#34;}.fa-figma:before{content:\u0026#34;\\f799\u0026#34;}.fa-file:before{content:\u0026#34;\\f15b\u0026#34;}.fa-file-alt:before{content:\u0026#34;\\f15c\u0026#34;}.fa-file-archive:before{content:\u0026#34;\\f1c6\u0026#34;}.fa-file-audio:before{content:\u0026#34;\\f1c7\u0026#34;}.fa-file-code:before{content:\u0026#34;\\f1c9\u0026#34;}.fa-file-contract:before{content:\u0026#34;\\f56c\u0026#34;}.fa-file-csv:before{content:\u0026#34;\\f6dd\u0026#34;}.fa-file-download:before{content:\u0026#34;\\f56d\u0026#34;}.fa-file-excel:before{content:\u0026#34;\\f1c3\u0026#34;}.fa-file-export:before{content:\u0026#34;\\f56e\u0026#34;}.fa-file-image:before{content:\u0026#34;\\f1c5\u0026#34;}.fa-file-import:before{content:\u0026#34;\\f56f\u0026#34;}.fa-file-invoice:before{content:\u0026#34;\\f570\u0026#34;}.fa-file-invoice-dollar:before{content:\u0026#34;\\f571\u0026#34;}.fa-file-medical:before{content:\u0026#34;\\f477\u0026#34;}.fa-file-medical-alt:before{content:\u0026#34;\\f478\u0026#34;}.fa-file-pdf:before{content:\u0026#34;\\f1c1\u0026#34;}.fa-file-powerpoint:before{content:\u0026#34;\\f1c4\u0026#34;}.fa-file-prescription:before{content:\u0026#34;\\f572\u0026#34;}.fa-file-signature:before{content:\u0026#34;\\f573\u0026#34;}.fa-file-upload:before{content:\u0026#34;\\f574\u0026#34;}.fa-file-video:before{content:\u0026#34;\\f1c8\u0026#34;}.fa-file-word:before{content:\u0026#34;\\f1c2\u0026#34;}.fa-fill:before{content:\u0026#34;\\f575\u0026#34;}.fa-fill-drip:before{content:\u0026#34;\\f576\u0026#34;}.fa-film:before{content:\u0026#34;\\f008\u0026#34;}.fa-filter:before{content:\u0026#34;\\f0b0\u0026#34;}.fa-fingerprint:before{content:\u0026#34;\\f577\u0026#34;}.fa-fire:before{content:\u0026#34;\\f06d\u0026#34;}.fa-fire-alt:before{content:\u0026#34;\\f7e4\u0026#34;}.fa-fire-extinguisher:before{content:\u0026#34;\\f134\u0026#34;}.fa-firefox:before{content:\u0026#34;\\f269\u0026#34;}.fa-firefox-browser:before{content:\u0026#34;\\e007\u0026#34;}.fa-first-aid:before{content:\u0026#34;\\f479\u0026#34;}.fa-first-order:before{content:\u0026#34;\\f2b0\u0026#34;}.fa-first-order-alt:before{content:\u0026#34;\\f50a\u0026#34;}.fa-firstdraft:before{content:\u0026#34;\\f3a1\u0026#34;}.fa-fish:before{content:\u0026#34;\\f578\u0026#34;}.fa-fist-raised:before{content:\u0026#34;\\f6de\u0026#34;}.fa-flag:before{content:\u0026#34;\\f024\u0026#34;}.fa-flag-checkered:before{content:\u0026#34;\\f11e\u0026#34;}.fa-flag-usa:before{content:\u0026#34;\\f74d\u0026#34;}.fa-flask:before{content:\u0026#34;\\f0c3\u0026#34;}.fa-flickr:before{content:\u0026#34;\\f16e\u0026#34;}.fa-flipboard:before{content:\u0026#34;\\f44d\u0026#34;}.fa-flushed:before{content:\u0026#34;\\f579\u0026#34;}.fa-fly:before{content:\u0026#34;\\f417\u0026#34;}.fa-folder:before{content:\u0026#34;\\f07b\u0026#34;}.fa-folder-minus:before{content:\u0026#34;\\f65d\u0026#34;}.fa-folder-open:before{content:\u0026#34;\\f07c\u0026#34;}.fa-folder-plus:before{content:\u0026#34;\\f65e\u0026#34;}.fa-font:before{content:\u0026#34;\\f031\u0026#34;}.fa-font-awesome:before{content:\u0026#34;\\f2b4\u0026#34;}.fa-font-awesome-alt:before{content:\u0026#34;\\f35c\u0026#34;}.fa-font-awesome-flag:before{content:\u0026#34;\\f425\u0026#34;}.fa-font-awesome-logo-full:before{content:\u0026#34;\\f4e6\u0026#34;}.fa-fonticons:before{content:\u0026#34;\\f280\u0026#34;}.fa-fonticons-fi:before{content:\u0026#34;\\f3a2\u0026#34;}.fa-football-ball:before{content:\u0026#34;\\f44e\u0026#34;}.fa-fort-awesome:before{content:\u0026#34;\\f286\u0026#34;}.fa-fort-awesome-alt:before{content:\u0026#34;\\f3a3\u0026#34;}.fa-forumbee:before{content:\u0026#34;\\f211\u0026#34;}.fa-forward:before{content:\u0026#34;\\f04e\u0026#34;}.fa-foursquare:before{content:\u0026#34;\\f180\u0026#34;}.fa-free-code-camp:before{content:\u0026#34;\\f2c5\u0026#34;}.fa-freebsd:before{content:\u0026#34;\\f3a4\u0026#34;}.fa-frog:before{content:\u0026#34;\\f52e\u0026#34;}.fa-frown:before{content:\u0026#34;\\f119\u0026#34;}.fa-frown-open:before{content:\u0026#34;\\f57a\u0026#34;}.fa-fulcrum:before{content:\u0026#34;\\f50b\u0026#34;}.fa-funnel-dollar:before{content:\u0026#34;\\f662\u0026#34;}.fa-futbol:before{content:\u0026#34;\\f1e3\u0026#34;}.fa-galactic-republic:before{content:\u0026#34;\\f50c\u0026#34;}.fa-galactic-senate:before{content:\u0026#34;\\f50d\u0026#34;}.fa-gamepad:before{content:\u0026#34;\\f11b\u0026#34;}.fa-gas-pump:before{content:\u0026#34;\\f52f\u0026#34;}.fa-gavel:before{content:\u0026#34;\\f0e3\u0026#34;}.fa-gem:before{content:\u0026#34;\\f3a5\u0026#34;}.fa-genderless:before{content:\u0026#34;\\f22d\u0026#34;}.fa-get-pocket:before{content:\u0026#34;\\f265\u0026#34;}.fa-gg:before{content:\u0026#34;\\f260\u0026#34;}.fa-gg-circle:before{content:\u0026#34;\\f261\u0026#34;}.fa-ghost:before{content:\u0026#34;\\f6e2\u0026#34;}.fa-gift:before{content:\u0026#34;\\f06b\u0026#34;}.fa-gifts:before{content:\u0026#34;\\f79c\u0026#34;}.fa-git:before{content:\u0026#34;\\f1d3\u0026#34;}.fa-git-alt:before{content:\u0026#34;\\f841\u0026#34;}.fa-git-square:before{content:\u0026#34;\\f1d2\u0026#34;}.fa-github:before{content:\u0026#34;\\f09b\u0026#34;}.fa-github-alt:before{content:\u0026#34;\\f113\u0026#34;}.fa-github-square:before{content:\u0026#34;\\f092\u0026#34;}.fa-gitkraken:before{content:\u0026#34;\\f3a6\u0026#34;}.fa-gitlab:before{content:\u0026#34;\\f296\u0026#34;}.fa-gitter:before{content:\u0026#34;\\f426\u0026#34;}.fa-glass-cheers:before{content:\u0026#34;\\f79f\u0026#34;}.fa-glass-martini:before{content:\u0026#34;\\f000\u0026#34;}.fa-glass-martini-alt:before{content:\u0026#34;\\f57b\u0026#34;}.fa-glass-whiskey:before{content:\u0026#34;\\f7a0\u0026#34;}.fa-glasses:before{content:\u0026#34;\\f530\u0026#34;}.fa-glide:before{content:\u0026#34;\\f2a5\u0026#34;}.fa-glide-g:before{content:\u0026#34;\\f2a6\u0026#34;}.fa-globe:before{content:\u0026#34;\\f0ac\u0026#34;}.fa-globe-africa:before{content:\u0026#34;\\f57c\u0026#34;}.fa-globe-americas:before{content:\u0026#34;\\f57d\u0026#34;}.fa-globe-asia:before{content:\u0026#34;\\f57e\u0026#34;}.fa-globe-europe:before{content:\u0026#34;\\f7a2\u0026#34;}.fa-gofore:before{content:\u0026#34;\\f3a7\u0026#34;}.fa-golf-ball:before{content:\u0026#34;\\f450\u0026#34;}.fa-goodreads:before{content:\u0026#34;\\f3a8\u0026#34;}.fa-goodreads-g:before{content:\u0026#34;\\f3a9\u0026#34;}.fa-google:before{content:\u0026#34;\\f1a0\u0026#34;}.fa-google-drive:before{content:\u0026#34;\\f3aa\u0026#34;}.fa-google-pay:before{content:\u0026#34;\\e079\u0026#34;}.fa-google-play:before{content:\u0026#34;\\f3ab\u0026#34;}.fa-google-plus:before{content:\u0026#34;\\f2b3\u0026#34;}.fa-google-plus-g:before{content:\u0026#34;\\f0d5\u0026#34;}.fa-google-plus-square:before{content:\u0026#34;\\f0d4\u0026#34;}.fa-google-wallet:before{content:\u0026#34;\\f1ee\u0026#34;}.fa-gopuram:before{content:\u0026#34;\\f664\u0026#34;}.fa-graduation-cap:before{content:\u0026#34;\\f19d\u0026#34;}.fa-gratipay:before{content:\u0026#34;\\f184\u0026#34;}.fa-grav:before{content:\u0026#34;\\f2d6\u0026#34;}.fa-greater-than:before{content:\u0026#34;\\f531\u0026#34;}.fa-greater-than-equal:before{content:\u0026#34;\\f532\u0026#34;}.fa-grimace:before{content:\u0026#34;\\f57f\u0026#34;}.fa-grin:before{content:\u0026#34;\\f580\u0026#34;}.fa-grin-alt:before{content:\u0026#34;\\f581\u0026#34;}.fa-grin-beam:before{content:\u0026#34;\\f582\u0026#34;}.fa-grin-beam-sweat:before{content:\u0026#34;\\f583\u0026#34;}.fa-grin-hearts:before{content:\u0026#34;\\f584\u0026#34;}.fa-grin-squint:before{content:\u0026#34;\\f585\u0026#34;}.fa-grin-squint-tears:before{content:\u0026#34;\\f586\u0026#34;}.fa-grin-stars:before{content:\u0026#34;\\f587\u0026#34;}.fa-grin-tears:before{content:\u0026#34;\\f588\u0026#34;}.fa-grin-tongue:before{content:\u0026#34;\\f589\u0026#34;}.fa-grin-tongue-squint:before{content:\u0026#34;\\f58a\u0026#34;}.fa-grin-tongue-wink:before{content:\u0026#34;\\f58b\u0026#34;}.fa-grin-wink:before{content:\u0026#34;\\f58c\u0026#34;}.fa-grip-horizontal:before{content:\u0026#34;\\f58d\u0026#34;}.fa-grip-lines:before{content:\u0026#34;\\f7a4\u0026#34;}.fa-grip-lines-vertical:before{content:\u0026#34;\\f7a5\u0026#34;}.fa-grip-vertical:before{content:\u0026#34;\\f58e\u0026#34;}.fa-gripfire:before{content:\u0026#34;\\f3ac\u0026#34;}.fa-grunt:before{content:\u0026#34;\\f3ad\u0026#34;}.fa-guilded:before{content:\u0026#34;\\e07e\u0026#34;}.fa-guitar:before{content:\u0026#34;\\f7a6\u0026#34;}.fa-gulp:before{content:\u0026#34;\\f3ae\u0026#34;}.fa-h-square:before{content:\u0026#34;\\f0fd\u0026#34;}.fa-hacker-news:before{content:\u0026#34;\\f1d4\u0026#34;}.fa-hacker-news-square:before{content:\u0026#34;\\f3af\u0026#34;}.fa-hackerrank:before{content:\u0026#34;\\f5f7\u0026#34;}.fa-hamburger:before{content:\u0026#34;\\f805\u0026#34;}.fa-hammer:before{content:\u0026#34;\\f6e3\u0026#34;}.fa-hamsa:before{content:\u0026#34;\\f665\u0026#34;}.fa-hand-holding:before{content:\u0026#34;\\f4bd\u0026#34;}.fa-hand-holding-heart:before{content:\u0026#34;\\f4be\u0026#34;}.fa-hand-holding-medical:before{content:\u0026#34;\\e05c\u0026#34;}.fa-hand-holding-usd:before{content:\u0026#34;\\f4c0\u0026#34;}.fa-hand-holding-water:before{content:\u0026#34;\\f4c1\u0026#34;}.fa-hand-lizard:before{content:\u0026#34;\\f258\u0026#34;}.fa-hand-middle-finger:before{content:\u0026#34;\\f806\u0026#34;}.fa-hand-paper:before{content:\u0026#34;\\f256\u0026#34;}.fa-hand-peace:before{content:\u0026#34;\\f25b\u0026#34;}.fa-hand-point-down:before{content:\u0026#34;\\f0a7\u0026#34;}.fa-hand-point-left:before{content:\u0026#34;\\f0a5\u0026#34;}.fa-hand-point-right:before{content:\u0026#34;\\f0a4\u0026#34;}.fa-hand-point-up:before{content:\u0026#34;\\f0a6\u0026#34;}.fa-hand-pointer:before{content:\u0026#34;\\f25a\u0026#34;}.fa-hand-rock:before{content:\u0026#34;\\f255\u0026#34;}.fa-hand-scissors:before{content:\u0026#34;\\f257\u0026#34;}.fa-hand-sparkles:before{content:\u0026#34;\\e05d\u0026#34;}.fa-hand-spock:before{content:\u0026#34;\\f259\u0026#34;}.fa-hands:before{content:\u0026#34;\\f4c2\u0026#34;}.fa-hands-helping:before{content:\u0026#34;\\f4c4\u0026#34;}.fa-hands-wash:before{content:\u0026#34;\\e05e\u0026#34;}.fa-handshake:before{content:\u0026#34;\\f2b5\u0026#34;}.fa-handshake-alt-slash:before{content:\u0026#34;\\e05f\u0026#34;}.fa-handshake-slash:before{content:\u0026#34;\\e060\u0026#34;}.fa-hanukiah:before{content:\u0026#34;\\f6e6\u0026#34;}.fa-hard-hat:before{content:\u0026#34;\\f807\u0026#34;}.fa-hashtag:before{content:\u0026#34;\\f292\u0026#34;}.fa-hat-cowboy:before{content:\u0026#34;\\f8c0\u0026#34;}.fa-hat-cowboy-side:before{content:\u0026#34;\\f8c1\u0026#34;}.fa-hat-wizard:before{content:\u0026#34;\\f6e8\u0026#34;}.fa-hdd:before{content:\u0026#34;\\f0a0\u0026#34;}.fa-head-side-cough:before{content:\u0026#34;\\e061\u0026#34;}.fa-head-side-cough-slash:before{content:\u0026#34;\\e062\u0026#34;}.fa-head-side-mask:before{content:\u0026#34;\\e063\u0026#34;}.fa-head-side-virus:before{content:\u0026#34;\\e064\u0026#34;}.fa-heading:before{content:\u0026#34;\\f1dc\u0026#34;}.fa-headphones:before{content:\u0026#34;\\f025\u0026#34;}.fa-headphones-alt:before{content:\u0026#34;\\f58f\u0026#34;}.fa-headset:before{content:\u0026#34;\\f590\u0026#34;}.fa-heart:before{content:\u0026#34;\\f004\u0026#34;}.fa-heart-broken:before{content:\u0026#34;\\f7a9\u0026#34;}.fa-heartbeat:before{content:\u0026#34;\\f21e\u0026#34;}.fa-helicopter:before{content:\u0026#34;\\f533\u0026#34;}.fa-highlighter:before{content:\u0026#34;\\f591\u0026#34;}.fa-hiking:before{content:\u0026#34;\\f6ec\u0026#34;}.fa-hippo:before{content:\u0026#34;\\f6ed\u0026#34;}.fa-hips:before{content:\u0026#34;\\f452\u0026#34;}.fa-hire-a-helper:before{content:\u0026#34;\\f3b0\u0026#34;}.fa-history:before{content:\u0026#34;\\f1da\u0026#34;}.fa-hive:before{content:\u0026#34;\\e07f\u0026#34;}.fa-hockey-puck:before{content:\u0026#34;\\f453\u0026#34;}.fa-holly-berry:before{content:\u0026#34;\\f7aa\u0026#34;}.fa-home:before{content:\u0026#34;\\f015\u0026#34;}.fa-hooli:before{content:\u0026#34;\\f427\u0026#34;}.fa-hornbill:before{content:\u0026#34;\\f592\u0026#34;}.fa-horse:before{content:\u0026#34;\\f6f0\u0026#34;}.fa-horse-head:before{content:\u0026#34;\\f7ab\u0026#34;}.fa-hospital:before{content:\u0026#34;\\f0f8\u0026#34;}.fa-hospital-alt:before{content:\u0026#34;\\f47d\u0026#34;}.fa-hospital-symbol:before{content:\u0026#34;\\f47e\u0026#34;}.fa-hospital-user:before{content:\u0026#34;\\f80d\u0026#34;}.fa-hot-tub:before{content:\u0026#34;\\f593\u0026#34;}.fa-hotdog:before{content:\u0026#34;\\f80f\u0026#34;}.fa-hotel:before{content:\u0026#34;\\f594\u0026#34;}.fa-hotjar:before{content:\u0026#34;\\f3b1\u0026#34;}.fa-hourglass:before{content:\u0026#34;\\f254\u0026#34;}.fa-hourglass-end:before{content:\u0026#34;\\f253\u0026#34;}.fa-hourglass-half:before{content:\u0026#34;\\f252\u0026#34;}.fa-hourglass-start:before{content:\u0026#34;\\f251\u0026#34;}.fa-house-damage:before{content:\u0026#34;\\f6f1\u0026#34;}.fa-house-user:before{content:\u0026#34;\\e065\u0026#34;}.fa-houzz:before{content:\u0026#34;\\f27c\u0026#34;}.fa-hryvnia:before{content:\u0026#34;\\f6f2\u0026#34;}.fa-html5:before{content:\u0026#34;\\f13b\u0026#34;}.fa-hubspot:before{content:\u0026#34;\\f3b2\u0026#34;}.fa-i-cursor:before{content:\u0026#34;\\f246\u0026#34;}.fa-ice-cream:before{content:\u0026#34;\\f810\u0026#34;}.fa-icicles:before{content:\u0026#34;\\f7ad\u0026#34;}.fa-icons:before{content:\u0026#34;\\f86d\u0026#34;}.fa-id-badge:before{content:\u0026#34;\\f2c1\u0026#34;}.fa-id-card:before{content:\u0026#34;\\f2c2\u0026#34;}.fa-id-card-alt:before{content:\u0026#34;\\f47f\u0026#34;}.fa-ideal:before{content:\u0026#34;\\e013\u0026#34;}.fa-igloo:before{content:\u0026#34;\\f7ae\u0026#34;}.fa-image:before{content:\u0026#34;\\f03e\u0026#34;}.fa-images:before{content:\u0026#34;\\f302\u0026#34;}.fa-imdb:before{content:\u0026#34;\\f2d8\u0026#34;}.fa-inbox:before{content:\u0026#34;\\f01c\u0026#34;}.fa-indent:before{content:\u0026#34;\\f03c\u0026#34;}.fa-industry:before{content:\u0026#34;\\f275\u0026#34;}.fa-infinity:before{content:\u0026#34;\\f534\u0026#34;}.fa-info:before{content:\u0026#34;\\f129\u0026#34;}.fa-info-circle:before{content:\u0026#34;\\f05a\u0026#34;}.fa-innosoft:before{content:\u0026#34;\\e080\u0026#34;}.fa-instagram:before{content:\u0026#34;\\f16d\u0026#34;}.fa-instagram-square:before{content:\u0026#34;\\e055\u0026#34;}.fa-instalod:before{content:\u0026#34;\\e081\u0026#34;}.fa-intercom:before{content:\u0026#34;\\f7af\u0026#34;}.fa-internet-explorer:before{content:\u0026#34;\\f26b\u0026#34;}.fa-invision:before{content:\u0026#34;\\f7b0\u0026#34;}.fa-ioxhost:before{content:\u0026#34;\\f208\u0026#34;}.fa-italic:before{content:\u0026#34;\\f033\u0026#34;}.fa-itch-io:before{content:\u0026#34;\\f83a\u0026#34;}.fa-itunes:before{content:\u0026#34;\\f3b4\u0026#34;}.fa-itunes-note:before{content:\u0026#34;\\f3b5\u0026#34;}.fa-java:before{content:\u0026#34;\\f4e4\u0026#34;}.fa-jedi:before{content:\u0026#34;\\f669\u0026#34;}.fa-jedi-order:before{content:\u0026#34;\\f50e\u0026#34;}.fa-jenkins:before{content:\u0026#34;\\f3b6\u0026#34;}.fa-jira:before{content:\u0026#34;\\f7b1\u0026#34;}.fa-joget:before{content:\u0026#34;\\f3b7\u0026#34;}.fa-joint:before{content:\u0026#34;\\f595\u0026#34;}.fa-joomla:before{content:\u0026#34;\\f1aa\u0026#34;}.fa-journal-whills:before{content:\u0026#34;\\f66a\u0026#34;}.fa-js:before{content:\u0026#34;\\f3b8\u0026#34;}.fa-js-square:before{content:\u0026#34;\\f3b9\u0026#34;}.fa-jsfiddle:before{content:\u0026#34;\\f1cc\u0026#34;}.fa-kaaba:before{content:\u0026#34;\\f66b\u0026#34;}.fa-kaggle:before{content:\u0026#34;\\f5fa\u0026#34;}.fa-key:before{content:\u0026#34;\\f084\u0026#34;}.fa-keybase:before{content:\u0026#34;\\f4f5\u0026#34;}.fa-keyboard:before{content:\u0026#34;\\f11c\u0026#34;}.fa-keycdn:before{content:\u0026#34;\\f3ba\u0026#34;}.fa-khanda:before{content:\u0026#34;\\f66d\u0026#34;}.fa-kickstarter:before{content:\u0026#34;\\f3bb\u0026#34;}.fa-kickstarter-k:before{content:\u0026#34;\\f3bc\u0026#34;}.fa-kiss:before{content:\u0026#34;\\f596\u0026#34;}.fa-kiss-beam:before{content:\u0026#34;\\f597\u0026#34;}.fa-kiss-wink-heart:before{content:\u0026#34;\\f598\u0026#34;}.fa-kiwi-bird:before{content:\u0026#34;\\f535\u0026#34;}.fa-korvue:before{content:\u0026#34;\\f42f\u0026#34;}.fa-landmark:before{content:\u0026#34;\\f66f\u0026#34;}.fa-language:before{content:\u0026#34;\\f1ab\u0026#34;}.fa-laptop:before{content:\u0026#34;\\f109\u0026#34;}.fa-laptop-code:before{content:\u0026#34;\\f5fc\u0026#34;}.fa-laptop-house:before{content:\u0026#34;\\e066\u0026#34;}.fa-laptop-medical:before{content:\u0026#34;\\f812\u0026#34;}.fa-laravel:before{content:\u0026#34;\\f3bd\u0026#34;}.fa-lastfm:before{content:\u0026#34;\\f202\u0026#34;}.fa-lastfm-square:before{content:\u0026#34;\\f203\u0026#34;}.fa-laugh:before{content:\u0026#34;\\f599\u0026#34;}.fa-laugh-beam:before{content:\u0026#34;\\f59a\u0026#34;}.fa-laugh-squint:before{content:\u0026#34;\\f59b\u0026#34;}.fa-laugh-wink:before{content:\u0026#34;\\f59c\u0026#34;}.fa-layer-group:before{content:\u0026#34;\\f5fd\u0026#34;}.fa-leaf:before{content:\u0026#34;\\f06c\u0026#34;}.fa-leanpub:before{content:\u0026#34;\\f212\u0026#34;}.fa-lemon:before{content:\u0026#34;\\f094\u0026#34;}.fa-less:before{content:\u0026#34;\\f41d\u0026#34;}.fa-less-than:before{content:\u0026#34;\\f536\u0026#34;}.fa-less-than-equal:before{content:\u0026#34;\\f537\u0026#34;}.fa-level-down-alt:before{content:\u0026#34;\\f3be\u0026#34;}.fa-level-up-alt:before{content:\u0026#34;\\f3bf\u0026#34;}.fa-life-ring:before{content:\u0026#34;\\f1cd\u0026#34;}.fa-lightbulb:before{content:\u0026#34;\\f0eb\u0026#34;}.fa-line:before{content:\u0026#34;\\f3c0\u0026#34;}.fa-link:before{content:\u0026#34;\\f0c1\u0026#34;}.fa-linkedin:before{content:\u0026#34;\\f08c\u0026#34;}.fa-linkedin-in:before{content:\u0026#34;\\f0e1\u0026#34;}.fa-linode:before{content:\u0026#34;\\f2b8\u0026#34;}.fa-linux:before{content:\u0026#34;\\f17c\u0026#34;}.fa-lira-sign:before{content:\u0026#34;\\f195\u0026#34;}.fa-list:before{content:\u0026#34;\\f03a\u0026#34;}.fa-list-alt:before{content:\u0026#34;\\f022\u0026#34;}.fa-list-ol:before{content:\u0026#34;\\f0cb\u0026#34;}.fa-list-ul:before{content:\u0026#34;\\f0ca\u0026#34;}.fa-location-arrow:before{content:\u0026#34;\\f124\u0026#34;}.fa-lock:before{content:\u0026#34;\\f023\u0026#34;}.fa-lock-open:before{content:\u0026#34;\\f3c1\u0026#34;}.fa-long-arrow-alt-down:before{content:\u0026#34;\\f309\u0026#34;}.fa-long-arrow-alt-left:before{content:\u0026#34;\\f30a\u0026#34;}.fa-long-arrow-alt-right:before{content:\u0026#34;\\f30b\u0026#34;}.fa-long-arrow-alt-up:before{content:\u0026#34;\\f30c\u0026#34;}.fa-low-vision:before{content:\u0026#34;\\f2a8\u0026#34;}.fa-luggage-cart:before{content:\u0026#34;\\f59d\u0026#34;}.fa-lungs:before{content:\u0026#34;\\f604\u0026#34;}.fa-lungs-virus:before{content:\u0026#34;\\e067\u0026#34;}.fa-lyft:before{content:\u0026#34;\\f3c3\u0026#34;}.fa-magento:before{content:\u0026#34;\\f3c4\u0026#34;}.fa-magic:before{content:\u0026#34;\\f0d0\u0026#34;}.fa-magnet:before{content:\u0026#34;\\f076\u0026#34;}.fa-mail-bulk:before{content:\u0026#34;\\f674\u0026#34;}.fa-mailchimp:before{content:\u0026#34;\\f59e\u0026#34;}.fa-male:before{content:\u0026#34;\\f183\u0026#34;}.fa-mandalorian:before{content:\u0026#34;\\f50f\u0026#34;}.fa-map:before{content:\u0026#34;\\f279\u0026#34;}.fa-map-marked:before{content:\u0026#34;\\f59f\u0026#34;}.fa-map-marked-alt:before{content:\u0026#34;\\f5a0\u0026#34;}.fa-map-marker:before{content:\u0026#34;\\f041\u0026#34;}.fa-map-marker-alt:before{content:\u0026#34;\\f3c5\u0026#34;}.fa-map-pin:before{content:\u0026#34;\\f276\u0026#34;}.fa-map-signs:before{content:\u0026#34;\\f277\u0026#34;}.fa-markdown:before{content:\u0026#34;\\f60f\u0026#34;}.fa-marker:before{content:\u0026#34;\\f5a1\u0026#34;}.fa-mars:before{content:\u0026#34;\\f222\u0026#34;}.fa-mars-double:before{content:\u0026#34;\\f227\u0026#34;}.fa-mars-stroke:before{content:\u0026#34;\\f229\u0026#34;}.fa-mars-stroke-h:before{content:\u0026#34;\\f22b\u0026#34;}.fa-mars-stroke-v:before{content:\u0026#34;\\f22a\u0026#34;}.fa-mask:before{content:\u0026#34;\\f6fa\u0026#34;}.fa-mastodon:before{content:\u0026#34;\\f4f6\u0026#34;}.fa-maxcdn:before{content:\u0026#34;\\f136\u0026#34;}.fa-mdb:before{content:\u0026#34;\\f8ca\u0026#34;}.fa-medal:before{content:\u0026#34;\\f5a2\u0026#34;}.fa-medapps:before{content:\u0026#34;\\f3c6\u0026#34;}.fa-medium:before{content:\u0026#34;\\f23a\u0026#34;}.fa-medium-m:before{content:\u0026#34;\\f3c7\u0026#34;}.fa-medkit:before{content:\u0026#34;\\f0fa\u0026#34;}.fa-medrt:before{content:\u0026#34;\\f3c8\u0026#34;}.fa-meetup:before{content:\u0026#34;\\f2e0\u0026#34;}.fa-megaport:before{content:\u0026#34;\\f5a3\u0026#34;}.fa-meh:before{content:\u0026#34;\\f11a\u0026#34;}.fa-meh-blank:before{content:\u0026#34;\\f5a4\u0026#34;}.fa-meh-rolling-eyes:before{content:\u0026#34;\\f5a5\u0026#34;}.fa-memory:before{content:\u0026#34;\\f538\u0026#34;}.fa-mendeley:before{content:\u0026#34;\\f7b3\u0026#34;}.fa-menorah:before{content:\u0026#34;\\f676\u0026#34;}.fa-mercury:before{content:\u0026#34;\\f223\u0026#34;}.fa-meteor:before{content:\u0026#34;\\f753\u0026#34;}.fa-microblog:before{content:\u0026#34;\\e01a\u0026#34;}.fa-microchip:before{content:\u0026#34;\\f2db\u0026#34;}.fa-microphone:before{content:\u0026#34;\\f130\u0026#34;}.fa-microphone-alt:before{content:\u0026#34;\\f3c9\u0026#34;}.fa-microphone-alt-slash:before{content:\u0026#34;\\f539\u0026#34;}.fa-microphone-slash:before{content:\u0026#34;\\f131\u0026#34;}.fa-microscope:before{content:\u0026#34;\\f610\u0026#34;}.fa-microsoft:before{content:\u0026#34;\\f3ca\u0026#34;}.fa-minus:before{content:\u0026#34;\\f068\u0026#34;}.fa-minus-circle:before{content:\u0026#34;\\f056\u0026#34;}.fa-minus-square:before{content:\u0026#34;\\f146\u0026#34;}.fa-mitten:before{content:\u0026#34;\\f7b5\u0026#34;}.fa-mix:before{content:\u0026#34;\\f3cb\u0026#34;}.fa-mixcloud:before{content:\u0026#34;\\f289\u0026#34;}.fa-mixer:before{content:\u0026#34;\\e056\u0026#34;}.fa-mizuni:before{content:\u0026#34;\\f3cc\u0026#34;}.fa-mobile:before{content:\u0026#34;\\f10b\u0026#34;}.fa-mobile-alt:before{content:\u0026#34;\\f3cd\u0026#34;}.fa-modx:before{content:\u0026#34;\\f285\u0026#34;}.fa-monero:before{content:\u0026#34;\\f3d0\u0026#34;}.fa-money-bill:before{content:\u0026#34;\\f0d6\u0026#34;}.fa-money-bill-alt:before{content:\u0026#34;\\f3d1\u0026#34;}.fa-money-bill-wave:before{content:\u0026#34;\\f53a\u0026#34;}.fa-money-bill-wave-alt:before{content:\u0026#34;\\f53b\u0026#34;}.fa-money-check:before{content:\u0026#34;\\f53c\u0026#34;}.fa-money-check-alt:before{content:\u0026#34;\\f53d\u0026#34;}.fa-monument:before{content:\u0026#34;\\f5a6\u0026#34;}.fa-moon:before{content:\u0026#34;\\f186\u0026#34;}.fa-mortar-pestle:before{content:\u0026#34;\\f5a7\u0026#34;}.fa-mosque:before{content:\u0026#34;\\f678\u0026#34;}.fa-motorcycle:before{content:\u0026#34;\\f21c\u0026#34;}.fa-mountain:before{content:\u0026#34;\\f6fc\u0026#34;}.fa-mouse:before{content:\u0026#34;\\f8cc\u0026#34;}.fa-mouse-pointer:before{content:\u0026#34;\\f245\u0026#34;}.fa-mug-hot:before{content:\u0026#34;\\f7b6\u0026#34;}.fa-music:before{content:\u0026#34;\\f001\u0026#34;}.fa-napster:before{content:\u0026#34;\\f3d2\u0026#34;}.fa-neos:before{content:\u0026#34;\\f612\u0026#34;}.fa-network-wired:before{content:\u0026#34;\\f6ff\u0026#34;}.fa-neuter:before{content:\u0026#34;\\f22c\u0026#34;}.fa-newspaper:before{content:\u0026#34;\\f1ea\u0026#34;}.fa-nimblr:before{content:\u0026#34;\\f5a8\u0026#34;}.fa-node:before{content:\u0026#34;\\f419\u0026#34;}.fa-node-js:before{content:\u0026#34;\\f3d3\u0026#34;}.fa-not-equal:before{content:\u0026#34;\\f53e\u0026#34;}.fa-notes-medical:before{content:\u0026#34;\\f481\u0026#34;}.fa-npm:before{content:\u0026#34;\\f3d4\u0026#34;}.fa-ns8:before{content:\u0026#34;\\f3d5\u0026#34;}.fa-nutritionix:before{content:\u0026#34;\\f3d6\u0026#34;}.fa-object-group:before{content:\u0026#34;\\f247\u0026#34;}.fa-object-ungroup:before{content:\u0026#34;\\f248\u0026#34;}.fa-octopus-deploy:before{content:\u0026#34;\\e082\u0026#34;}.fa-odnoklassniki:before{content:\u0026#34;\\f263\u0026#34;}.fa-odnoklassniki-square:before{content:\u0026#34;\\f264\u0026#34;}.fa-oil-can:before{content:\u0026#34;\\f613\u0026#34;}.fa-old-republic:before{content:\u0026#34;\\f510\u0026#34;}.fa-om:before{content:\u0026#34;\\f679\u0026#34;}.fa-opencart:before{content:\u0026#34;\\f23d\u0026#34;}.fa-openid:before{content:\u0026#34;\\f19b\u0026#34;}.fa-opera:before{content:\u0026#34;\\f26a\u0026#34;}.fa-optin-monster:before{content:\u0026#34;\\f23c\u0026#34;}.fa-orcid:before{content:\u0026#34;\\f8d2\u0026#34;}.fa-osi:before{content:\u0026#34;\\f41a\u0026#34;}.fa-otter:before{content:\u0026#34;\\f700\u0026#34;}.fa-outdent:before{content:\u0026#34;\\f03b\u0026#34;}.fa-page4:before{content:\u0026#34;\\f3d7\u0026#34;}.fa-pagelines:before{content:\u0026#34;\\f18c\u0026#34;}.fa-pager:before{content:\u0026#34;\\f815\u0026#34;}.fa-paint-brush:before{content:\u0026#34;\\f1fc\u0026#34;}.fa-paint-roller:before{content:\u0026#34;\\f5aa\u0026#34;}.fa-palette:before{content:\u0026#34;\\f53f\u0026#34;}.fa-palfed:before{content:\u0026#34;\\f3d8\u0026#34;}.fa-pallet:before{content:\u0026#34;\\f482\u0026#34;}.fa-paper-plane:before{content:\u0026#34;\\f1d8\u0026#34;}.fa-paperclip:before{content:\u0026#34;\\f0c6\u0026#34;}.fa-parachute-box:before{content:\u0026#34;\\f4cd\u0026#34;}.fa-paragraph:before{content:\u0026#34;\\f1dd\u0026#34;}.fa-parking:before{content:\u0026#34;\\f540\u0026#34;}.fa-passport:before{content:\u0026#34;\\f5ab\u0026#34;}.fa-pastafarianism:before{content:\u0026#34;\\f67b\u0026#34;}.fa-paste:before{content:\u0026#34;\\f0ea\u0026#34;}.fa-patreon:before{content:\u0026#34;\\f3d9\u0026#34;}.fa-pause:before{content:\u0026#34;\\f04c\u0026#34;}.fa-pause-circle:before{content:\u0026#34;\\f28b\u0026#34;}.fa-paw:before{content:\u0026#34;\\f1b0\u0026#34;}.fa-paypal:before{content:\u0026#34;\\f1ed\u0026#34;}.fa-peace:before{content:\u0026#34;\\f67c\u0026#34;}.fa-pen:before{content:\u0026#34;\\f304\u0026#34;}.fa-pen-alt:before{content:\u0026#34;\\f305\u0026#34;}.fa-pen-fancy:before{content:\u0026#34;\\f5ac\u0026#34;}.fa-pen-nib:before{content:\u0026#34;\\f5ad\u0026#34;}.fa-pen-square:before{content:\u0026#34;\\f14b\u0026#34;}.fa-pencil-alt:before{content:\u0026#34;\\f303\u0026#34;}.fa-pencil-ruler:before{content:\u0026#34;\\f5ae\u0026#34;}.fa-penny-arcade:before{content:\u0026#34;\\f704\u0026#34;}.fa-people-arrows:before{content:\u0026#34;\\e068\u0026#34;}.fa-people-carry:before{content:\u0026#34;\\f4ce\u0026#34;}.fa-pepper-hot:before{content:\u0026#34;\\f816\u0026#34;}.fa-perbyte:before{content:\u0026#34;\\e083\u0026#34;}.fa-percent:before{content:\u0026#34;\\f295\u0026#34;}.fa-percentage:before{content:\u0026#34;\\f541\u0026#34;}.fa-periscope:before{content:\u0026#34;\\f3da\u0026#34;}.fa-person-booth:before{content:\u0026#34;\\f756\u0026#34;}.fa-phabricator:before{content:\u0026#34;\\f3db\u0026#34;}.fa-phoenix-framework:before{content:\u0026#34;\\f3dc\u0026#34;}.fa-phoenix-squadron:before{content:\u0026#34;\\f511\u0026#34;}.fa-phone:before{content:\u0026#34;\\f095\u0026#34;}.fa-phone-alt:before{content:\u0026#34;\\f879\u0026#34;}.fa-phone-slash:before{content:\u0026#34;\\f3dd\u0026#34;}.fa-phone-square:before{content:\u0026#34;\\f098\u0026#34;}.fa-phone-square-alt:before{content:\u0026#34;\\f87b\u0026#34;}.fa-phone-volume:before{content:\u0026#34;\\f2a0\u0026#34;}.fa-photo-video:before{content:\u0026#34;\\f87c\u0026#34;}.fa-php:before{content:\u0026#34;\\f457\u0026#34;}.fa-pied-piper:before{content:\u0026#34;\\f2ae\u0026#34;}.fa-pied-piper-alt:before{content:\u0026#34;\\f1a8\u0026#34;}.fa-pied-piper-hat:before{content:\u0026#34;\\f4e5\u0026#34;}.fa-pied-piper-pp:before{content:\u0026#34;\\f1a7\u0026#34;}.fa-pied-piper-square:before{content:\u0026#34;\\e01e\u0026#34;}.fa-piggy-bank:before{content:\u0026#34;\\f4d3\u0026#34;}.fa-pills:before{content:\u0026#34;\\f484\u0026#34;}.fa-pinterest:before{content:\u0026#34;\\f0d2\u0026#34;}.fa-pinterest-p:before{content:\u0026#34;\\f231\u0026#34;}.fa-pinterest-square:before{content:\u0026#34;\\f0d3\u0026#34;}.fa-pizza-slice:before{content:\u0026#34;\\f818\u0026#34;}.fa-place-of-worship:before{content:\u0026#34;\\f67f\u0026#34;}.fa-plane:before{content:\u0026#34;\\f072\u0026#34;}.fa-plane-arrival:before{content:\u0026#34;\\f5af\u0026#34;}.fa-plane-departure:before{content:\u0026#34;\\f5b0\u0026#34;}.fa-plane-slash:before{content:\u0026#34;\\e069\u0026#34;}.fa-play:before{content:\u0026#34;\\f04b\u0026#34;}.fa-play-circle:before{content:\u0026#34;\\f144\u0026#34;}.fa-playstation:before{content:\u0026#34;\\f3df\u0026#34;}.fa-plug:before{content:\u0026#34;\\f1e6\u0026#34;}.fa-plus:before{content:\u0026#34;\\f067\u0026#34;}.fa-plus-circle:before{content:\u0026#34;\\f055\u0026#34;}.fa-plus-square:before{content:\u0026#34;\\f0fe\u0026#34;}.fa-podcast:before{content:\u0026#34;\\f2ce\u0026#34;}.fa-poll:before{content:\u0026#34;\\f681\u0026#34;}.fa-poll-h:before{content:\u0026#34;\\f682\u0026#34;}.fa-poo:before{content:\u0026#34;\\f2fe\u0026#34;}.fa-poo-storm:before{content:\u0026#34;\\f75a\u0026#34;}.fa-poop:before{content:\u0026#34;\\f619\u0026#34;}.fa-portrait:before{content:\u0026#34;\\f3e0\u0026#34;}.fa-pound-sign:before{content:\u0026#34;\\f154\u0026#34;}.fa-power-off:before{content:\u0026#34;\\f011\u0026#34;}.fa-pray:before{content:\u0026#34;\\f683\u0026#34;}.fa-praying-hands:before{content:\u0026#34;\\f684\u0026#34;}.fa-prescription:before{content:\u0026#34;\\f5b1\u0026#34;}.fa-prescription-bottle:before{content:\u0026#34;\\f485\u0026#34;}.fa-prescription-bottle-alt:before{content:\u0026#34;\\f486\u0026#34;}.fa-print:before{content:\u0026#34;\\f02f\u0026#34;}.fa-procedures:before{content:\u0026#34;\\f487\u0026#34;}.fa-product-hunt:before{content:\u0026#34;\\f288\u0026#34;}.fa-project-diagram:before{content:\u0026#34;\\f542\u0026#34;}.fa-pump-medical:before{content:\u0026#34;\\e06a\u0026#34;}.fa-pump-soap:before{content:\u0026#34;\\e06b\u0026#34;}.fa-pushed:before{content:\u0026#34;\\f3e1\u0026#34;}.fa-puzzle-piece:before{content:\u0026#34;\\f12e\u0026#34;}.fa-python:before{content:\u0026#34;\\f3e2\u0026#34;}.fa-qq:before{content:\u0026#34;\\f1d6\u0026#34;}.fa-qrcode:before{content:\u0026#34;\\f029\u0026#34;}.fa-question:before{content:\u0026#34;\\f128\u0026#34;}.fa-question-circle:before{content:\u0026#34;\\f059\u0026#34;}.fa-quidditch:before{content:\u0026#34;\\f458\u0026#34;}.fa-quinscape:before{content:\u0026#34;\\f459\u0026#34;}.fa-quora:before{content:\u0026#34;\\f2c4\u0026#34;}.fa-quote-left:before{content:\u0026#34;\\f10d\u0026#34;}.fa-quote-right:before{content:\u0026#34;\\f10e\u0026#34;}.fa-quran:before{content:\u0026#34;\\f687\u0026#34;}.fa-r-project:before{content:\u0026#34;\\f4f7\u0026#34;}.fa-radiation:before{content:\u0026#34;\\f7b9\u0026#34;}.fa-radiation-alt:before{content:\u0026#34;\\f7ba\u0026#34;}.fa-rainbow:before{content:\u0026#34;\\f75b\u0026#34;}.fa-random:before{content:\u0026#34;\\f074\u0026#34;}.fa-raspberry-pi:before{content:\u0026#34;\\f7bb\u0026#34;}.fa-ravelry:before{content:\u0026#34;\\f2d9\u0026#34;}.fa-react:before{content:\u0026#34;\\f41b\u0026#34;}.fa-reacteurope:before{content:\u0026#34;\\f75d\u0026#34;}.fa-readme:before{content:\u0026#34;\\f4d5\u0026#34;}.fa-rebel:before{content:\u0026#34;\\f1d0\u0026#34;}.fa-receipt:before{content:\u0026#34;\\f543\u0026#34;}.fa-record-vinyl:before{content:\u0026#34;\\f8d9\u0026#34;}.fa-recycle:before{content:\u0026#34;\\f1b8\u0026#34;}.fa-red-river:before{content:\u0026#34;\\f3e3\u0026#34;}.fa-reddit:before{content:\u0026#34;\\f1a1\u0026#34;}.fa-reddit-alien:before{content:\u0026#34;\\f281\u0026#34;}.fa-reddit-square:before{content:\u0026#34;\\f1a2\u0026#34;}.fa-redhat:before{content:\u0026#34;\\f7bc\u0026#34;}.fa-redo:before{content:\u0026#34;\\f01e\u0026#34;}.fa-redo-alt:before{content:\u0026#34;\\f2f9\u0026#34;}.fa-registered:before{content:\u0026#34;\\f25d\u0026#34;}.fa-remove-format:before{content:\u0026#34;\\f87d\u0026#34;}.fa-renren:before{content:\u0026#34;\\f18b\u0026#34;}.fa-reply:before{content:\u0026#34;\\f3e5\u0026#34;}.fa-reply-all:before{content:\u0026#34;\\f122\u0026#34;}.fa-replyd:before{content:\u0026#34;\\f3e6\u0026#34;}.fa-republican:before{content:\u0026#34;\\f75e\u0026#34;}.fa-researchgate:before{content:\u0026#34;\\f4f8\u0026#34;}.fa-resolving:before{content:\u0026#34;\\f3e7\u0026#34;}.fa-restroom:before{content:\u0026#34;\\f7bd\u0026#34;}.fa-retweet:before{content:\u0026#34;\\f079\u0026#34;}.fa-rev:before{content:\u0026#34;\\f5b2\u0026#34;}.fa-ribbon:before{content:\u0026#34;\\f4d6\u0026#34;}.fa-ring:before{content:\u0026#34;\\f70b\u0026#34;}.fa-road:before{content:\u0026#34;\\f018\u0026#34;}.fa-robot:before{content:\u0026#34;\\f544\u0026#34;}.fa-rocket:before{content:\u0026#34;\\f135\u0026#34;}.fa-rocketchat:before{content:\u0026#34;\\f3e8\u0026#34;}.fa-rockrms:before{content:\u0026#34;\\f3e9\u0026#34;}.fa-route:before{content:\u0026#34;\\f4d7\u0026#34;}.fa-rss:before{content:\u0026#34;\\f09e\u0026#34;}.fa-rss-square:before{content:\u0026#34;\\f143\u0026#34;}.fa-ruble-sign:before{content:\u0026#34;\\f158\u0026#34;}.fa-ruler:before{content:\u0026#34;\\f545\u0026#34;}.fa-ruler-combined:before{content:\u0026#34;\\f546\u0026#34;}.fa-ruler-horizontal:before{content:\u0026#34;\\f547\u0026#34;}.fa-ruler-vertical:before{content:\u0026#34;\\f548\u0026#34;}.fa-running:before{content:\u0026#34;\\f70c\u0026#34;}.fa-rupee-sign:before{content:\u0026#34;\\f156\u0026#34;}.fa-rust:before{content:\u0026#34;\\e07a\u0026#34;}.fa-sad-cry:before{content:\u0026#34;\\f5b3\u0026#34;}.fa-sad-tear:before{content:\u0026#34;\\f5b4\u0026#34;}.fa-safari:before{content:\u0026#34;\\f267\u0026#34;}.fa-salesforce:before{content:\u0026#34;\\f83b\u0026#34;}.fa-sass:before{content:\u0026#34;\\f41e\u0026#34;}.fa-satellite:before{content:\u0026#34;\\f7bf\u0026#34;}.fa-satellite-dish:before{content:\u0026#34;\\f7c0\u0026#34;}.fa-save:before{content:\u0026#34;\\f0c7\u0026#34;}.fa-schlix:before{content:\u0026#34;\\f3ea\u0026#34;}.fa-school:before{content:\u0026#34;\\f549\u0026#34;}.fa-screwdriver:before{content:\u0026#34;\\f54a\u0026#34;}.fa-scribd:before{content:\u0026#34;\\f28a\u0026#34;}.fa-scroll:before{content:\u0026#34;\\f70e\u0026#34;}.fa-sd-card:before{content:\u0026#34;\\f7c2\u0026#34;}.fa-search:before{content:\u0026#34;\\f002\u0026#34;}.fa-search-dollar:before{content:\u0026#34;\\f688\u0026#34;}.fa-search-location:before{content:\u0026#34;\\f689\u0026#34;}.fa-search-minus:before{content:\u0026#34;\\f010\u0026#34;}.fa-search-plus:before{content:\u0026#34;\\f00e\u0026#34;}.fa-searchengin:before{content:\u0026#34;\\f3eb\u0026#34;}.fa-seedling:before{content:\u0026#34;\\f4d8\u0026#34;}.fa-sellcast:before{content:\u0026#34;\\f2da\u0026#34;}.fa-sellsy:before{content:\u0026#34;\\f213\u0026#34;}.fa-server:before{content:\u0026#34;\\f233\u0026#34;}.fa-servicestack:before{content:\u0026#34;\\f3ec\u0026#34;}.fa-shapes:before{content:\u0026#34;\\f61f\u0026#34;}.fa-share:before{content:\u0026#34;\\f064\u0026#34;}.fa-share-alt:before{content:\u0026#34;\\f1e0\u0026#34;}.fa-share-alt-square:before{content:\u0026#34;\\f1e1\u0026#34;}.fa-share-square:before{content:\u0026#34;\\f14d\u0026#34;}.fa-shekel-sign:before{content:\u0026#34;\\f20b\u0026#34;}.fa-shield-alt:before{content:\u0026#34;\\f3ed\u0026#34;}.fa-shield-virus:before{content:\u0026#34;\\e06c\u0026#34;}.fa-ship:before{content:\u0026#34;\\f21a\u0026#34;}.fa-shipping-fast:before{content:\u0026#34;\\f48b\u0026#34;}.fa-shirtsinbulk:before{content:\u0026#34;\\f214\u0026#34;}.fa-shoe-prints:before{content:\u0026#34;\\f54b\u0026#34;}.fa-shopify:before{content:\u0026#34;\\e057\u0026#34;}.fa-shopping-bag:before{content:\u0026#34;\\f290\u0026#34;}.fa-shopping-basket:before{content:\u0026#34;\\f291\u0026#34;}.fa-shopping-cart:before{content:\u0026#34;\\f07a\u0026#34;}.fa-shopware:before{content:\u0026#34;\\f5b5\u0026#34;}.fa-shower:before{content:\u0026#34;\\f2cc\u0026#34;}.fa-shuttle-van:before{content:\u0026#34;\\f5b6\u0026#34;}.fa-sign:before{content:\u0026#34;\\f4d9\u0026#34;}.fa-sign-in-alt:before{content:\u0026#34;\\f2f6\u0026#34;}.fa-sign-language:before{content:\u0026#34;\\f2a7\u0026#34;}.fa-sign-out-alt:before{content:\u0026#34;\\f2f5\u0026#34;}.fa-signal:before{content:\u0026#34;\\f012\u0026#34;}.fa-signature:before{content:\u0026#34;\\f5b7\u0026#34;}.fa-sim-card:before{content:\u0026#34;\\f7c4\u0026#34;}.fa-simplybuilt:before{content:\u0026#34;\\f215\u0026#34;}.fa-sink:before{content:\u0026#34;\\e06d\u0026#34;}.fa-sistrix:before{content:\u0026#34;\\f3ee\u0026#34;}.fa-sitemap:before{content:\u0026#34;\\f0e8\u0026#34;}.fa-sith:before{content:\u0026#34;\\f512\u0026#34;}.fa-skating:before{content:\u0026#34;\\f7c5\u0026#34;}.fa-sketch:before{content:\u0026#34;\\f7c6\u0026#34;}.fa-skiing:before{content:\u0026#34;\\f7c9\u0026#34;}.fa-skiing-nordic:before{content:\u0026#34;\\f7ca\u0026#34;}.fa-skull:before{content:\u0026#34;\\f54c\u0026#34;}.fa-skull-crossbones:before{content:\u0026#34;\\f714\u0026#34;}.fa-skyatlas:before{content:\u0026#34;\\f216\u0026#34;}.fa-skype:before{content:\u0026#34;\\f17e\u0026#34;}.fa-slack:before{content:\u0026#34;\\f198\u0026#34;}.fa-slack-hash:before{content:\u0026#34;\\f3ef\u0026#34;}.fa-slash:before{content:\u0026#34;\\f715\u0026#34;}.fa-sleigh:before{content:\u0026#34;\\f7cc\u0026#34;}.fa-sliders-h:before{content:\u0026#34;\\f1de\u0026#34;}.fa-slideshare:before{content:\u0026#34;\\f1e7\u0026#34;}.fa-smile:before{content:\u0026#34;\\f118\u0026#34;}.fa-smile-beam:before{content:\u0026#34;\\f5b8\u0026#34;}.fa-smile-wink:before{content:\u0026#34;\\f4da\u0026#34;}.fa-smog:before{content:\u0026#34;\\f75f\u0026#34;}.fa-smoking:before{content:\u0026#34;\\f48d\u0026#34;}.fa-smoking-ban:before{content:\u0026#34;\\f54d\u0026#34;}.fa-sms:before{content:\u0026#34;\\f7cd\u0026#34;}.fa-snapchat:before{content:\u0026#34;\\f2ab\u0026#34;}.fa-snapchat-ghost:before{content:\u0026#34;\\f2ac\u0026#34;}.fa-snapchat-square:before{content:\u0026#34;\\f2ad\u0026#34;}.fa-snowboarding:before{content:\u0026#34;\\f7ce\u0026#34;}.fa-snowflake:before{content:\u0026#34;\\f2dc\u0026#34;}.fa-snowman:before{content:\u0026#34;\\f7d0\u0026#34;}.fa-snowplow:before{content:\u0026#34;\\f7d2\u0026#34;}.fa-soap:before{content:\u0026#34;\\e06e\u0026#34;}.fa-socks:before{content:\u0026#34;\\f696\u0026#34;}.fa-solar-panel:before{content:\u0026#34;\\f5ba\u0026#34;}.fa-sort:before{content:\u0026#34;\\f0dc\u0026#34;}.fa-sort-alpha-down:before{content:\u0026#34;\\f15d\u0026#34;}.fa-sort-alpha-down-alt:before{content:\u0026#34;\\f881\u0026#34;}.fa-sort-alpha-up:before{content:\u0026#34;\\f15e\u0026#34;}.fa-sort-alpha-up-alt:before{content:\u0026#34;\\f882\u0026#34;}.fa-sort-amount-down:before{content:\u0026#34;\\f160\u0026#34;}.fa-sort-amount-down-alt:before{content:\u0026#34;\\f884\u0026#34;}.fa-sort-amount-up:before{content:\u0026#34;\\f161\u0026#34;}.fa-sort-amount-up-alt:before{content:\u0026#34;\\f885\u0026#34;}.fa-sort-down:before{content:\u0026#34;\\f0dd\u0026#34;}.fa-sort-numeric-down:before{content:\u0026#34;\\f162\u0026#34;}.fa-sort-numeric-down-alt:before{content:\u0026#34;\\f886\u0026#34;}.fa-sort-numeric-up:before{content:\u0026#34;\\f163\u0026#34;}.fa-sort-numeric-up-alt:before{content:\u0026#34;\\f887\u0026#34;}.fa-sort-up:before{content:\u0026#34;\\f0de\u0026#34;}.fa-soundcloud:before{content:\u0026#34;\\f1be\u0026#34;}.fa-sourcetree:before{content:\u0026#34;\\f7d3\u0026#34;}.fa-spa:before{content:\u0026#34;\\f5bb\u0026#34;}.fa-space-shuttle:before{content:\u0026#34;\\f197\u0026#34;}.fa-speakap:before{content:\u0026#34;\\f3f3\u0026#34;}.fa-speaker-deck:before{content:\u0026#34;\\f83c\u0026#34;}.fa-spell-check:before{content:\u0026#34;\\f891\u0026#34;}.fa-spider:before{content:\u0026#34;\\f717\u0026#34;}.fa-spinner:before{content:\u0026#34;\\f110\u0026#34;}.fa-splotch:before{content:\u0026#34;\\f5bc\u0026#34;}.fa-spotify:before{content:\u0026#34;\\f1bc\u0026#34;}.fa-spray-can:before{content:\u0026#34;\\f5bd\u0026#34;}.fa-square:before{content:\u0026#34;\\f0c8\u0026#34;}.fa-square-full:before{content:\u0026#34;\\f45c\u0026#34;}.fa-square-root-alt:before{content:\u0026#34;\\f698\u0026#34;}.fa-squarespace:before{content:\u0026#34;\\f5be\u0026#34;}.fa-stack-exchange:before{content:\u0026#34;\\f18d\u0026#34;}.fa-stack-overflow:before{content:\u0026#34;\\f16c\u0026#34;}.fa-stackpath:before{content:\u0026#34;\\f842\u0026#34;}.fa-stamp:before{content:\u0026#34;\\f5bf\u0026#34;}.fa-star:before{content:\u0026#34;\\f005\u0026#34;}.fa-star-and-crescent:before{content:\u0026#34;\\f699\u0026#34;}.fa-star-half:before{content:\u0026#34;\\f089\u0026#34;}.fa-star-half-alt:before{content:\u0026#34;\\f5c0\u0026#34;}.fa-star-of-david:before{content:\u0026#34;\\f69a\u0026#34;}.fa-star-of-life:before{content:\u0026#34;\\f621\u0026#34;}.fa-staylinked:before{content:\u0026#34;\\f3f5\u0026#34;}.fa-steam:before{content:\u0026#34;\\f1b6\u0026#34;}.fa-steam-square:before{content:\u0026#34;\\f1b7\u0026#34;}.fa-steam-symbol:before{content:\u0026#34;\\f3f6\u0026#34;}.fa-step-backward:before{content:\u0026#34;\\f048\u0026#34;}.fa-step-forward:before{content:\u0026#34;\\f051\u0026#34;}.fa-stethoscope:before{content:\u0026#34;\\f0f1\u0026#34;}.fa-sticker-mule:before{content:\u0026#34;\\f3f7\u0026#34;}.fa-sticky-note:before{content:\u0026#34;\\f249\u0026#34;}.fa-stop:before{content:\u0026#34;\\f04d\u0026#34;}.fa-stop-circle:before{content:\u0026#34;\\f28d\u0026#34;}.fa-stopwatch:before{content:\u0026#34;\\f2f2\u0026#34;}.fa-stopwatch-20:before{content:\u0026#34;\\e06f\u0026#34;}.fa-store:before{content:\u0026#34;\\f54e\u0026#34;}.fa-store-alt:before{content:\u0026#34;\\f54f\u0026#34;}.fa-store-alt-slash:before{content:\u0026#34;\\e070\u0026#34;}.fa-store-slash:before{content:\u0026#34;\\e071\u0026#34;}.fa-strava:before{content:\u0026#34;\\f428\u0026#34;}.fa-stream:before{content:\u0026#34;\\f550\u0026#34;}.fa-street-view:before{content:\u0026#34;\\f21d\u0026#34;}.fa-strikethrough:before{content:\u0026#34;\\f0cc\u0026#34;}.fa-stripe:before{content:\u0026#34;\\f429\u0026#34;}.fa-stripe-s:before{content:\u0026#34;\\f42a\u0026#34;}.fa-stroopwafel:before{content:\u0026#34;\\f551\u0026#34;}.fa-studiovinari:before{content:\u0026#34;\\f3f8\u0026#34;}.fa-stumbleupon:before{content:\u0026#34;\\f1a4\u0026#34;}.fa-stumbleupon-circle:before{content:\u0026#34;\\f1a3\u0026#34;}.fa-subscript:before{content:\u0026#34;\\f12c\u0026#34;}.fa-subway:before{content:\u0026#34;\\f239\u0026#34;}.fa-suitcase:before{content:\u0026#34;\\f0f2\u0026#34;}.fa-suitcase-rolling:before{content:\u0026#34;\\f5c1\u0026#34;}.fa-sun:before{content:\u0026#34;\\f185\u0026#34;}.fa-superpowers:before{content:\u0026#34;\\f2dd\u0026#34;}.fa-superscript:before{content:\u0026#34;\\f12b\u0026#34;}.fa-supple:before{content:\u0026#34;\\f3f9\u0026#34;}.fa-surprise:before{content:\u0026#34;\\f5c2\u0026#34;}.fa-suse:before{content:\u0026#34;\\f7d6\u0026#34;}.fa-swatchbook:before{content:\u0026#34;\\f5c3\u0026#34;}.fa-swift:before{content:\u0026#34;\\f8e1\u0026#34;}.fa-swimmer:before{content:\u0026#34;\\f5c4\u0026#34;}.fa-swimming-pool:before{content:\u0026#34;\\f5c5\u0026#34;}.fa-symfony:before{content:\u0026#34;\\f83d\u0026#34;}.fa-synagogue:before{content:\u0026#34;\\f69b\u0026#34;}.fa-sync:before{content:\u0026#34;\\f021\u0026#34;}.fa-sync-alt:before{content:\u0026#34;\\f2f1\u0026#34;}.fa-syringe:before{content:\u0026#34;\\f48e\u0026#34;}.fa-table:before{content:\u0026#34;\\f0ce\u0026#34;}.fa-table-tennis:before{content:\u0026#34;\\f45d\u0026#34;}.fa-tablet:before{content:\u0026#34;\\f10a\u0026#34;}.fa-tablet-alt:before{content:\u0026#34;\\f3fa\u0026#34;}.fa-tablets:before{content:\u0026#34;\\f490\u0026#34;}.fa-tachometer-alt:before{content:\u0026#34;\\f3fd\u0026#34;}.fa-tag:before{content:\u0026#34;\\f02b\u0026#34;}.fa-tags:before{content:\u0026#34;\\f02c\u0026#34;}.fa-tape:before{content:\u0026#34;\\f4db\u0026#34;}.fa-tasks:before{content:\u0026#34;\\f0ae\u0026#34;}.fa-taxi:before{content:\u0026#34;\\f1ba\u0026#34;}.fa-teamspeak:before{content:\u0026#34;\\f4f9\u0026#34;}.fa-teeth:before{content:\u0026#34;\\f62e\u0026#34;}.fa-teeth-open:before{content:\u0026#34;\\f62f\u0026#34;}.fa-telegram:before{content:\u0026#34;\\f2c6\u0026#34;}.fa-telegram-plane:before{content:\u0026#34;\\f3fe\u0026#34;}.fa-temperature-high:before{content:\u0026#34;\\f769\u0026#34;}.fa-temperature-low:before{content:\u0026#34;\\f76b\u0026#34;}.fa-tencent-weibo:before{content:\u0026#34;\\f1d5\u0026#34;}.fa-tenge:before{content:\u0026#34;\\f7d7\u0026#34;}.fa-terminal:before{content:\u0026#34;\\f120\u0026#34;}.fa-text-height:before{content:\u0026#34;\\f034\u0026#34;}.fa-text-width:before{content:\u0026#34;\\f035\u0026#34;}.fa-th:before{content:\u0026#34;\\f00a\u0026#34;}.fa-th-large:before{content:\u0026#34;\\f009\u0026#34;}.fa-th-list:before{content:\u0026#34;\\f00b\u0026#34;}.fa-the-red-yeti:before{content:\u0026#34;\\f69d\u0026#34;}.fa-theater-masks:before{content:\u0026#34;\\f630\u0026#34;}.fa-themeco:before{content:\u0026#34;\\f5c6\u0026#34;}.fa-themeisle:before{content:\u0026#34;\\f2b2\u0026#34;}.fa-thermometer:before{content:\u0026#34;\\f491\u0026#34;}.fa-thermometer-empty:before{content:\u0026#34;\\f2cb\u0026#34;}.fa-thermometer-full:before{content:\u0026#34;\\f2c7\u0026#34;}.fa-thermometer-half:before{content:\u0026#34;\\f2c9\u0026#34;}.fa-thermometer-quarter:before{content:\u0026#34;\\f2ca\u0026#34;}.fa-thermometer-three-quarters:before{content:\u0026#34;\\f2c8\u0026#34;}.fa-think-peaks:before{content:\u0026#34;\\f731\u0026#34;}.fa-thumbs-down:before{content:\u0026#34;\\f165\u0026#34;}.fa-thumbs-up:before{content:\u0026#34;\\f164\u0026#34;}.fa-thumbtack:before{content:\u0026#34;\\f08d\u0026#34;}.fa-ticket-alt:before{content:\u0026#34;\\f3ff\u0026#34;}.fa-tiktok:before{content:\u0026#34;\\e07b\u0026#34;}.fa-times:before{content:\u0026#34;\\f00d\u0026#34;}.fa-times-circle:before{content:\u0026#34;\\f057\u0026#34;}.fa-tint:before{content:\u0026#34;\\f043\u0026#34;}.fa-tint-slash:before{content:\u0026#34;\\f5c7\u0026#34;}.fa-tired:before{content:\u0026#34;\\f5c8\u0026#34;}.fa-toggle-off:before{content:\u0026#34;\\f204\u0026#34;}.fa-toggle-on:before{content:\u0026#34;\\f205\u0026#34;}.fa-toilet:before{content:\u0026#34;\\f7d8\u0026#34;}.fa-toilet-paper:before{content:\u0026#34;\\f71e\u0026#34;}.fa-toilet-paper-slash:before{content:\u0026#34;\\e072\u0026#34;}.fa-toolbox:before{content:\u0026#34;\\f552\u0026#34;}.fa-tools:before{content:\u0026#34;\\f7d9\u0026#34;}.fa-tooth:before{content:\u0026#34;\\f5c9\u0026#34;}.fa-torah:before{content:\u0026#34;\\f6a0\u0026#34;}.fa-torii-gate:before{content:\u0026#34;\\f6a1\u0026#34;}.fa-tractor:before{content:\u0026#34;\\f722\u0026#34;}.fa-trade-federation:before{content:\u0026#34;\\f513\u0026#34;}.fa-trademark:before{content:\u0026#34;\\f25c\u0026#34;}.fa-traffic-light:before{content:\u0026#34;\\f637\u0026#34;}.fa-trailer:before{content:\u0026#34;\\e041\u0026#34;}.fa-train:before{content:\u0026#34;\\f238\u0026#34;}.fa-tram:before{content:\u0026#34;\\f7da\u0026#34;}.fa-transgender:before{content:\u0026#34;\\f224\u0026#34;}.fa-transgender-alt:before{content:\u0026#34;\\f225\u0026#34;}.fa-trash:before{content:\u0026#34;\\f1f8\u0026#34;}.fa-trash-alt:before{content:\u0026#34;\\f2ed\u0026#34;}.fa-trash-restore:before{content:\u0026#34;\\f829\u0026#34;}.fa-trash-restore-alt:before{content:\u0026#34;\\f82a\u0026#34;}.fa-tree:before{content:\u0026#34;\\f1bb\u0026#34;}.fa-trello:before{content:\u0026#34;\\f181\u0026#34;}.fa-trophy:before{content:\u0026#34;\\f091\u0026#34;}.fa-truck:before{content:\u0026#34;\\f0d1\u0026#34;}.fa-truck-loading:before{content:\u0026#34;\\f4de\u0026#34;}.fa-truck-monster:before{content:\u0026#34;\\f63b\u0026#34;}.fa-truck-moving:before{content:\u0026#34;\\f4df\u0026#34;}.fa-truck-pickup:before{content:\u0026#34;\\f63c\u0026#34;}.fa-tshirt:before{content:\u0026#34;\\f553\u0026#34;}.fa-tty:before{content:\u0026#34;\\f1e4\u0026#34;}.fa-tumblr:before{content:\u0026#34;\\f173\u0026#34;}.fa-tumblr-square:before{content:\u0026#34;\\f174\u0026#34;}.fa-tv:before{content:\u0026#34;\\f26c\u0026#34;}.fa-twitch:before{content:\u0026#34;\\f1e8\u0026#34;}.fa-twitter:before{content:\u0026#34;\\f099\u0026#34;}.fa-twitter-square:before{content:\u0026#34;\\f081\u0026#34;}.fa-typo3:before{content:\u0026#34;\\f42b\u0026#34;}.fa-uber:before{content:\u0026#34;\\f402\u0026#34;}.fa-ubuntu:before{content:\u0026#34;\\f7df\u0026#34;}.fa-uikit:before{content:\u0026#34;\\f403\u0026#34;}.fa-umbraco:before{content:\u0026#34;\\f8e8\u0026#34;}.fa-umbrella:before{content:\u0026#34;\\f0e9\u0026#34;}.fa-umbrella-beach:before{content:\u0026#34;\\f5ca\u0026#34;}.fa-uncharted:before{content:\u0026#34;\\e084\u0026#34;}.fa-underline:before{content:\u0026#34;\\f0cd\u0026#34;}.fa-undo:before{content:\u0026#34;\\f0e2\u0026#34;}.fa-undo-alt:before{content:\u0026#34;\\f2ea\u0026#34;}.fa-uniregistry:before{content:\u0026#34;\\f404\u0026#34;}.fa-unity:before{content:\u0026#34;\\e049\u0026#34;}.fa-universal-access:before{content:\u0026#34;\\f29a\u0026#34;}.fa-university:before{content:\u0026#34;\\f19c\u0026#34;}.fa-unlink:before{content:\u0026#34;\\f127\u0026#34;}.fa-unlock:before{content:\u0026#34;\\f09c\u0026#34;}.fa-unlock-alt:before{content:\u0026#34;\\f13e\u0026#34;}.fa-unsplash:before{content:\u0026#34;\\e07c\u0026#34;}.fa-untappd:before{content:\u0026#34;\\f405\u0026#34;}.fa-upload:before{content:\u0026#34;\\f093\u0026#34;}.fa-ups:before{content:\u0026#34;\\f7e0\u0026#34;}.fa-usb:before{content:\u0026#34;\\f287\u0026#34;}.fa-user:before{content:\u0026#34;\\f007\u0026#34;}.fa-user-alt:before{content:\u0026#34;\\f406\u0026#34;}.fa-user-alt-slash:before{content:\u0026#34;\\f4fa\u0026#34;}.fa-user-astronaut:before{content:\u0026#34;\\f4fb\u0026#34;}.fa-user-check:before{content:\u0026#34;\\f4fc\u0026#34;}.fa-user-circle:before{content:\u0026#34;\\f2bd\u0026#34;}.fa-user-clock:before{content:\u0026#34;\\f4fd\u0026#34;}.fa-user-cog:before{content:\u0026#34;\\f4fe\u0026#34;}.fa-user-edit:before{content:\u0026#34;\\f4ff\u0026#34;}.fa-user-friends:before{content:\u0026#34;\\f500\u0026#34;}.fa-user-graduate:before{content:\u0026#34;\\f501\u0026#34;}.fa-user-injured:before{content:\u0026#34;\\f728\u0026#34;}.fa-user-lock:before{content:\u0026#34;\\f502\u0026#34;}.fa-user-md:before{content:\u0026#34;\\f0f0\u0026#34;}.fa-user-minus:before{content:\u0026#34;\\f503\u0026#34;}.fa-user-ninja:before{content:\u0026#34;\\f504\u0026#34;}.fa-user-nurse:before{content:\u0026#34;\\f82f\u0026#34;}.fa-user-plus:before{content:\u0026#34;\\f234\u0026#34;}.fa-user-secret:before{content:\u0026#34;\\f21b\u0026#34;}.fa-user-shield:before{content:\u0026#34;\\f505\u0026#34;}.fa-user-slash:before{content:\u0026#34;\\f506\u0026#34;}.fa-user-tag:before{content:\u0026#34;\\f507\u0026#34;}.fa-user-tie:before{content:\u0026#34;\\f508\u0026#34;}.fa-user-times:before{content:\u0026#34;\\f235\u0026#34;}.fa-users:before{content:\u0026#34;\\f0c0\u0026#34;}.fa-users-cog:before{content:\u0026#34;\\f509\u0026#34;}.fa-users-slash:before{content:\u0026#34;\\e073\u0026#34;}.fa-usps:before{content:\u0026#34;\\f7e1\u0026#34;}.fa-ussunnah:before{content:\u0026#34;\\f407\u0026#34;}.fa-utensil-spoon:before{content:\u0026#34;\\f2e5\u0026#34;}.fa-utensils:before{content:\u0026#34;\\f2e7\u0026#34;}.fa-vaadin:before{content:\u0026#34;\\f408\u0026#34;}.fa-vector-square:before{content:\u0026#34;\\f5cb\u0026#34;}.fa-venus:before{content:\u0026#34;\\f221\u0026#34;}.fa-venus-double:before{content:\u0026#34;\\f226\u0026#34;}.fa-venus-mars:before{content:\u0026#34;\\f228\u0026#34;}.fa-vest:before{content:\u0026#34;\\e085\u0026#34;}.fa-vest-patches:before{content:\u0026#34;\\e086\u0026#34;}.fa-viacoin:before{content:\u0026#34;\\f237\u0026#34;}.fa-viadeo:before{content:\u0026#34;\\f2a9\u0026#34;}.fa-viadeo-square:before{content:\u0026#34;\\f2aa\u0026#34;}.fa-vial:before{content:\u0026#34;\\f492\u0026#34;}.fa-vials:before{content:\u0026#34;\\f493\u0026#34;}.fa-viber:before{content:\u0026#34;\\f409\u0026#34;}.fa-video:before{content:\u0026#34;\\f03d\u0026#34;}.fa-video-slash:before{content:\u0026#34;\\f4e2\u0026#34;}.fa-vihara:before{content:\u0026#34;\\f6a7\u0026#34;}.fa-vimeo:before{content:\u0026#34;\\f40a\u0026#34;}.fa-vimeo-square:before{content:\u0026#34;\\f194\u0026#34;}.fa-vimeo-v:before{content:\u0026#34;\\f27d\u0026#34;}.fa-vine:before{content:\u0026#34;\\f1ca\u0026#34;}.fa-virus:before{content:\u0026#34;\\e074\u0026#34;}.fa-virus-slash:before{content:\u0026#34;\\e075\u0026#34;}.fa-viruses:before{content:\u0026#34;\\e076\u0026#34;}.fa-vk:before{content:\u0026#34;\\f189\u0026#34;}.fa-vnv:before{content:\u0026#34;\\f40b\u0026#34;}.fa-voicemail:before{content:\u0026#34;\\f897\u0026#34;}.fa-volleyball-ball:before{content:\u0026#34;\\f45f\u0026#34;}.fa-volume-down:before{content:\u0026#34;\\f027\u0026#34;}.fa-volume-mute:before{content:\u0026#34;\\f6a9\u0026#34;}.fa-volume-off:before{content:\u0026#34;\\f026\u0026#34;}.fa-volume-up:before{content:\u0026#34;\\f028\u0026#34;}.fa-vote-yea:before{content:\u0026#34;\\f772\u0026#34;}.fa-vr-cardboard:before{content:\u0026#34;\\f729\u0026#34;}.fa-vuejs:before{content:\u0026#34;\\f41f\u0026#34;}.fa-walking:before{content:\u0026#34;\\f554\u0026#34;}.fa-wallet:before{content:\u0026#34;\\f555\u0026#34;}.fa-warehouse:before{content:\u0026#34;\\f494\u0026#34;}.fa-watchman-monitoring:before{content:\u0026#34;\\e087\u0026#34;}.fa-water:before{content:\u0026#34;\\f773\u0026#34;}.fa-wave-square:before{content:\u0026#34;\\f83e\u0026#34;}.fa-waze:before{content:\u0026#34;\\f83f\u0026#34;}.fa-weebly:before{content:\u0026#34;\\f5cc\u0026#34;}.fa-weibo:before{content:\u0026#34;\\f18a\u0026#34;}.fa-weight:before{content:\u0026#34;\\f496\u0026#34;}.fa-weight-hanging:before{content:\u0026#34;\\f5cd\u0026#34;}.fa-weixin:before{content:\u0026#34;\\f1d7\u0026#34;}.fa-whatsapp:before{content:\u0026#34;\\f232\u0026#34;}.fa-whatsapp-square:before{content:\u0026#34;\\f40c\u0026#34;}.fa-wheelchair:before{content:\u0026#34;\\f193\u0026#34;}.fa-whmcs:before{content:\u0026#34;\\f40d\u0026#34;}.fa-wifi:before{content:\u0026#34;\\f1eb\u0026#34;}.fa-wikipedia-w:before{content:\u0026#34;\\f266\u0026#34;}.fa-wind:before{content:\u0026#34;\\f72e\u0026#34;}.fa-window-close:before{content:\u0026#34;\\f410\u0026#34;}.fa-window-maximize:before{content:\u0026#34;\\f2d0\u0026#34;}.fa-window-minimize:before{content:\u0026#34;\\f2d1\u0026#34;}.fa-window-restore:before{content:\u0026#34;\\f2d2\u0026#34;}.fa-windows:before{content:\u0026#34;\\f17a\u0026#34;}.fa-wine-bottle:before{content:\u0026#34;\\f72f\u0026#34;}.fa-wine-glass:before{content:\u0026#34;\\f4e3\u0026#34;}.fa-wine-glass-alt:before{content:\u0026#34;\\f5ce\u0026#34;}.fa-wix:before{content:\u0026#34;\\f5cf\u0026#34;}.fa-wizards-of-the-coast:before{content:\u0026#34;\\f730\u0026#34;}.fa-wodu:before{content:\u0026#34;\\e088\u0026#34;}.fa-wolf-pack-battalion:before{content:\u0026#34;\\f514\u0026#34;}.fa-won-sign:before{content:\u0026#34;\\f159\u0026#34;}.fa-wordpress:before{content:\u0026#34;\\f19a\u0026#34;}.fa-wordpress-simple:before{content:\u0026#34;\\f411\u0026#34;}.fa-wpbeginner:before{content:\u0026#34;\\f297\u0026#34;}.fa-wpexplorer:before{content:\u0026#34;\\f2de\u0026#34;}.fa-wpforms:before{content:\u0026#34;\\f298\u0026#34;}.fa-wpressr:before{content:\u0026#34;\\f3e4\u0026#34;}.fa-wrench:before{content:\u0026#34;\\f0ad\u0026#34;}.fa-x-ray:before{content:\u0026#34;\\f497\u0026#34;}.fa-xbox:before{content:\u0026#34;\\f412\u0026#34;}.fa-xing:before{content:\u0026#34;\\f168\u0026#34;}.fa-xing-square:before{content:\u0026#34;\\f169\u0026#34;}.fa-y-combinator:before{content:\u0026#34;\\f23b\u0026#34;}.fa-yahoo:before{content:\u0026#34;\\f19e\u0026#34;}.fa-yammer:before{content:\u0026#34;\\f840\u0026#34;}.fa-yandex:before{content:\u0026#34;\\f413\u0026#34;}.fa-yandex-international:before{content:\u0026#34;\\f414\u0026#34;}.fa-yarn:before{content:\u0026#34;\\f7e3\u0026#34;}.fa-yelp:before{content:\u0026#34;\\f1e9\u0026#34;}.fa-yen-sign:before{content:\u0026#34;\\f157\u0026#34;}.fa-yin-yang:before{content:\u0026#34;\\f6ad\u0026#34;}.fa-yoast:before{content:\u0026#34;\\f2b1\u0026#34;}.fa-youtube:before{content:\u0026#34;\\f167\u0026#34;}.fa-youtube-square:before{content:\u0026#34;\\f431\u0026#34;}.fa-zhihu:before{content:\u0026#34;\\f63f\u0026#34;}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:\u0026#34;Font Awesome 5 Brands\u0026#34;;font-style:normal;font-weight:400;font-display:block;src:url(/font/fa/fa-brands-400.eot);src:url(/font/fa/fa-brands-400.eot?#iefix) format(\u0026#34;embedded-opentype\u0026#34;),url(/font/fa/fa-brands-400.woff2) format(\u0026#34;woff2\u0026#34;),url(/font/fa/fa-brands-400.woff) format(\u0026#34;woff\u0026#34;),url(/font/fa/fa-brands-400.ttf) format(\u0026#34;truetype\u0026#34;),url(/font/fa/fa-brands-400.svg#fontawesome) format(\u0026#34;svg\u0026#34;)}.fab{font-family:\u0026#34;Font Awesome 5 Brands\u0026#34;}@font-face{font-family:\u0026#34;Font Awesome 5 Free\u0026#34;;font-style:normal;font-weight:400;font-display:block;src:url(/font/fa/fa-regular-400.eot);src:url(/font/fa/fa-regular-400.eot?#iefix) format(\u0026#34;embedded-opentype\u0026#34;),url(/font/fa/fa-regular-400.woff2) format(\u0026#34;woff2\u0026#34;),url(/font/fa/fa-regular-400.woff) format(\u0026#34;woff\u0026#34;),url(/font/fa/fa-regular-400.ttf) format(\u0026#34;truetype\u0026#34;),url(/font/fa/fa-regular-400.svg#fontawesome) format(\u0026#34;svg\u0026#34;)}.fab,.far{font-weight:400}@font-face{font-family:\u0026#34;Font Awesome 5 Free\u0026#34;;font-style:normal;font-weight:900;font-display:block;src:url(/font/fa/fa-solid-900.eot);src:url(/font/fa/fa-solid-900.eot?#iefix) format(\u0026#34;embedded-opentype\u0026#34;),url(/font/fa/fa-solid-900.woff2) format(\u0026#34;woff2\u0026#34;),url(/font/fa/fa-solid-900.woff) format(\u0026#34;woff\u0026#34;),url(/font/fa/fa-solid-900.ttf) format(\u0026#34;truetype\u0026#34;),url(/font/fa/fa-solid-900.svg#fontawesome) format(\u0026#34;svg\u0026#34;)}.fa,.far,.fas{font-family:\u0026#34;Font Awesome 5 Free\u0026#34;}.fa,.fas{font-weight:900} 6 7.details .details-summary:hover { 8 cursor: pointer; 9} 10.details i.details-icon { 11color: var(--content); 12-webkit-transition: transform 0.2s ease; 13-moz-transition: transform 0.2s ease; 14-o-transition: transform 0.2s ease; 15transition: transform 0.2s ease; 16} 17[class=dark] .details i.details-icon { 18color: var(--content); 19} 20.details .details-content { 21max-height: 0; 22overflow-y: hidden; 23word-wrap: break-word; 24overflow-x: hidden; 25-webkit-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) 0s; 26-moz-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) 0s; 27-o-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) 0s; 28transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) 0s; 29} 30.details.open i.details-icon { 31-webkit-transform: rotate(90deg); 32-moz-transform: rotate(90deg); 33-ms-transform: rotate(90deg); 34-o-transform: rotate(90deg); 35transform: rotate(90deg); 36} 37.details.open .details-content { 38max-height: 12000px; 39-webkit-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s; 40-moz-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s; 41-o-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s; 42transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s; 43overflow-x: hidden; 44} 45 46.admonition { 47 position: relative; 48 margin: 1rem 0; 49 padding: 0 0.75rem; 50 background-color: rgba(68, 138, 255, 0.1); 51 border-left: 0.25rem solid #448aff; 52 border-radius: 8px; 53 overflow: auto; 54} 55.admonition .admonition-title { 56 font-weight: bold; 57 margin: 0 -0.75rem; 58 padding: 0.25rem 3rem; 59 border-radius: 7px; 60 border-bottom: 1px solid rgba(68, 138, 255, 0.1); 61 background-color: rgba(68, 138, 255, 0.25); 62} 63.admonition.open .admonition-title { 64 background-color: rgba(68, 138, 255, 0.1); 65} 66.admonition .admonition-content { 67 padding: 0.5rem 0; 68} 69.admonition i.icon { 70 font-size: 1.45rem; 71 color: #448aff; 72 position: absolute; 73 top: 1.25rem; 74 left: 0.65rem; 75} 76.admonition i.details-icon { 77 position: absolute; 78 top: 0.9rem; 79 right: 0.5rem; 80} 81.admonition.note { 82 border-left-color: #448aff; 83} 84.admonition.note i.icon { 85 color: #448aff; 86} 87.admonition.abstract { 88 border-left-color: #00b0ff; 89} 90.admonition.abstract i.icon { 91 color: #00b0ff; 92} 93.admonition.info { 94 border-left-color: #00b8d4; 95} 96.admonition.info i.icon { 97 color: #00b8d4; 98} 99.admonition.tip { 100 border-left-color: #00bfa5; 101} 102.admonition.tip i.icon { 103 color: #00bfa5; 104} 105.admonition.success { 106 border-left-color: #00c853; 107} 108.admonition.success i.icon { 109 color: #00c853; 110} 111.admonition.question { 112 border-left-color: #64dd17; 113} 114.admonition.question i.icon { 115 color: #64dd17; 116} 117.admonition.warning { 118 border-left-color: #ff9100; 119} 120.admonition.warning i.icon { 121 color: #ff9100; 122} 123.admonition.failure { 124 border-left-color: #ff5252; 125} 126.admonition.failure i.icon { 127 color: #ff5252; 128} 129.admonition.danger { 130 border-left-color: #ff1744; 131} 132.admonition.danger i.icon { 133 color: #ff1744; 134} 135.admonition.bug { 136 border-left-color: #f50057; 137} 138.admonition.bug i.icon { 139 color: #f50057; 140} 141.admonition.example { 142 border-left-color: #651fff; 143} 144.admonition.example i.icon { 145 color: #651fff; 146} 147.admonition.quote { 148 border-left-color: #9e9e9e; 149} 150.admonition.quote i.icon { 151 color: #9e9e9e; 152} 153.admonition.note { 154 background-color: rgba(68, 138, 255, 0.1); 155} 156.admonition.note .admonition-title { 157 border-bottom-color: rgba(68, 138, 255, 0.1); 158 background-color: rgba(68, 138, 255, 0.25); 159} 160.admonition.note.open .admonition-title { 161 background-color: rgba(68, 138, 255, 0.1); 162} 163.admonition.abstract { 164 background-color: rgba(0, 176, 255, 0.1); 165} 166.admonition.abstract .admonition-title { 167 border-bottom-color: rgba(0, 176, 255, 0.1); 168 background-color: rgba(0, 176, 255, 0.25); 169} 170.admonition.abstract.open .admonition-title { 171 background-color: rgba(0, 176, 255, 0.1); 172} 173.admonition.info { 174 background-color: rgba(0, 184, 212, 0.1); 175} 176.admonition.info .admonition-title { 177 border-bottom-color: rgba(0, 184, 212, 0.1); 178 background-color: rgba(0, 184, 212, 0.25); 179} 180.admonition.info.open .admonition-title { 181 background-color: rgba(0, 184, 212, 0.1); 182} 183.admonition.tip { 184 background-color: rgba(0, 191, 165, 0.1); 185} 186.admonition.tip .admonition-title { 187 border-bottom-color: rgba(0, 191, 165, 0.1); 188 background-color: rgba(0, 191, 165, 0.25); 189} 190.admonition.tip.open .admonition-title { 191 background-color: rgba(0, 191, 165, 0.1); 192} 193.admonition.success { 194 background-color: rgba(0, 200, 83, 0.1); 195} 196.admonition.success .admonition-title { 197 border-bottom-color: rgba(0, 200, 83, 0.1); 198 background-color: rgba(0, 200, 83, 0.25); 199} 200.admonition.success.open .admonition-title { 201 background-color: rgba(0, 200, 83, 0.1); 202} 203.admonition.question { 204 background-color: rgba(100, 221, 23, 0.1); 205} 206.admonition.question .admonition-title { 207 border-bottom-color: rgba(100, 221, 23, 0.1); 208 background-color: rgba(100, 221, 23, 0.25); 209} 210.admonition.question.open .admonition-title { 211 background-color: rgba(100, 221, 23, 0.1); 212} 213.admonition.warning { 214 background-color: rgba(255, 145, 0, 0.1); 215} 216.admonition.warning .admonition-title { 217 border-bottom-color: rgba(255, 145, 0, 0.1); 218 background-color: rgba(255, 145, 0, 0.25); 219} 220.admonition.warning.open .admonition-title { 221 background-color: rgba(255, 145, 0, 0.1); 222} 223.admonition.failure { 224 background-color: rgba(255, 82, 82, 0.1); 225} 226.admonition.failure .admonition-title { 227 border-bottom-color: rgba(255, 82, 82, 0.1); 228 background-color: rgba(255, 82, 82, 0.25); 229} 230.admonition.failure.open .admonition-title { 231 background-color: rgba(255, 82, 82, 0.1); 232} 233.admonition.danger { 234 background-color: rgba(255, 23, 68, 0.1); 235} 236.admonition.danger .admonition-title { 237 border-bottom-color: rgba(255, 23, 68, 0.1); 238 background-color: rgba(255, 23, 68, 0.25); 239} 240.admonition.danger.open .admonition-title { 241 background-color: rgba(255, 23, 68, 0.1); 242} 243.admonition.bug { 244 background-color: rgba(245, 0, 87, 0.1); 245} 246.admonition.bug .admonition-title { 247 border-bottom-color: rgba(245, 0, 87, 0.1); 248 background-color: rgba(245, 0, 87, 0.25); 249} 250.admonition.bug.open .admonition-title { 251 background-color: rgba(245, 0, 87, 0.1); 252} 253.admonition.example { 254 background-color: rgba(101, 31, 255, 0.1); 255} 256.admonition.example .admonition-title { 257 border-bottom-color: rgba(101, 31, 255, 0.1); 258 background-color: rgba(101, 31, 255, 0.25); 259} 260.admonition.example.open .admonition-title { 261 background-color: rgba(101, 31, 255, 0.1); 262} 263.admonition.quote { 264 background-color: rgba(60,60,60,.1); 265} 266.admonition.quote .admonition-title { 267 border-bottom-color: rgba(60, 60, 60, 0.1); 268 background-color: rgba(60, 60, 60, 0.25); 269} 270.admonition.quote.open .admonition-title { 271 background-color: rgba(60, 60, 60, 0.1); 272} 273.admonition:last-child { 274 margin-bottom: 0.75rem; 275} 276.admonition.note.open .admonition-title:hover, 277.admonition.note .admonition-title:hover { 278 background-color: rgba(68, 138, 255, 0.5); 279 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 280} 281 282.admonition.abstract.open .admonition-title:hover, 283.admonition.abstract .admonition-title:hover { 284 background-color: rgba(0, 176, 255, 0.5); 285 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 286} 287 288.admonition.info.open .admonition-title:hover, 289.admonition.info .admonition-title:hover { 290 background-color: rgba(0, 184, 212, 0.5); 291 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 292} 293 294.admonition.tip.open .admonition-title:hover, 295.admonition.tip .admonition-title:hover { 296 background-color: rgba(0, 191, 165, 0.5); 297 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 298} 299 300.admonition.success.open .admonition-title:hover, 301.admonition.success .admonition-title:hover { 302 background-color: rgba(0, 200, 83, 0.5); 303 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 304} 305 306.admonition.question.open .admonition-title:hover, 307.admonition.question .admonition-title:hover { 308 background-color: rgba(100, 221, 23, 0.5); 309 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 310} 311 312.admonition.warning.open .admonition-title:hover, 313.admonition.warning .admonition-title:hover { 314 background-color: rgba(255, 145, 0, 0.5); 315 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 316} 317 318.admonition.failure.open .admonition-title:hover, 319.admonition.failure .admonition-title:hover { 320 background-color: rgba(255, 82, 82, 0.5); 321 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 322} 323 324.admonition.danger.open .admonition-title:hover, 325.admonition.danger .admonition-title:hover { 326 background-color: rgba(255, 23, 68, 0.5); 327 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 328} 329 330.admonition.bug.open .admonition-title:hover, 331.admonition.bug .admonition-title:hover { 332 background-color: rgba(245, 0, 87, 0.5); 333 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 334} 335 336.admonition.example.open .admonition-title:hover, 337.admonition.example .admonition-title:hover { 338 background-color: rgba(101, 31, 255, 0.5); 339 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 340} 341 342.admonition.quote.open .admonition-title:hover, 343.admonition.quote .admonition-title:hover { 344 background-color: rgba(60, 60, 60, 0.5); 345 cursor: url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 346} 347[data-scheme=\u0026#34;dark\u0026#34;] .admonition.quote.open .admonition-title:hover, 348[data-scheme=\u0026#34;dark\u0026#34;] .admonition.quote .admonition-title:hover { 349 background-color: rgba(160, 160, 160, 0.5); 350} 在 layouts/partials/footer/custom.html 最后添加： 1{{- /* admonition toggle */}} 2\u0026lt;script\u0026gt; 3 let details = document.getElementsByClassName(\u0026#39;details\u0026#39;) 4 details = details || []; 5 for (let i = 0; i \u0026lt; details.length; i++) { 6 let element = details[i] 7 const summary = element.getElementsByClassName(\u0026#39;details-summary\u0026#39;)[0]; 8 if (summary) { 9 summary.addEventListener(\u0026#39;click\u0026#39;, () =\u0026gt; { 10 element.classList.toggle(\u0026#39;open\u0026#39;); 11 }, false); 12 } 13 } 14\u0026lt;/script\u0026gt; 复制字体文件 将字体文件复制到 static/font/fa/ 中。所有用到的文件目录结构如下：\n1tree hugo/ 2# hugo/ 3# ├─ assets 4# │ └─ scss 5# │ └─ custom.scss 6# ├─ layouts 7# │ ├─ partials 8# │ │ └─ footer 9# │ │ └─ custom.html 10# │ └─ shortcodes 11# │ └─ admonition.html 12# └─ static 13# └─ font 14# └─ fa 15# ├─ fa-brands-400.eot 16# ├─ fa-brands-400.svg 17# ├─ fa-brands-400.ttf 18# ├─ fa-brands-400.woff 19# ├─ fa-brands-400.woff2 20# ├─ fa-regular-400.eot 21# ├─ fa-regular-400.svg 22# ├─ fa-regular-400.ttf 23# ├─ fa-regular-400.woff 24# ├─ fa-regular-400.woff2 25# ├─ fa-solid-900.eot 26# ├─ fa-solid-900.svg 27# ├─ fa-solid-900.ttf 28# ├─ fa-solid-900.woff 29# └─ fa-solid-900.woff2 使用： 1{{\u0026lt; admonition type=note title=\u0026#34;标注\u0026#34; open=false \u0026gt;}}标注{{\u0026lt; /admonition \u0026gt;}} 小记 参考 ■■Loading：《hugo装修日志07》■■ - 天堂错误文件\nHugo|在Stack主题上可行的短代码们 - 眠于水月间\nHugo使用shortcode插入中文直書（CSS text-orientation）- Ivon的部落格\n自定义 Hugo Shortcodes 简码 - 荷戟独彷徨\nHugo Stack 主題加入 SWELL 螢光筆樣式 - ty_wu \u0026rsquo;s Blog\nHugo Stack 主題加入 Admonition 提示框樣式 - ty_wu \u0026rsquo;s Blog\n‫ ‫ ‫ 忙忙忙，只能又水一篇了 脚注参考链接 1 : 续・深入探究 Hugo 短代码：我今天还就非得把这个脚注写出来不可\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n脚注参考链接 2 : Footnotes are not rendered as expected in nested shortcodes\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nfootnote 3 ： 点返回试试 →\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-04-15T12:42:25+08:00","image":"https://langminjeii.ifantic.de/post/shortcodes.png","permalink":"https://langminjeii.ifantic.de/post/hugo-%E7%9F%AD%E4%BB%A3%E7%A0%81/","title":"Hugo 短代码"},{"content":"部署 Gemini 代理 🚀 Gemini 代理服务 本项目是一个基于 FastAPI 框架开发的高性能、易于部署的Gemini OpenAI兼容 和 Gemini API 代理服务。它不仅兼容 OpenAI 的 API 接口，还支持 Google 的 Gemini 原生接口。该代理服务内置了多 API Key 轮询、负载均衡、自动重试、访问控制（Bearer Token 认证）、流式响应等功能，旨在简化 AI 应用的开发和部署流程。 Python 复制space空间 修改 visibility 为 Public(一定注意修改成public，不然访问不到会报错)，配置 ALLOWED_TOKENS 和 API_KEYS。其余配置默认不用修改。 ALLOWED_TOKENS 格式为[\u0026quot;自定义apikey\u0026quot;]，注意中括号、逗号和引号都有严格遵循。这个变量是对API的访问鉴权。 API_KEYS 格式为单个key的形式：[\u0026quot;gemini_key1\u0026quot;]；多个key的形式[\u0026quot;gemini_key1\u0026quot;,\u0026quot;gemini_key2\u0026quot;]。注意中括号、逗号和引号都有严格遵循。这里填的就是在谷歌控制台创建的apikey。 BASE_URL 保持默认 耐心等待一下。部署成功后，会出现running状态 这时该huggingface服务的默认host为 huggingface用户名-gemini-balance.hf.space，比如上面的是 snailyp-gemini-balance.hf.space 注意：huggingface服务48h不使用会进入sleeping,建议通过青龙面板或者uptime kuma等定时任务进行保活。（直接用get请求调用 https://用户名-gemini-balance.hf.space 即可） 支持端点 /hf/v1/models 和 /hf/v1/chat/completions Cherry Studio 在这里下载Cherry Studio的客户端。\n比较有用的是知识库的功能。那么要怎么用呢？\n知识库配置：硅基流动 Embedding 点击这里注册账号。\n无论用什么注册到后面都是要绑定手机号的，所以推荐直接绑手机即可。\n然后左边栏下滑，创建一个apikey：\n回到模型广场，用筛选器能看到有四个免费的嵌入模型，最推荐的中文处理模型是 BAAI/bge-large-zh-v1.5 和 BAAI/bge-m3。\n然后到 Cherry Studio 添加嵌入模型。\n搭配 Gemini 使用知识库 我这里以 cherry studio 的文档为例，添加站点地图docs.cherry-ai.com/sitemap-pages.xml\n注意，站点地图只支持 xml 路径！稍等一会，显示绿勾就是处理完成了.\n再添加一个模型，类型就选默认的 OpenAI 即可。\nAPI地址填 HuggingFace 代理的host地址，也就是上面所说的 huggingface用户名-gemini-balance.hf.space。 API密钥填你上面部署时配置的 ALLOWED_TOKENS 。 检查连接：\n左上角进入对话，选择创建好的模型：\n选择你创建好的知识库：\n只要是在知识库里能检索到的信息AI就会引用回答，还会标出参考来源。\n目前 gemini-2.0-flash-exp-search 支持联网。虽然它没显示联网图标\nMCP 使用 初次使用时需要在设置中安装MCP相关环境的环境依赖（需科学网络）：\n直接安装即可。如若安装不了，可以参考官方文档，查看以下路径查看是否有配置文件：\nWindows: C:\\Users\\用户名\\.cherrystudio\\bin\nmacOS，Linux: ~/.cherrystudio/bin\n如果没有对应目录，需要手动建立。然后下载可执行文件放到这个目录下面：\nBun: https://github.com/oven-sh/bun/releases\nUV: https://github.com/astral-sh/uv/releases\n重新进入 MCP 服务器界面，看到小绿勾就说明环境依赖配置成功了：\n调用cherry自带的 fetch MCP 服务器试一下：\n不过注意：上面部署的 Gemini 代理的 OpenAI 格式的api有时会不支持对tools的调用，要换成 Gemini 格式的api；而且有时候回答着突然阻断了。换成 Gemini 格式的api其实就是把 OpenAI 格式的api地址后面的 /hf/v1 删掉就行了，比如 OpenAI 格式的api地址是 https://username-gemini-balance.hf.space/hf/v1/chat/completions，那么 Gemini 格式的api地址就是 https://username-gemini-balance.hf.space/chat/completions。至于阻断，好像没有太好的解决方法。\n再推荐一个谷歌搜索的 MCP 服务器（需要代理）：\nG-Search MCP A powerful MCP server for Google search that enables parallel searching with multiple keywords simultaneously. TypeScript 如果开了代理还是搜索失败的话，可以让 AI 打开搜索的 debug 模式，然后就可以看到浏览器搜索过程了。\n补充 参考链接：\n【gemini代理系列5】huggingface部署gemini代理，账号轮询调用，解锁区域限制 - LINUX DO 4000字长文，写给大家看的MCP使用指南 - LINUX DO Cherry Studio 自定义字体设置：\nsrc: url('file://D:/user/Cherry Studio/LXGWWenKaiTC-Regular.ttf') 改为自己电脑上霞鹜文楷字体\n1@font-face { 2 font-family: \u0026#39;LXGWWenKaiTC-Regular\u0026#39;; 3 src: url(\u0026#39;file://D:/user/Cherry Studio/LXGWWenKaiTC-Regular.ttf\u0026#39;) format(\u0026#39;truetype\u0026#39;);/*此处改为自己电脑上霞鹜文楷字体的安装路径*/ 4 font-weight: normal; 5 font-style: normal; 6 font-display: swap; 7 unicode-range: U+4E00-9FFF /* 扩展CJK字符范围 */ 8} 9 10/* 强制全局应用字体 */ 11*, 12*::after { 13 font-family: 14 \u0026#39;LXGWWenKaiTC-Regular\u0026#39;, /* 西文字体 可更换*/ 15 \u0026#39;LXGWWenKaiTC-Regular\u0026#39;, /* 中文字体 可更换*/ 16 sans-serif !important; /* 强制覆盖所有样式 */ 17} 18 19/* 单独设置等宽字体 */ 20code, pre, kbd, samp, tt { 21 font-family: 22 Consolas, 23 Monaco, 24 \u0026#39;Andale Mono\u0026#39;, 25 \u0026#39;Ubuntu Mono\u0026#39;, 26 monospace !important; 27} 28 29/* 覆盖表单元素 */ 30input, button, textarea, select { 31 font-family: inherit !important; 32} 还有更多好玩的用法就自己探索啦~\n","date":"2025-04-09T10:51:31+08:00","image":"https://langminjeii.ifantic.de/post/gemini.png","permalink":"https://langminjeii.ifantic.de/post/gemini-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E4%BB%A3%E7%90%86%E6%90%AD%E9%85%8D-cherry-studio/","title":"Gemini 负载均衡代理搭配 Cherry Studio"},{"content":"前言 之前在这篇文章里加入了给文章进行密码加密的功能。\n但是不用脑子想都知道，这种加密方式只是掩耳盗铃图一乐。为什么呢？来看一下它的实现原理：\n前端密码存储： 1const correctPassword = {{ .Params.password }}; 这里的密码是从 Hugo 的文章前置参数 (front matter) 中获取的，直接插入到 JavaScript 代码中。\n用户输入密码： 1const enteredPassword = prompt(\u0026#39;请输入文章密码：\u0026#39;); 使用浏览器内置的 prompt 弹窗，让用户输入密码。\n密码校验： 1if (enteredPassword !== correctPassword) { 2 alert(\u0026#39;密码错误！\u0026#39;); 3 if (history.length \u0026lt;= 1) { 4 window.opener = null; 5 window.open(\u0026#39;\u0026#39;, \u0026#39;_self\u0026#39;); 6 window.close(); 7 } else { 8 history.back(); 9 } 10} 如果用户输入的密码与前端存储的密码不一致，则提示密码错误，并尝试关闭页面或返回上一页。\n所以，存在的问题与安全隐患也就很明显了：\n1. 密码明文暴露问题：\n由于 Hugo 是静态网站生成器，所有模板渲染后的内容都是静态 HTML 和 JavaScript 文件。因此，{{ .Params.password }} 会直接以明文形式暴露在生成的 HTML 源代码中。用户只需打开浏览器的“查看源代码”或开发者工具，即可轻松看到密码明文，完全失去了密码保护的意义。 2. 前端校验不安全：\n纯前端的密码校验本质上是不安全的，因为前端代码完全暴露给用户，用户可以轻松绕过或修改校验逻辑。用户甚至可以直接在浏览器控制台中修改 JavaScript 变量，绕过密码检查。 例如，用户可以在控制台中执行： 1enteredPassword = correctPassword; // 直接绕过密码检查 3. 用户体验问题（次要）：\n使用浏览器原生的 prompt 弹窗，界面不美观，用户体验较差。 那么一般来讲，要怎么解决呢？\n1. 后端校验：\n真正安全的密码保护必须在后端实现。静态网站本身无法安全地实现密码保护功能。 可以使用服务器端（如 Node.js、PHP、Python 等）实现密码校验，只有密码正确时才返回文章内容。 2. 第三方服务或 API 校验：\n如果一定要使用静态网站，可以考虑使用第三方服务（如 Netlify Identity、Firebase Authentication）实现安全的身份验证和密码保护。 3. 客户端加密（次优方案）：\n如果一定要纯前端实现，可以考虑使用客户端加密方案（如 AES 加密），密码不直接暴露在源码中，而是存储加密后的内容，用户输入密码后再解密内容。 但即使如此，密钥管理仍然是个问题，安全性仍然有限。 然而，Hugo 是静态网站生成器，生成的内容都是静态文件；Cloudflare Pages 作为一个静态网站托管服务，专门用于托管静态内容（HTML、CSS、JS 等），并不支持动态后端代码（如 PHP、Node.js 等）。\n应用到 Cloudflare Pages 上面，这些加密手段如何呢？\n一、后端校验（不可行）\n原因：Cloudflare Pages 仅支持静态文件托管，不支持运行后端代码（如 Node.js、PHP、Python 等）。 结论：无法直接在 Cloudflare Pages 上实现后端密码校验。 二、第三方服务或 API 校验（推荐，可行）\n虽然 Cloudflare Pages 本身不支持后端代码，但你可以借助第三方服务实现安全的密码保护：\n实现思路：\n使用第三方身份验证服务（如 Firebase Authentication、Auth0、Netlify Identity 等）实现用户登录和密码保护。 用户访问页面时，前端调用第三方服务的 API 进行身份验证，验证通过后再加载受保护的内容。 受保护的内容可以存储在 Cloudflare Pages 上，但内容本身是加密的，只有通过第三方服务验证后才能解密显示。 优点：\n安全性高，密码不会暴露在前端代码中。 用户体验较好，支持多种登录方式（邮箱、Google、GitHub 等）。 缺点：\n需要额外的第三方服务，增加了一定的复杂性。 三、客户端加密方案（可行，但安全性有限）\n实现思路：\n使用 AES 等对称加密算法，在发布文章前对文章内容进行加密。 加密后的内容存储在 Hugo 生成的静态页面中。 用户访问页面时，输入密码后，前端 JavaScript 使用用户输入的密码尝试解密内容。 如果密码正确，内容解密成功并显示；密码错误则无法解密，显示错误提示。 具体实现步骤：\n在本地使用 AES 加密工具（如 CryptoJS）对文章内容进行加密。 将加密后的内容放入 Hugo 的 Markdown 文件或模板中。 在 Hugo 模板中引入 CryptoJS 库，用户输入密码后尝试解密内容。 示例代码（简化版）：\n1\u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 2\u0026lt;script\u0026gt; 3 const encryptedContent = \u0026#34;{{ .Params.encryptedContent }}\u0026#34;; // 加密后的内容 4 const password = prompt(\u0026#34;请输入密码：\u0026#34;); 5 try { 6 const bytes = CryptoJS.AES.decrypt(encryptedContent, password); 7 const originalText = bytes.toString(CryptoJS.enc.Utf8); 8 if (!originalText) throw new Error(\u0026#34;解密失败\u0026#34;); 9 document.getElementById(\u0026#34;content\u0026#34;).innerHTML = originalText; 10 } catch (e) { 11 alert(\u0026#34;密码错误或解密失败！\u0026#34;); 12 } 13\u0026lt;/script\u0026gt; 14\u0026lt;div id=\u0026#34;content\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 优点：\n不需要额外的第三方服务，纯静态实现。 密码不会直接暴露在源码中（但加密后的内容仍然暴露）。 缺点：\n安全性有限，攻击者仍然可以暴力破解或尝试解密。 用户体验一般，且每次修改内容都需要重新加密。 Hugo Encryptor 加密 还有这个Hugo的加密项目：\nHugo Encryptor Hugo-Encryptor 是一款能够帮助作者保护文章内容的工具。它使用 AES-256 来对文章的内容进行加密，并且通过在文章中嵌入内联 JavaScript 代码来验证读者输入的密码是否正确。没有正确的文章密码，读者将无法看到文章的加密内容。 Python 它的实现原理是：\n在 Hugo 构建阶段（本地构建时），使用 AES 算法对文章内容进行加密。 加密后的内容以密文形式存储在生成的静态 HTML 文件中。 用户访问页面时，前端 JavaScript 提示用户输入密码，输入密码后尝试解密内容。 如果密码正确，内容解密成功并显示；密码错误则无法解密，提示错误。 这种方式属于我们之前提到的客户端 AES 加密方案。\n由于 Cloudflare Pages 是纯静态托管服务，hugo_encryptor 生成的内容也是纯静态的 HTML 和 JavaScript 文件，完全适合 Cloudflare Pages。而且 hugo_encryptor 不需要任何后端代码或动态服务，完全依靠客户端 JavaScript 解密，理论上是适合 Cloudflare Pages 这种静态托管环境的。\n缺点也很明显：\n密文内容仍然暴露在前端，理论上存在暴力破解的可能性（但 AES 算法本身安全性较高，暴力破解难度较大）。 密码管理和更改内容时需要重新加密，稍显麻烦。 主要是每次修改提交都要加密一次，实在是有点不胜其烦，于是不想用这个方法。\nWorkers 加密 所以，还有什么方法吗？那当然是继续薅赛博佛祖的羊毛啦！用万能的 Cloudflare Workers 啦！\n新建一个 Workers，然后替换成以下代码：\n1// 内存中存储随机令牌与路径的映射关系 2const tokenStore = new Map(); 3 4async function handleRequest(request, env) { 5 const url = new URL(request.url); 6 const pathname = url.pathname; 7 8 // 强制 HTTPS 协议访问 9 if (url.protocol !== \u0026#34;https:\u0026#34;) { 10 url.protocol = \u0026#34;https:\u0026#34;; 11 return Response.redirect(url.toString(), 301); 12 } 13 14 // 配置受保护的文章 15 const ARTICLES = { 16 \u0026#34;/post/your-private-post/\u0026#34;: { // 你要加密的文章URL 17 password: env.PRIVATE_POST_PASSWORD, // 环境变量名称 18 authType: \u0026#34;permanent\u0026#34;, // 永久授权 19 }, 20 \u0026#34;/post/your-secret-post/\u0026#34;: { // 你要加密的文章URL 21 password: env.SECRET_POST_PASSWORD, // 环境变量名称 22 authType: \u0026#34;temporary\u0026#34;, // 临时授权 23 }, 24 }; 25 26 const articleConfig = ARTICLES[pathname]; 27 if (!articleConfig) { 28 return fetch(request); // 如果路径未配置保护，直接返回原始请求 29 } 30 31 const { password: correctPassword, authType } = articleConfig; 32 33 // 检查 Cookie 中的随机令牌 34 const cookie = request.headers.get(\u0026#34;Cookie\u0026#34;) || \u0026#34;\u0026#34;; 35 const cookies = Object.fromEntries(cookie.split(\u0026#34;; \u0026#34;).map((c) =\u0026gt; c.split(\u0026#34;=\u0026#34;))); 36 const token = cookies[`auth_${pathname}`]; 37 38 if (token \u0026amp;\u0026amp; tokenStore.get(token) === pathname) { 39 // 如果令牌有效，返回原始请求 40 return fetch(request); 41 } 42 43 // 如果是 POST 请求，验证密码 44 if (request.method === \u0026#34;POST\u0026#34;) { 45 const formData = await request.formData(); 46 const enteredPassword = formData.get(\u0026#34;password\u0026#34;); 47 48 if (enteredPassword === correctPassword) { 49 // 密码正确，生成随机令牌 50 const randomToken = crypto.randomUUID(); 51 tokenStore.set(randomToken, pathname); // 将令牌与路径映射存储 52 53 // 设置 Cookie 54 const headers = { 55 Location: pathname, 56 }; 57 if (authType === \u0026#34;permanent\u0026#34;) { 58 // 永久授权：Cookie 有效期 1 年 59 headers[\u0026#34;Set-Cookie\u0026#34;] = `auth_${pathname}=${randomToken}; Path=${pathname}; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000`; 60 } else if (authType === \u0026#34;temporary\u0026#34;) { 61 // 临时授权：Cookie 有效期 1 分钟 62 headers[\u0026#34;Set-Cookie\u0026#34;] = `auth_${pathname}=${randomToken}; Path=${pathname}; HttpOnly; Secure; SameSite=Lax; Max-Age=1`; 63 } 64 65 return new Response(null, { 66 status: 302, 67 headers, 68 }); 69 } else { 70 // 密码错误，返回密码输入页面 71 return new Response(passwordPage(pathname, true), { 72 headers: { \u0026#34;Content-Type\u0026#34;: \u0026#34;text/html;charset=UTF-8\u0026#34; }, 73 }); 74 } 75 } 76 77 // 默认返回密码输入页面 78 return new Response(passwordPage(pathname), { 79 headers: { \u0026#34;Content-Type\u0026#34;: \u0026#34;text/html;charset=UTF-8\u0026#34; }, 80 }); 81} 82 83function passwordPage(pathname, error = false) { 84 return ` 85 \u0026lt;!DOCTYPE html\u0026gt; 86 \u0026lt;html lang=\u0026#34;zh\u0026#34;\u0026gt; 87 \u0026lt;head\u0026gt; 88 \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; 89 \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; 90 \u0026lt;title\u0026gt;请输入密码\u0026lt;/title\u0026gt; 91 \u0026lt;style\u0026gt; 92 body { 93 font-family: \u0026#39;Segoe UI\u0026#39;, Tahoma, Geneva, Verdana, sans-serif; 94 background-image: url(\u0026#39;your img url\u0026#39;); //你的验证页面背景图URL 95 background-size: cover; 96 background-position: center; 97 background-repeat: no-repeat; 98 display: flex; 99 justify-content: center; 100 align-items: center; 101 height: 100vh; 102 margin: 0; 103 padding: 0; 104 } 105 body::before { 106 content: \u0026#39;\u0026#39;; 107 position: fixed; 108 top: 0; 109 left: 0; 110 width: 100%; 111 height: 100%; 112 z-index: -1; 113 backdrop-filter: blur(2px); 114 background-color: rgba(255, 255, 255, 0.7); 115 } 116 .container { 117 background-color: rgba(255, 255, 255, 0.9); 118 padding: 30px; 119 border-radius: 10px; 120 box-shadow: 0 4px 10px rgba(0,0,0,0.3); 121 text-align: center; 122 width: 90%; 123 max-width: 320px; 124 margin: 15px; 125 } 126 h3 { 127 margin-bottom: 20px; 128 color: #333; 129 font-size: 18px; 130 } 131 input[type=\u0026#34;password\u0026#34;] { 132 width: 90%; 133 padding: 12px; 134 margin-bottom: 15px; 135 border-radius: 5px; 136 border: 1px solid #ccc; 137 font-size: 16px; 138 } 139 button { 140 padding: 12px 24px; 141 background-color: #4CAF50; 142 color: white; 143 border: none; 144 border-radius: 5px; 145 cursor: pointer; 146 font-size: 16px; 147 width: 100%; 148 } 149 button:hover { 150 background-color: #45a049; 151 } 152 .error { 153 color: red; 154 margin-bottom: 15px; 155 font-size: 14px; 156 } 157 \u0026lt;/style\u0026gt; 158 \u0026lt;/head\u0026gt; 159 \u0026lt;body\u0026gt; 160 \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; 161 \u0026lt;h3\u0026gt;此文章受密码保护，请输入密码：\u0026lt;/h3\u0026gt; 162 ${error ? \u0026#34;\u0026lt;p class=\u0026#39;error\u0026#39;\u0026gt;密码错误，请重试！\u0026lt;/p\u0026gt;\u0026#34; : \u0026#34;\u0026#34;} 163 \u0026lt;form method=\u0026#34;POST\u0026#34;\u0026gt; 164 \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; required placeholder=\u0026#34;请输入密码\u0026#34; /\u0026gt; 165 \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;提交\u0026lt;/button\u0026gt; 166 \u0026lt;/form\u0026gt; 167 \u0026lt;/div\u0026gt; 168 \u0026lt;/body\u0026gt; 169 \u0026lt;/html\u0026gt; 170 `; 171} 172 173export default { 174 async fetch(request, env) { 175 return handleRequest(request, env); 176 }, 177}; 实现原理 实现原理：\n身份验证流程： 用户访问受保护的文章路径 系统检查用户是否有有效的认证令牌(Cookie) 如果没有有效令牌，显示密码输入页面 用户输入密码后提交，系统验证密码 密码验证成功后，生成随机令牌并设置对应的 Cookie 最后重定向用户到原始文章页面 令牌管理： 使用内存中的 Map(tokenStore) 存储令牌与路径的映射 支持两种授权类型：永久授权（Cookie 有效期1年）和临时授权（Cookie 有效期1分钟） 使用方法 部署好 Workers 后在 设置 -\u0026gt; 变量和机密 点击 添加\n变量名称 一定要大写，对应的是 Workers 里的 PRIVATE_POST_PASSWORD 或者 SECRET_POST_PASSWORD（如 TEST），值就是你要加密文章所要设定的密码（如 password）。\n变量名称一定要大写！变量名称一定要大写！变量名称一定要大写！\n然后同样是在设置里，选择第一项 域和路由，添加 路由，区域 选你的主域名，路由填你要加密的文章路径，就完成咯！\n举例与测试 以我测试的两篇文章为例：\n首先创建了两篇测试文章，一篇用于永久验证的URL是 /post/cf-auth，另一篇用于单次验证的URL是 /post/cf-pwd。然后在 Workers 里修改参数： 1// 配置受保护的文章 2const ARTICLES = { 3 \u0026#34;/post/cf-auth/\u0026#34;: { // 你要加密的文章URL 4 password: env.AUTH_PWD, // 环境变量名称 5 authType: \u0026#34;permanent\u0026#34;, // 永久授权 6 }, 7 \u0026#34;/post/cf-pwd/\u0026#34;: { // 你要加密的文章URL 8 password: env.TEST_PWD, // 环境变量名称 9 authType: \u0026#34;temporary\u0026#34;, // 临时授权 10 }, 11}; 修改完部署后去配置环境变量： 添加路由 区域选择你的博客域名，我这里是 iftcblog.me。然后路由要填你要加密的文章路径，但是前面不要加 https://，也不要把后面的星号 * 忘了。\n前面不要加 https://！记得后面要加上星号 *！\n然后访问永久验证文章与短暂验证文章，就搞定啦！（密码已经写在上面了哦）\n小结 当然还是有潜在的安全问题，如 CSRF 保护和适当的令牌存储机制等等。但是我又不是专业的，感觉做到这个程度已经差不太多了。顺便贴一篇查到的关于 cookies 的文章：Cookie 的 SameSite 属性 | 阮一峰的网络日志\n至于页面的美化么，我又完全不懂前端。不想折腾了，先这么将就着吧，等日后有好的想法再说。\n","date":"2025-04-07T11:21:13+08:00","image":"https://langminjeii.ifantic.de/post/secret.png","permalink":"https://langminjeii.ifantic.de/post/%E7%94%A8-cf-workers-%E5%8A%A0%E5%AF%86%E6%96%87%E7%AB%A0/","title":"用 CF Workers 加密文章"},{"content":"移动端显示目录 找了很多别人的代码，发现在移动端都是显示不了。收到 大佬 的启发，我觉得有可能是移动端在垂直排布时会把右边栏隐藏掉，所以找到 assets/scss/partials/sidebar.scss 找到第73行 display: none; ，把它注释掉：\n1.right-sidebar { 2 width: 100%; 3 display: none; 4 flex-direction: column; 5 gap: var(--widget-separation); 6 7 \u0026amp;.sticky { 8 top: 0; 9 } 10 11 @include respond(lg) { 12 padding-top: var(--main-top-padding); 13 padding-bottom: var(--main-top-padding); 14 } 15} 然后修改 layouts/partials/widget/toc.html 为：\n1{{ if (.Context.Scratch.Get \u0026#34;TOCEnabled\u0026#34;) }} 2 \u0026lt;!-- TOC Toggle Button (visible on small screens) --\u0026gt; 3 \u0026lt;button id=\u0026#34;toc-toggle\u0026#34; class=\u0026#34;toc-toggle-button\u0026#34; aria-label=\u0026#34;Toggle Table of Contents\u0026#34;\u0026gt; 4 {{ partial \u0026#34;helper/icon\u0026#34; \u0026#34;hash\u0026#34; }} 5 \u0026lt;/button\u0026gt; 6 7 \u0026lt;section class=\u0026#34;widget archives\u0026#34; id=\u0026#34;toc-container\u0026#34;\u0026gt; 8 \u0026lt;div class=\u0026#34;widget-icon\u0026#34;\u0026gt; 9 {{ partial \u0026#34;helper/icon\u0026#34; \u0026#34;hash\u0026#34; }} 10 \u0026lt;/div\u0026gt; 11 \u0026lt;h2 class=\u0026#34;widget-title section-title\u0026#34;\u0026gt;{{ T \u0026#34;article.tableOfContents\u0026#34; }}\u0026lt;/h2\u0026gt; 12 13 \u0026lt;div class=\u0026#34;widget--toc\u0026#34;\u0026gt; 14 {{ .Context.TableOfContents }} 15 \u0026lt;/div\u0026gt; 16 \u0026lt;/section\u0026gt; 17 18 \u0026lt;style\u0026gt; 19 /* TOC responsive styles */ 20 @media screen and (max-width: 768px) { 21 #toc-container { 22 position: fixed; 23 top: 0; 24 left: -300px; /* Hide off-screen by default */ 25 width: 280px; 26 height: 100vh; 27 background: linear-gradient( 28 to bottom, 29 rgba(82, 37, 70, 0.9), 30 rgba(136, 48, 78, 0.85) 31 ); 32 box-shadow: 2px 0 8px rgba(94, 95, 92, 0.943); 33 z-index: 1000; 34 overflow-y: auto; 35 transition: left 0.6s ease; 36 padding: 20px; 37 } 38 39 #toc-container.show { 40 left: 0; /* Show when class is added */ 41 } 42 43 .toc-toggle-button { 44 position: fixed; 45 right: 10px; 46 top: 50%; 47 transform: translateY(-50%); 48 z-index: 999; 49 width: 40px; 50 height: 40px; 51 border-radius: 50%; 52 background: rgba(226, 235, 61, 0.943); 53 border: 1px solid rgba(226, 235, 61, 0.943); 54 display: flex; 55 align-items: center; 56 justify-content: center; 57 cursor: pointer; 58 box-shadow: 0 2px 5px #e84242e4; 59 } 60 61 body.toc-open { 62 overflow: hidden; /* Prevent scrolling when TOC is open */ 63 } 64 } 65 66 @media screen and (min-width: 769px) { 67 .toc-toggle-button { 68 display: none; /* Hide button on larger screens */ 69 } 70 } 71 \u0026lt;/style\u0026gt; 72 73 \u0026lt;script\u0026gt; 74 document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { 75 const tocToggle = document.getElementById(\u0026#39;toc-toggle\u0026#39;); 76 const tocContainer = document.getElementById(\u0026#39;toc-container\u0026#39;); 77 const body = document.body; 78 79 tocToggle.addEventListener(\u0026#39;click\u0026#39;, function() { 80 tocContainer.classList.toggle(\u0026#39;show\u0026#39;); 81 body.classList.toggle(\u0026#39;toc-open\u0026#39;); 82 }); 83 84 // Close TOC when clicking outside of it 85 document.addEventListener(\u0026#39;click\u0026#39;, function(e) { 86 if (window.innerWidth \u0026lt;= 768) { 87 const isClickInsideTOC = tocContainer.contains(e.target); 88 const isClickOnButton = tocToggle.contains(e.target); 89 90 if (!isClickInsideTOC \u0026amp;\u0026amp; !isClickOnButton \u0026amp;\u0026amp; tocContainer.classList.contains(\u0026#39;show\u0026#39;)) { 91 tocContainer.classList.remove(\u0026#39;show\u0026#39;); 92 body.classList.remove(\u0026#39;toc-open\u0026#39;); 93 } 94 } 95 }); 96 97 // Add this: Close TOC when clicking on any link inside the TOC 98 tocContainer.querySelectorAll(\u0026#39;a\u0026#39;).forEach(link =\u0026gt; { 99 link.addEventListener(\u0026#39;click\u0026#39;, function() { 100 if (window.innerWidth \u0026lt;= 768) { 101 tocContainer.classList.remove(\u0026#39;show\u0026#39;); 102 body.classList.remove(\u0026#39;toc-open\u0026#39;); 103 } 104 }); 105 }); 106 }); 107 \u0026lt;/script\u0026gt; 108{{ end }} 这样就能在左边显示目录的按钮啦！但是由于上面的方法是简单粗暴的取消右边栏的隐藏，所以就算是在主页往下滑也能看见搜索、归档、标签和分类栏，我是觉得这样还不错，就不折腾了（其实是懒得改）。\n多语言文字字体修改 参考大佬的教程去Google fonts上找了一下，发现简中的都是抽象得不能再抽象的字体。然后转头去找繁体的字体，发现虽然同样只有8种字体，但这不比简中的好看多了？\n然后我惊奇的发现，我用的 LXGW WenKai TC 字体是能支持简中的。好你个谷歌做的什么**玩意（骂骂咧咧）\n比如我选择 LXGW WenKai TC 这个字体，点击右上角的Get font\n然后点击左侧的Get embed code\n复制右边的代码，把原先 layouts/partials/footer/components/custom-font.html 里的内容全部注释掉，贴进去\n如果想选多种字体，就按相同的步骤在 family= 后面跟上你想用的字体名称。\n我修改后的 custom-font.html 内容如下：\n1\u0026lt;!-- \u0026lt;script\u0026gt; 2 (function () { 3 const customFont = document.createElement(\u0026#39;link\u0026#39;); 4 customFont.href = \u0026#34;https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700\u0026amp;display=swap\u0026#34;; 5 6 customFont.type = \u0026#34;text/css\u0026#34;; 7 customFont.rel = \u0026#34;stylesheet\u0026#34;; 8 9 document.head.appendChild(customFont); 10 }()); 11\u0026lt;/script\u0026gt; --\u0026gt; 12 13\u0026lt;link rel=\u0026#34;preconnect\u0026#34; href=\u0026#34;https://fonts.googleapis.com\u0026#34;\u0026gt; 14\u0026lt;link rel=\u0026#34;preconnect\u0026#34; href=\u0026#34;https://fonts.gstatic.com\u0026#34; crossorigin\u0026gt; 15\u0026lt;link href=\u0026#34;https://fonts.googleapis.com/css2?family=LXGW+WenKai+TC\u0026amp;family=Gowun+Batang\u0026amp;family=Noto+Serif+SC:wght@300\u0026amp;family=Noto+Serif+TC:wght@300\u0026amp;display=swap\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; 然后我又发现了一个问题：多语言要怎么设置呢？网上找了一圈也没找着答案，凭我三脚猫的功夫看文档也搞不定这个问题啊。在我抓耳挠腮几近放弃的时候，突然想起了近几年最伟大的发明————AI！于是去问了Claude3.7，终于解决了（不愧是代码届的神）。大佬的老方法是在 custom.scss 最后加入：\n1body, .article-content { 2 // font-family: \u0026#39;Long Cang\u0026#39;, cursive; 3 // font-family: \u0026#39;Ma Shan Zheng\u0026#39;, cursive; 4 // font-family: \u0026#39;Noto Sans SC\u0026#39;, sans-serif; 5 font-family: \u0026#39;Noto Serif SC\u0026#39;, serif; 6 // font-family: \u0026#39;Zhi Mang Xing\u0026#39;, cursive; 7} 但是我韩语的字体还是默认的。按照Claude3.7的回答，只需要在 custom.scss 最后加入：\n1/* 为中英韩文内容设置字体 */ 2:lang(en), 3:lang(zh-cn), 4:lang(ko), 5:lang(zh-hk) { 6 font-family: \u0026#34;LXGW WenKai TC\u0026#34;, \u0026#34;Gowun Batang\u0026#34;, serif; 7} 这样简中、繁中都是 LXGW WenKai TC 字体，韩语都是 Gowun Batang 字体啦！\n英语是 LXGW WenKai TC 附带的字体，我觉得还不错就不改了。\n我还添加了第二字体，比如说韩语界面如果有中文的话就不会回退到默认的中文字体，而是用 LXGW WenKai TC 字体。\n至于韩语界面，我发现如果把 Gowun Batang 放前面 LXGW WenKai TC 放后面的话，那英文字体就会用 Gowun Batang 的，跟后者相比感觉略逊一筹。所以直接把 LXGW WenKai TC 放在最前面，然后四种语言合并在一起就好了。\n但是好像有个问题，因为是谷歌CDN fonts.googleapis.com 的字体，第一次加载会有点慢（有时候不止第一次）。暂时把 fonts.googleapis.com 改成 fonts.googleapis.cn ，其实好像也没差多少，索性不管了，反正也不至于特别慢吧~~。\n碎碎念页面 成果展示\n前置准备 可以用 Artitalk.js 来完成一个碎碎念的页面。\n在 hugo.yaml 中找到 homepage，在下方添加 - type: artitalk 。 在 content/page 文件夹下新建 artitalk 文件夹，其中新建 index.md。 前往 LeanCloud 国际版，注册账号。 注册完成之后根据 LeanCloud 的提示绑定手机号和邮箱。 绑定完成之后点击 创建应用 ，应用名称随意。 接着在 结构化数据 中创建 class，命名为 shuoshuo。 在你新建的应用中找到 结构化数据 下的 用户 。点击 添加用户 ，输入想用的用户名及密码。 回到 结构化数据 中，点击 class 下的 shuoshuo。找到权限，在 Class 访问权限 中将 add_fields 以及 create 权限设置为指定用户，输入你刚才输入的用户名会自动匹配。为了安全起见，将 delete 和 update 也设置为跟它们一样的权限。 然后再新建一个名为 atComment 的class，权限什么的使用默认的即可。 点击 _User，添加列，列名称为 img，默认值填上你这个账号想要用的发布说说的头像url。这一项如果不进行配置，说说头像会显示为默认头像 —— Artitalk 的 logo。 在最菜单栏中找到设置 -\u0026gt; 应用 keys，记下来 AppID 和 AppKey ，一会会用。 最后将 _User 中的权限全部调为指定用户，或者数据创建者，为了保证不被篡改用户数据以达到强制发布说说。 踩坑记录 由于 LeanCloud 的域名疑似被墙了，最好在 cloudflare 中创建 workers 进行部署。在 workers 中填入以下 代码： 1//此脚本由MHuiG - MiniValine写成，ChenYFan将其做少量修改制成，此脚本仅适用于Artitalk 2 3async function handleRequest(event) { 4 AppId = (function () { try { return APPID } catch (e) { return \u0026#34;\u0026#34; } })() 5 AppKey = (function () { try { return APPKEY } catch (e) { return \u0026#34;\u0026#34; } })() 6 if (AppId == \u0026#34;\u0026#34; || AppKey == \u0026#34;\u0026#34;) { return new Response(\u0026#39;Artitalk-Safe异常：您没有设定appid和appkey\u0026#39;) } 7 ServerDomain = (function () { try { return SERVERDOMAIN } catch (e) { return `${(AppId.substr(0, 8)).toLowerCase()}.api.lncldglobal.com` } })() 8 atComment = (function () { try { return ATCOMMENT == \u0026#34;true\u0026#34; ? true : false } catch (e) { return true } })() 9 CORS = (function () { try { return CORS } catch (e) { return \u0026#39;*\u0026#39; } })() 10 const request = event.request 11 const url = new URL(request.url) 12 const urlObj = new URL(url) 13 const path = urlObj.href.substr(urlObj.origin.length) 14 const classac = l(rp(path).split(\u0026#39;/\u0026#39;)) 15 if (classac == \u0026#34;atComment\u0026#34; \u0026amp;\u0026amp; !atComment) { return new Response(\u0026#39;{\u0026#34;code\u0026#34;:101,\u0026#34;error\u0026#34;:\u0026#34;Artitalk-Safe：评论功能未开启\u0026#34;}\u0026#39;, { headers: { \u0026#34;content-type\u0026#34;: \u0026#34;application/json;charset=utf-8\u0026#34; } }) } 16 if (classac == \u0026#34;_File\u0026#34; || classac == \u0026#34;_Followee\u0026#34; || classac == \u0026#34;_Follower\u0026#34; || classac == \u0026#34;_Installation\u0026#34; || classac == \u0026#34;_Role\u0026#34;) { return new Response(\u0026#39;{\u0026#34;code\u0026#34;:101,\u0026#34;error\u0026#34;:\u0026#34;Artitalk-Safe：操作是禁止的\u0026#34;}\u0026#39;, { headers: { \u0026#34;content-type\u0026#34;: \u0026#34;application/json;charset=utf-8\u0026#34; } }) } 17 18 url.hostname = ServerDomain 19 reqHEDNew = new Headers(request.headers) 20 if (reqHEDNew.get(\u0026#34;X-LC-Id\u0026#34;)) { 21 reqHEDNew.set(\u0026#34;X-LC-Id\u0026#34;, AppId) 22 reqHEDNew.set(\u0026#34;X-LC-Key\u0026#34;, AppKey) 23 } 24 if (reqHEDNew.get(\u0026#34;x-lc-sign\u0026#34;)) { 25 reqHEDNew.delete(\u0026#34;X-LC-Sign\u0026#34;) 26 } 27 reqNew = new Request(request, { headers: reqHEDNew }) 28 responsefetch = await fetch(url.toString(), reqNew) 29 resHEDNew = new Headers(responsefetch.headers) 30 resHEDNew.set(\u0026#34;Access-Control-Allow-Origin\u0026#34;, CORS) 31 response = new Response(responsefetch.body, { headers: resHEDNew }) 32 return response 33} 34function rp(p) { 35 return p.split(\u0026#34;?\u0026#34;)[0] 36} 37function l(p) { 38 return p[getJsonLength(p) - 1] 39} 40 41 42function getJsonLength(jsonData) { 43 44 var jsonLength = 0; 45 46 for (var item in jsonData) { 47 48 jsonLength++; 49 50 } 51 52 return jsonLength; 53} 54addEventListener(\u0026#34;fetch\u0026#34;, event =\u0026gt; { 55 event.respondWith(handleRequest(event)) 56}) 设置两个变量 APPID 和 APPKEY 变量名务必大写！！！\n最后在 layouts/_default/ 下新增 artitalk.html , 并修改样式如下。 注意，用 cloudflare workers 是 Artitalk.js 推荐的 Artitalk_SafeMode（貌似无法正常使用），通过中间件的方式，在中间件替换 APPID 和 APPKEY 保护两者，避免两者暴露被刷。因此你可以在前端随意伪造 APPID 和 APPKEY ，然后将 serverurl 修改为中间件网址即可。\n1{{ define \u0026#34;main\u0026#34; }} 2 \u0026lt;!-- 引用 artitalk --\u0026gt; 3 \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;https://unpkg.com/artitalk\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 4 \u0026lt;!-- 存放说说的容器 --\u0026gt; 5 \u0026lt;div id=\u0026#34;artitalk_main\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 6 \u0026lt;script\u0026gt; 7 new Artitalk({ 8 appId: \u0026#39;\u0026#39;, // 随便填 9 appKey: \u0026#39;\u0026#39;, // 随便填 10 pageSize: 15, // 每页说说数量 11 color3: \u0026#34;var(--card-text-color-main)\u0026#34;, // 文字颜色 12 color1: \u0026#34;var(--card-background)\u0026#34;, // 背景颜色1 13 color2: \u0026#34;var(--card-background)\u0026#34;, // 背景颜色2 14 serverURL: \u0026#34;\u0026#34;, // 你的cloudflare worker域名，可以绑定自己的域名 15 }) 16 \u0026lt;/script\u0026gt; 17{{ end }} 最后配置 但是发现用cf workers还是会被墙，无法正常使用。只能绕个弯路，给应用绑定一个api域名：点击设置 -\u0026gt; 域名绑定 -\u0026gt; 绑定新域名 然后去cloudflare的对应域名添加上这个 CNAME 记录就可以了。 注意，不要开启小黄云！！！\n添加 CNAME 记录之后它会自动帮你申请证书，等待大概几分钟之后显示“已绑定”，就大功告成啦！ 最后在 layouts/_default/ 下新增 artitalk.html , 并修改样式如下。 1{{ define \u0026#34;main\u0026#34; }} 2 \u0026lt;!-- 引用 artitalk --\u0026gt; 3 \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;https://unpkg.com/artitalk\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 4 \u0026lt;!-- 存放说说的容器 --\u0026gt; 5 \u0026lt;div id=\u0026#34;artitalk_main\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 6 \u0026lt;script\u0026gt; 7 new Artitalk({ 8 appId: \u0026#39;\u0026#39;, // your appId 9 appKey: \u0026#39;\u0026#39;, // your appKey 10 pageSize: 15, // 每页说说数量 11 color3: \u0026#34;var(--card-text-color-main)\u0026#34;, // 文字颜色 12 color1: \u0026#34;var(--card-background)\u0026#34;, // 背景颜色1 13 color2: \u0026#34;var(--card-background)\u0026#34;, // 背景颜色2 14 serverURL: \u0026#34;\u0026#34;, // 上面绑定的api域名 15 }) 16 \u0026lt;/script\u0026gt; 17{{ end }} 然后我发现这个碎碎念页面加载有时候很慢，感觉还是js的问题。跟热力图的方案一样，把js下载下来放到 static/js 文件夹里，然后把 artitalk.html 里第二行改成：\n1\u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;/js/artitalk.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 最后我发现星屑右下角的发布按钮会被我的返回顶部按钮与聊天气泡重叠，并且暗黑模式下预览框的文字是黑色的看不见，于是在 assets/scss/custom.scss 里稍微修改了一下：\n1#artitalk_main .artitalk_child { 2 margin-right: 15%; 3} 4 5#artitalk_main .power { 6 margin-right: 15%; 7 margin-bottom: 2%; 8} 9 10[data-scheme=\u0026#34;dark\u0026#34;] { 11 #artitalk_main #preview { 12 color: #FFF; 13 } 14} 右侧导航栏动画 参考链接，在 assets/scss/custom.scss 中加入以下代码：\n1/*------------------右侧导航栏--------------*/ 2// 搜索菜单动画 3.search-form.widget { 4 transition: transform 0.6s ease; 5} 6 7.search-form.widget:hover { 8 transform: scale(1.1, 1.1); 9} 10 11 12//归档小图标放大动画 13.widget.archives .widget-archive--list { 14 transition: transform .3s ease; 15} 16 17.widget.archives .widget-archive--list:hover { 18 transform: scale(1.05, 1.05); 19} 20 21 22//右侧标签放大动画 23.tagCloud .tagCloud-tags a { 24 border-radius: 10px; 25 font-size: 1.4rem; 26 transition: transform .3s ease; 27} 28 29.tagCloud .tagCloud-tags a:hover { 30 transform: scale(1.1, 1.1); 31} 相关文章卡片缩小 在 assets/scss/custom.scss 最后加上：\n1.related-content article { 2 width: 200px; 3 height: 120px; 4} 自定义鼠标指针样式 参考了大佬的博客：hugo stack 主题美化 2 - yelleis的博客\nCustom Cursor选个喜欢的样式，截图（正方形）； remove.bg或 RemovePhotos 改成透明底； 在线图片压缩工具尺寸压缩到 50x50； 上传到图床，复制链接到 url('') 内即可。 增添到 assets/scss/custom.scss：\n1body { 2 cursor: url(/img/pointer.png), auto; 3} 4 5a:hover{ 6 cursor:url(\u0026#39;/img/link.png\u0026#39;), pointer !important; 7} //指向链接时的鼠标样式 8 9button:hover { 10 cursor: url(\u0026#39;/img/text.png\u0026#39;), pointer !important; 11} //代码复制按钮 12 13input:hover { 14 cursor: url(\u0026#39;/img/text.png\u0026#39;), text !important; 15} //评论框(好像没有用欸) 添加背景图片 参考链接，在 assets/scss/custom.scss 修改了一下字体颜色，然后加了个背景图：\n1:root { 2 --accent-color: #255F38; 3 --accent-color-darker: #FE4F2D; 4 --accent-color-text: #FFF; 5 --body-text-color: #4F1C51; 6} 7 8:root { 9 --card-background-selected: #EBEBEB; 10 --card-text-color-tertiary: #888B8D; 11} 12 13// 浅色模式背景图片设置 14[data-scheme=\u0026#34;light\u0026#34;] body { 15 position: relative; 16 background-image: url(\u0026#39;/img/bj.jpg\u0026#39;); // 替换为你的图片路径 17 background-attachment: fixed; 18 background-size: cover; 19 background-position: center; 20} 21 22// 添加遮罩层 23[data-scheme=\u0026#34;light\u0026#34;] body::before { 24 content: \u0026#39;\u0026#39;; 25 position: fixed; 26 top: 0; 27 left: 0; 28 width: 100%; 29 height: 100%; 30 z-index: -1; 31 backdrop-filter: blur(2px); // 可选：添加模糊效果 32 // 浅色模式遮罩（较浅） 33 background-color: rgba(255, 255, 255, 0.7); 34} 把滚动条换成进度条 弄了背景之后滚动条的背景块会跟随着主题的颜色走，找了很久也没找到解决办法。突然找到Hugo：阅读进度条，那我把滚动条隐藏起来换成进度条不就好了吗？把 assets/scss/custom.scss 里面滚动条的 display 属性改成 none。\n1.menu::-webkit-scrollbar { 2 display: display; 3 display: none; 4} 如果在所有页面上都实现这个效果，可以将html代码插入到 layouts\\_default\\baseof.html 并引用css和js文件，若是只想在文章页面实现，可以插入到 layouts\\_default\\single.html。\n1\u0026lt;progress id=\u0026#34;content_progress\u0026#34; value=\u0026#34;0\u0026#34;\u0026gt;\u0026lt;/progress\u0026gt; 2\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/progress.css\u0026#34;\u0026gt; 3\u0026lt;script src=\u0026#34;/js/progress.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 然后新建 static\\css\\progress.css：\n1#content_progress { 2 /* Positioning */ 3 position: fixed; 4 left: 0; 5 bottom: 0; 6 z-index: 1000; 7 width: 100%; 8 height: 6px; 9 -webkit-appearance: none; 10 -moz-appearance: none; 11 appearance: none; 12 border: none; 13 background-color: transparent; 14 color: #3F7D58; 15} 16 17#content_progress::-webkit-progress-bar { 18 background-color: transparent; 19} 20 21#content_progress::-webkit-progress-value { 22 background-color: #5F99AE; 23} 24 25#content_progress::-moz-progress-bar { 26 background-color: #5F99AE; 27} 新建 static\\js\\progress.js:\n1document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function () { 2 var winHeight = window.innerHeight, 3 docHeight = document.documentElement.scrollHeight, 4 progressBar = document.querySelector(\u0026#39;#content_progress\u0026#39;); 5 progressBar.max = docHeight - winHeight; 6 progressBar.value = window.scrollY; 7 8 document.addEventListener(\u0026#39;scroll\u0026#39;, function () { 9 progressBar.max = document.documentElement.scrollHeight - window.innerHeight; 10 progressBar.value = window.scrollY; 11 }); 12}); 但仅仅这么配置的话代码块也没有滚动条了，于是在 .highlight 部分加上滚动条配置。\n1.highlight { 2 ::-webkit-scrollbar { 3 display: flex; 4 } 5} 主页添加樱花特效 之前弄的樱花特效，感觉用来配我新的主页背景挺合适的。但是我又觉得这个特效有点眼花缭乱，最后决定只让它在浅色模式下生效。打开 layouts/partials/footer/custom.html 在最后加上：\n1{{ if or .IsHome (eq .Layout \u0026#34;archives\u0026#34;) (eq .Type \u0026#34;page\u0026#34;) }} 2\u0026lt;script\u0026gt; 3(function() { 4 // 检测当前是否为深色模式 5 function isDarkTheme() { 6 const savedTheme = localStorage.getItem(\u0026#39;StackColorScheme\u0026#39;); 7 const prefersDark = window.matchMedia(\u0026#39;(prefers-color-scheme: dark)\u0026#39;).matches; 8 const htmlHasDarkClass = document.documentElement.classList.contains(\u0026#39;dark\u0026#39;); 9 10 return savedTheme === \u0026#39;dark\u0026#39; || htmlHasDarkClass || 11 (savedTheme !== \u0026#39;light\u0026#39; \u0026amp;\u0026amp; prefersDark); 12 } 13 14 // 完全清理樱花特效 15 function cleanupSakuraEffect() { 16 // 移除所有可能的樱花元素（多种可能的类名） 17 [\u0026#39;sakura\u0026#39;, \u0026#39;ukiis-sakura\u0026#39;].forEach(className =\u0026gt; { 18 document.querySelectorAll(\u0026#39;.\u0026#39; + className).forEach(el =\u0026gt; el.remove()); 19 }); 20 21 // 查找并移除可能的动画容器 22 document.querySelectorAll(\u0026#39;canvas[style*=\u0026#34;position: fixed\u0026#34;]\u0026#39;).forEach(el =\u0026gt; el.remove()); 23 24 // 停止可能存在的动画循环 25 if (window.requestAnimationFrame \u0026amp;\u0026amp; window.cancelAnimationFrame) { 26 if (window.sakuraRequestId) { 27 window.cancelAnimationFrame(window.sakuraRequestId); 28 window.sakuraRequestId = null; 29 } 30 } 31 32 console.log(\u0026#39;Sakura elements completely cleaned up\u0026#39;); 33 } 34 35 // 管理樱花特效的加载与移除 36 function manageSakuraEffect() { 37 const isDark = isDarkTheme(); 38 const existingScript = document.querySelector(\u0026#39;script[src*=\u0026#34;UkennWeb\u0026#34;]\u0026#39;); 39 40 if (!isDark) { 41 // 浅色模式：如果未加载则加载特效 42 if (!existingScript) { 43 const script = document.createElement(\u0026#39;script\u0026#39;); 44 script.type = \u0026#39;text/javascript\u0026#39;; 45 script.src = \u0026#39;https://cdn.jsdelivr.net/gh/Ukenn2112/UkennWeb@3.0/index/web.js\u0026#39;; 46 document.body.appendChild(script); 47 console.log(\u0026#39;Sakura effect added for light theme\u0026#39;); 48 } 49 } else { 50 // 深色模式：先清理特效，再移除脚本 51 cleanupSakuraEffect(); 52 53 // 移除脚本 54 if (existingScript) { 55 existingScript.remove(); 56 console.log(\u0026#39;Sakura script removed for dark theme\u0026#39;); 57 } 58 59 // 查找并移除其他可能的相关脚本 60 document.querySelectorAll(\u0026#39;script[src*=\u0026#34;sakura\u0026#34;]\u0026#39;).forEach(el =\u0026gt; el.remove()); 61 } 62 } 63 64 // 初始加载时执行一次 65 manageSakuraEffect(); 66 67 // 监听localStorage变化（覆盖setItem方法） 68 const originalSetItem = localStorage.setItem; 69 localStorage.setItem = function(key, value) { 70 originalSetItem.apply(this, arguments); 71 if (key === \u0026#39;StackColorScheme\u0026#39;) { 72 setTimeout(manageSakuraEffect, 100); // 添加短暂延迟确保DOM更新 73 } 74 }; 75 76 // 精确监听html元素的class变化 77 const observer = new MutationObserver((mutations) =\u0026gt; { 78 mutations.forEach((mutation) =\u0026gt; { 79 if (mutation.type === \u0026#39;attributes\u0026#39; \u0026amp;\u0026amp; mutation.attributeName === \u0026#39;class\u0026#39;) { 80 setTimeout(manageSakuraEffect, 100); // 添加短暂延迟确保DOM更新 81 } 82 }); 83 }); 84 85 observer.observe(document.documentElement, { 86 attributes: true, 87 attributeFilter: [\u0026#39;class\u0026#39;] 88 }); 89 90 // 监听主题切换按钮点击事件 91 document.addEventListener(\u0026#39;click\u0026#39;, (e) =\u0026gt; { 92 if (e.target.closest(\u0026#39;.color-scheme-toggle\u0026#39;)) { 93 setTimeout(manageSakuraEffect, 300); // 延长延迟时间以确保主题切换完成 94 } 95 }); 96 97 // 监听系统深色模式变化 98 window.matchMedia(\u0026#39;(prefers-color-scheme: dark)\u0026#39;).addEventListener(\u0026#39;change\u0026#39;, () =\u0026gt; { 99 setTimeout(manageSakuraEffect, 100); 100 }); 101 102 // 额外添加一个全局清理方法，以便在需要时手动调用 103 window.cleanupSakuraEffect = cleanupSakuraEffect; 104})(); 105\u0026lt;/script\u0026gt; 106{{ end }} 文章生成日历 新建文件 layouts/page/calendar.html ，加入以下代码：\n1{{ define \u0026#34;main\u0026#34; }} 2 3\u0026lt;style\u0026gt; 4 #calendar { 5 /* max-width: 1200px; */ 6 width: 90%; /* 使其占满可用空间 */ 7 margin: 50px auto; 8 padding: 0 10px; 9 border-radius: 8px; 10 box-shadow: 0 2px 10px rgba(241, 62, 232, 0.9); 11 } 12 13 /* 新增：按钮颜色自定义 */ 14 .fc-button-primary { 15 background-color: #c511cb !important; /* 紫色 */ 16 border-color: #c511cbd7 !important; 17 } 18 .fc-button-primary:hover { 19 background-color: #f476ffe0 !important; /* 深紫色 */ 20 } 21 22 .fc-toolbar-title { 23 font-size: 1.3em; 24 font-weight: bold; 25 } 26 27 .fc-event { 28 font-size: 0.8em; 29 border-radius: 4px; 30 padding: 2px; 31 } 32 33 .fc-header-toolbar { 34 padding: 10px 0; 35 } 36 37 .fc-button { 38 background-color: #f6ff00e3; 39 border: none; 40 border-radius: 4px; 41 color: white; 42 padding: 5px 10px; 43 font-size: 0.9em; 44 cursor: pointer; 45 } 46 47 .fc-button:hover { 48 background-color: #0056b3; 49 } 50 51 .fc-button:disabled { 52 background-color: #e0e0e0; 53 color: #757575; 54 } 55 56 .fc-toolbar-title { 57 color: rgba(241, 62, 232, 0.9); 58 } 59 60 .event-tooltip { 61 position: absolute; 62 z-index: 10001; 63 background-color:rgba(234, 255, 0, 0.600); 64 border: 1px solid #ccc; 65 padding: 8px; 66 border-radius: 4px; 67 box-shadow: 0 2px 10px rgba(207, 50, 50, 0.1); 68 font-size: 0.9em; 69 } 70 71 /* 修复按钮间距问题 */ 72 .fc-button-group { 73 gap: 4px; /* 增加按钮间的间隙 */ 74 } 75 76 /* 响应式调整 */ 77 @media (max-width: 768px) { 78 #calendar { 79 width: 95%; 80 margin: 20px auto; 81 } 82 .fc-toolbar-title { 83 font-size: 1em; 84 } 85 .fc-button { 86 padding: 3px 6px; 87 font-size: 0.8em; 88 } 89 } 90 91 @media (max-width: 480px) { 92 #calendar { 93 width: 100%; 94 padding: 0 5px; 95 } 96 .fc-header-toolbar { 97 flex-direction: column; 98 align-items: center; 99 } 100 .fc-toolbar-chunk { 101 margin-bottom: 5px; 102 } 103 } 104 105 @media (min-width: 1200px) { 106 #calendar { 107 max-width: 1100px; 108 } 109 } 110 111 112\u0026lt;/style\u0026gt; 113 114\u0026lt;div id=\u0026#39;calendar\u0026#39;\u0026gt;\u0026lt;/div\u0026gt; 115\u0026lt;script src=\u0026#39;https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js\u0026#39;\u0026gt;\u0026lt;/script\u0026gt; 116 117\u0026lt;script\u0026gt; 118 document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function () { 119 const calendarEl = document.getElementById(\u0026#39;calendar\u0026#39;); 120 const calendar = new FullCalendar.Calendar(calendarEl, { 121 initialView: \u0026#39;dayGridMonth\u0026#39;, // 預設檢視模式 122 locale: \u0026#34;zh-cn\u0026#34;, // 預設語言 123 contentHeight: 600, 124 headerToolbar: { // 工具列 125 left: \u0026#39;prevYear,prev\u0026#39;, 126 center: \u0026#39;title\u0026#39;, 127 right: \u0026#39;today,next,nextYear\u0026#39; 128 }, 129 buttonText: { // 自訂翻譯 130 today: \u0026#39;今天\u0026#39;, 131 month: \u0026#39;月\u0026#39;, 132 }, 133 events: [ 134 {{ range .Site.RegularPages }} // 讀取所有已發表的文章，變成JSON陣列 135 { 136 title: \u0026#39;{{ .Title }}\u0026#39;, 137 start: \u0026#39;{{ .Date.Format \u0026#34;2006-01-02\u0026#34; }}\u0026#39;, 138 url: \u0026#39;{{ .RelPermalink }}\u0026#39; 139 }, 140 {{ end }} 141 ], 142 eventMouseEnter: function (info) { // 加入滑鼠懸浮就顯示完整標題的功能 143 const tooltip = document.createElement(\u0026#39;div\u0026#39;); 144 tooltip.className = \u0026#39;event-tooltip\u0026#39;; 145 tooltip.innerHTML = `\u0026lt;strong\u0026gt;${info.event.title}\u0026lt;/strong\u0026gt;`; 146 document.body.appendChild(tooltip); 147 148 tooltip.style.left = info.jsEvent.pageX + \u0026#39;px\u0026#39;; 149 tooltip.style.top = info.jsEvent.pageY + \u0026#39;px\u0026#39;; 150 151 info.el.addEventListener(\u0026#39;mouseleave\u0026#39;, function () { 152 tooltip.remove(); 153 }); 154 } 155 }); 156 calendar.render(); 157}); 158\u0026lt;/script\u0026gt; 159 160{{ end }} 在 content/page 下新建 calendar.md ，填入：\n1--- 2title: 日历 3layout: \u0026#34;calendar\u0026#34; 4menu: 5 main: 6 weight: 12 7 params: 8 icon: calendar 9--- icon 的话，可以去 这里 找到你喜欢的svg，新建一个 assets/icons/calendar.svg 粘贴进去就行。\n然后就能在侧边栏加入日历，查看你写过的所有文章啦！\n不过这个与热力图的功能有点重叠，而且统计的文章不分语言。（也好，可以用一些水文章撑一下场面）\n左边栏网站副标题换行 找到 layouts/partials/sidebar/left.html，修改第36行：\n1\u0026lt;div class=\u0026#34;site-meta\u0026#34;\u0026gt; 2 \u0026lt;h1 class=\u0026#34;site-name\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;{{ .Site.BaseURL | relLangURL }}\u0026#34;\u0026gt;{{ .Site.Title }}\u0026lt;/a\u0026gt;\u0026lt;/h1\u0026gt; 3 \u0026lt;h2 class=\u0026#34;site-description\u0026#34;\u0026gt;{{ .Site.Params.sidebar.subtitle }}\u0026lt;/h2\u0026gt; 4 \u0026lt;h2 class=\u0026#34;site-description\u0026#34;\u0026gt;{{ .Site.Params.sidebar.subtitle | safeHTML }}\u0026lt;/h2\u0026gt; 5\u0026lt;/div\u0026gt; 然后在 hugo.yaml 中在要换行的地方加上 \u0026lt;br\u0026gt;：\n1languages: 2 zh-cn: 3 params: 4 sidebar: 5 subtitle: \u0026#34;鸳鸯帐里暖芙蓉\u0026lt;br\u0026gt;此生何处不相逢\u0026#34; 就可以把诗当网站的简介啦！\n右侧分类条优化 调整分类的样式并在首页显示分类的条目数量，然后给每个分类增加对应的颜色条。参考链接，把 layouts/partials/widget/categories.html 第11和12行改成：\n1{{ range first $limit $context.Site.Taxonomies.categories.ByCount }} 2 \u0026lt;a href=\u0026#34;{{ .Page.RelPermalink }}\u0026#34; class=\u0026#34;font_size_{{ .Count }}\u0026#34;\u0026gt; 3 {{ .Page.Title }} 4 \u0026lt;a href=\u0026#34;{{ .Page.RelPermalink }}\u0026#34; class=\u0026#34;font_size_{{ .Count }}\u0026#34; style=\u0026#34;border-left: 6px solid {{ .Page.Params.style.background }}; filter:saturate(1.7);\u0026#34;\u0026gt; 5 {{ .Page.Title }}\u0026lt;span class=\u0026#34;category-count count-muted\u0026#34;\u0026gt; {{ .Count }}\u0026lt;/span\u0026gt; 6 \u0026lt;/a\u0026gt; 7{{ end }} 然后在 assets/scss/custom.scss 后面加上：\n1.count-muted { 2 color: #b7bec5; /* 常见灰色值，可根据需要调整 */ 3 opacity: 0.8; /* 可选：如果需要进一步淡化 */ 4} 最后在每个分类的 _index.md 添加以下参数：\n1--- 2style: 3 background: \u0026#34;#d09daa\u0026#34; 4 color: \u0026#34;#fff\u0026#34; 5--- 好看的配色可以去Color Hunt找找灵感，需要微调的话可以参考Color Hex。\n最底下的标签云也可以照猫画虎，不过我觉得没必要给标签左侧也加上颜色条，还要找配色。所以只在后面加上统计数就算了，修改 layouts/partials/widget/tag-cloud.html：\n1\u0026lt;a href=\u0026#34;{{ .Page.RelPermalink }}\u0026#34; class=\u0026#34;font_size_{{ .Count }}\u0026#34;\u0026gt; 2 {{ .Page.Title }} 3 {{ .Page.Title }} \u0026lt;span class=\u0026#34;category-count count-muted\u0026#34;\u0026gt; {{ .Count }}\u0026lt;/span\u0026gt; 4\u0026lt;/a\u0026gt; 设置不能被搜索到的文章 找到 /layouts/page/search.json，把第三行换成这两行代码：\n1{{- $pages := where .Site.RegularPages \u0026#34;Type\u0026#34; \u0026#34;in\u0026#34; .Site.Params.mainSections -}} 2{{- $notHidden := where .Site.RegularPages \u0026#34;Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} 3{{- $filtered := ($pages | intersect $notHidden) -}} 4{{- $searchable := where .Site.RegularPages \u0026#34;Params.nosearch\u0026#34; \u0026#34;!=\u0026#34; true -}} 5{{- $filtered := ($pages | intersect $notHidden | intersect $searchable) -}} 然后在你想要不被搜索到的文章头部加上这个参数就可以啦。\n1nosearch: true 添加标签墙 参考Hugo stack 主题美化 - 修改标签页页面 | Laphel，新建 layouts/tags/terms.html 填入：\n1{{ define \u0026#34;main\u0026#34; }} 2 \u0026lt;h1 class=\u0026#34;tags-title\u0026#34;\u0026gt;📖 Tags\u0026lt;/h1\u0026gt; 3 \u0026lt;p class=\u0026#34;tags-subtitle\u0026#34;\u0026gt;{{ len .Data.Terms }} Tags To Explore\u0026lt;/p\u0026gt; 4 \u0026lt;div class=\u0026#34;tag-cloud\u0026#34;\u0026gt; 5 {{ range shuffle .Data.Terms.Alphabetical }} 6 \u0026lt;a href=\u0026#34;{{ .Page.RelPermalink }}\u0026#34; class=\u0026#34;tag\u0026#34; style=\u0026#34;font-size: {{ add 12 (mul .Count 2) }}px;\u0026#34;\u0026gt; 7 {{ .Page.Title }} ({{ .Count }}) 8 \u0026lt;/a\u0026gt; 9 {{ end }} 10 \u0026lt;/div\u0026gt; 11{{ end }} assets/scss/custom.scss 添加样式：\n1// Tags 页面样式 2.tags-title { 3 text-align: center; 4 font-size: 2em; 5 color: #b213b5; 6} 7.tags-subtitle { 8 text-align: center; 9 margin-top: -30px; 10 font-weight: bold; 11 color: #336D82; 12} 13.tag-cloud { 14 text-align: center; 15 display: flex; 16 flex-wrap: wrap; 17 justify-content: center; 18 align-items: center; /* 上下居中 */ 19 min-height: 50vh; /* 可以根据需要调整高度 */ 20 padding: 20px 0; 21} 22 23.tag { 24 display: inline-block; 25 margin: 8px; 26 text-decoration: none; 27 padding: 5px 10px 5px 16px; /* 左侧留出更多空间给标签形状 */ 28 border-radius: 5px; 29 transition: all 0.3s ease-in-out; 30 background-color: #693382; 31 color: #F5ECE0; 32 position: relative; /* 为伪元素定位 */ 33 border-left: none; /* 移除左边框 */ 34} 35 36/* 创建标签上的小圆点 */ 37.tag:after { 38 content: \u0026#34;\u0026#34;; 39 position: absolute; 40 top: 50%; 41 left: 6px; 42 width: 4px; 43 height: 4px; 44 border-radius: 80%; 45 background-color: #EFEFEF; 46 transform: translateY(-50%); 47} 48 49.tag:hover { 50 color: #fff; 51 background-color: #3498db; 52 transform: scale(1.1); /* 悬停时稍微放大 */ 53} 54 55/* 悬停时也改变标签形状的颜色 */ 56.tag:hover:before { 57 border-color: transparent #3498db transparent transparent; 58} 时光胶囊 参考Hugo：增加年月日时间进度条，我稍加改动改成了短代码。新建 layout/shortcodes/timing.html：\n1{{/* Responsive Progress Bars Shortcode */}} 2{{ $barColor := .Get \u0026#34;barColor\u0026#34; | default \u0026#34;#9188f199\u0026#34; }} 3{{ $bgColor := .Get \u0026#34;bgColor\u0026#34; | default \u0026#34;#f8f8f883\u0026#34; }} 4{{ $maxWidth := .Get \u0026#34;maxWidth\u0026#34; | default \u0026#34;100%\u0026#34; }} 5 6\u0026lt;div class=\u0026#34;progress-container\u0026#34;\u0026gt; 7 \u0026lt;div class=\u0026#34;progress-item\u0026#34;\u0026gt; 8 \u0026lt;div class=\u0026#34;label-container\u0026#34;\u0026gt; 9 \u0026lt;span class=\u0026#34;progress-label\u0026#34;\u0026gt;Day\u0026lt;/span\u0026gt; 10 \u0026lt;/div\u0026gt; 11 \u0026lt;div class=\u0026#34;progress-wrapper\u0026#34;\u0026gt; 12 \u0026lt;div class=\u0026#34;progress\u0026#34;\u0026gt; 13 \u0026lt;div class=\u0026#34;progress-bar\u0026#34; id=\u0026#34;todayProgress\u0026#34; role=\u0026#34;progressbar\u0026#34; style=\u0026#34;width: 0%;\u0026#34; aria-valuenow=\u0026#34;0\u0026#34; aria-valuemin=\u0026#34;0\u0026#34; aria-valuemax=\u0026#34;100\u0026#34;\u0026gt;0%\u0026lt;/div\u0026gt; 14 \u0026lt;/div\u0026gt; 15 \u0026lt;span class=\u0026#34;remaining-time\u0026#34; id=\u0026#34;dayRemaining\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 16 \u0026lt;/div\u0026gt; 17 \u0026lt;/div\u0026gt; 18 \u0026lt;div class=\u0026#34;progress-item\u0026#34;\u0026gt; 19 \u0026lt;div class=\u0026#34;label-container\u0026#34;\u0026gt; 20 \u0026lt;span class=\u0026#34;progress-label\u0026#34;\u0026gt;Month\u0026lt;/span\u0026gt; 21 \u0026lt;/div\u0026gt; 22 \u0026lt;div class=\u0026#34;progress-wrapper\u0026#34;\u0026gt; 23 \u0026lt;div class=\u0026#34;progress\u0026#34;\u0026gt; 24 \u0026lt;div class=\u0026#34;progress-bar\u0026#34; id=\u0026#34;monthProgress\u0026#34; role=\u0026#34;progressbar\u0026#34; style=\u0026#34;width: 0%;\u0026#34; aria-valuenow=\u0026#34;0\u0026#34; aria-valuemin=\u0026#34;0\u0026#34; aria-valuemax=\u0026#34;100\u0026#34;\u0026gt;0%\u0026lt;/div\u0026gt; 25 \u0026lt;/div\u0026gt; 26 \u0026lt;span class=\u0026#34;remaining-time\u0026#34; id=\u0026#34;monthRemaining\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 27 \u0026lt;/div\u0026gt; 28 \u0026lt;/div\u0026gt; 29 \u0026lt;div class=\u0026#34;progress-item\u0026#34;\u0026gt; 30 \u0026lt;div class=\u0026#34;label-container\u0026#34;\u0026gt; 31 \u0026lt;span class=\u0026#34;progress-label\u0026#34;\u0026gt;Year\u0026lt;/span\u0026gt; 32 \u0026lt;/div\u0026gt; 33 \u0026lt;div class=\u0026#34;progress-wrapper\u0026#34;\u0026gt; 34 \u0026lt;div class=\u0026#34;progress\u0026#34;\u0026gt; 35 \u0026lt;div class=\u0026#34;progress-bar\u0026#34; id=\u0026#34;yearProgress\u0026#34; role=\u0026#34;progressbar\u0026#34; style=\u0026#34;width: 0%;\u0026#34; aria-valuenow=\u0026#34;0\u0026#34; aria-valuemin=\u0026#34;0\u0026#34; aria-valuemax=\u0026#34;100\u0026#34;\u0026gt;0%\u0026lt;/div\u0026gt; 36 \u0026lt;/div\u0026gt; 37 \u0026lt;span class=\u0026#34;remaining-time\u0026#34; id=\u0026#34;yearRemaining\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 38 \u0026lt;/div\u0026gt; 39 \u0026lt;/div\u0026gt; 40\u0026lt;/div\u0026gt; 41 42\u0026lt;style\u0026gt; 43.progress-container { 44 width: 100%; 45 max-width: {{ $maxWidth }}; 46 margin: 0 auto; 47} 48.progress-container .progress-item { 49 margin-bottom: 10px; 50 display: flex; 51 width: 100%; 52} 53.progress-container .label-container { 54 min-width: 70px; 55 flex-shrink: 0; 56 text-align: right; 57} 58.progress-container .progress-wrapper { 59 flex-grow: 1; 60 display: flex; 61 align-items: center; 62} 63.progress-container .progress-label { 64 margin-right: 10px; 65} 66.progress-container .progress { 67 height: 20px; 68 flex-grow: 1; 69 margin-bottom: 0px; 70 overflow: hidden; 71 background-color: {{ $bgColor }}; 72 border-radius: 4px; 73 -webkit-box-shadow: inset 0 1px 2px rgb(0 0 0 / 10%); 74 box-shadow: inset 0 1px 2px rgb(0 0 0 / 10%); 75} 76.progress-container .progress-bar { 77 float: left; 78 width: 0; 79 height: 100%; 80 font-size: 12px; 81 line-height: 20px; 82 color: #fff; 83 text-align: center; 84 background-color: {{ $barColor }}; 85 -webkit-box-shadow: inset 0 -1px 0 rgb(0 0 0 / 15%); 86 box-shadow: inset 0 -1px 0 rgb(0 0 0 / 15%); 87 -webkit-transition: width .6s ease; 88 -o-transition: width .6s ease; 89 transition: width .6s ease; 90} 91.progress-container .remaining-time { 92 margin-left: 10px; 93 margin-right: 10px; 94 font-size: 12px; 95 white-space: nowrap; 96 width: 80px; 97 text-align: left; 98 font-size: 90%; 99} 100 101\u0026lt;/style\u0026gt; 102 103{{ $js := ` 104function updateProgress(progressId, newValue, remainingId, remainingValue, unit) { 105 document.getElementById(progressId).style.width = newValue + \u0026#39;%\u0026#39;; 106 document.getElementById(progressId).setAttribute(\u0026#39;aria-valuenow\u0026#39;, newValue); 107 document.getElementById(progressId).textContent = newValue + \u0026#39;%\u0026#39;; 108 document.getElementById(remainingId).textContent = \u0026#39;剩余\u0026#39; + remainingValue + unit; 109} 110 111document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { 112 const now = new Date(); 113 114 // Calculate elapsed time for the day 115 const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 116 const elapsedTime = now - startOfDay; 117 const dayPercentage = Math.round((elapsedTime / (1000 * 60 * 60 * 24)) * 100); 118 const remainingHours = Math.round((24 - now.getHours() - now.getMinutes()/60) * 10) / 10; 119 120 // Calculate elapsed days for the month 121 const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); 122 const elapsedDays = (now - startOfMonth) / (1000 * 60 * 60 * 24); 123 const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate(); 124 const monthPercentage = Math.round((elapsedDays / daysInMonth) * 100); 125 const remainingDays = Math.ceil(daysInMonth - elapsedDays); 126 127 // Calculate elapsed days for the year 128 const startOfYear = new Date(now.getFullYear(), 0, 1); 129 const elapsedDaysInYear = (now - startOfYear) / (1000 * 60 * 60 * 24); 130 const daysInYear = (new Date(now.getFullYear() + 1, 0, 1) - new Date(now.getFullYear(), 0, 1)) / (1000 * 60 * 60 * 24); 131 const yearPercentage = Math.round((elapsedDaysInYear / daysInYear) * 100); 132 const remainingMonths = 12 - now.getMonth() - (now.getDate()/daysInMonth); 133 const remainingMonthsRounded = Math.ceil(remainingMonths); 134 135 updateProgress(\u0026#39;todayProgress\u0026#39;, dayPercentage, \u0026#39;dayRemaining\u0026#39;, remainingHours, \u0026#39;小时\u0026#39;); 136 updateProgress(\u0026#39;monthProgress\u0026#39;, monthPercentage, \u0026#39;monthRemaining\u0026#39;, remainingDays, \u0026#39;天\u0026#39;); 137 updateProgress(\u0026#39;yearProgress\u0026#39;, yearPercentage, \u0026#39;yearRemaining\u0026#39;, remainingMonthsRounded, \u0026#39;月\u0026#39;); 138}); 139` }} 140 141\u0026lt;script\u0026gt;{{ $js | safeJS }}\u0026lt;/script\u0026gt; 用 {{\u0026lt; timing \u0026gt;}} 就可以咯 ~\n左侧导航栏缩小间距 原先的导航栏样式有点宽，换语言和切换深色模式还要往下滑。于是想着缩小一下这个间距。\n参考链接，找到 /assets/scss/partials/menu.scss 中的这段代码，把 gap: 25px; 改成 gap: 15px; 看起来就饱满了。\n1#main-menu { 2 \u0026amp;, .menu-bottom-section ol { 3 flex-direction: column; 4 gap: 30px; 5 6 @include respond(xl) { 7 gap: 25px; 8 gap: 15px; 9 } 10 } 11} 提高自定义字体加载速度 偶然看到这篇文章：利用 unicode-range 属性, 提高自定义字体加载速度\n立马在 assets/scss/custom.scss 加上：\n1@font-face { 2 unicode-range: U+4E00-9FFF,U+0025-00F0,U+3040-30FF,U+1100-11FF,U+3130-318F,U+AC00-D7FF; 3 // U+4E00-9FFF CJK中日韩统一表意文字，包括简繁中文 4 // U+AC00-D7FF 韩语 5 // U+3040-30FF 日语 6 // U+0025-00F0 数字、字母、拉丁语等 7 // U+1100-11FF,U+3130-318F 韩文辅音 8} 好像有点作用 （也可能是心理安慰？\n短代码合集 有时候想插入短代码还要翻以前的文章，有点烦人。忘记从哪里看到有人弄了个短代码的总结页，但是也没发现有人做过这个的教程，只能自己用AI手搓一个。\n成品：短代码合集\n新建一个静态页面 shortcodes.md： 1--- 2title: \u0026#34;短代码合集\u0026#34; 3date: 2025-04-13 4layout: \u0026#34;shortcodes\u0026#34; 5slug: shortcodes 6description: \u0026#34;Hugo 博客短代码集合，点击即可复制\u0026#34; 7--- 在 layouts/_default/ 目录下新建 shortcodes.html： 1{{ define \u0026#34;main\u0026#34; }} 2\u0026lt;div class=\u0026#34;shortcodes-container\u0026#34;\u0026gt; 3 \u0026lt;h1\u0026gt;Hugo Shortcodes 速查表\u0026lt;/h1\u0026gt; 4 \u0026lt;div class=\u0026#34;shortcodes-intro\u0026#34;\u0026gt; 5 \u0026lt;p\u0026gt;这些是Hugo中常用的Shortcodes列表，点击即可复制使用。\u0026lt;/p\u0026gt; 6 \u0026lt;/div\u0026gt; 7 \u0026lt;div class=\u0026#34;category-container\u0026#34;\u0026gt; 8 \u0026lt;div class=\u0026#34;category-nav\u0026#34;\u0026gt; 9 \u0026lt;button class=\u0026#34;category-btn active\u0026#34; data-category=\u0026#34;all\u0026#34;\u0026gt;全部\u0026lt;/button\u0026gt; 10 \u0026lt;button class=\u0026#34;category-btn\u0026#34; data-category=\u0026#34;text\u0026#34;\u0026gt;文本格式\u0026lt;/button\u0026gt; 11 \u0026lt;button class=\u0026#34;category-btn\u0026#34; data-category=\u0026#34;layout\u0026#34;\u0026gt;布局排版\u0026lt;/button\u0026gt; 12 \u0026lt;button class=\u0026#34;category-btn\u0026#34; data-category=\u0026#34;effect\u0026#34;\u0026gt;动态效果\u0026lt;/button\u0026gt; 13 \u0026lt;button class=\u0026#34;category-btn\u0026#34; data-category=\u0026#34;other\u0026#34;\u0026gt;其他功能\u0026lt;/button\u0026gt; 14 \u0026lt;/div\u0026gt; 15 \u0026lt;/div\u0026gt; 16 \u0026lt;div id=\u0026#34;shortcodesGrid\u0026#34; class=\u0026#34;shortcodes-grid\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 17 \u0026lt;div id=\u0026#34;copyNotification\u0026#34; class=\u0026#34;copy-notification\u0026#34;\u0026gt;已复制到剪贴板!\u0026lt;/div\u0026gt; 18\u0026lt;/div\u0026gt; 19 20\u0026lt;script\u0026gt; 21document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { 22 const shortcodes = [ 23 // 文本格式 24 { name: \u0026#34;文本黑幕\u0026#34;, code: \u0026#34;{{ \u0026#34;\u0026lt;span class=\\\u0026#34;shady\\\u0026#34;\u0026gt;数据删除！\u0026lt;/span\u0026gt;\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;text\u0026#34; }, 25 { name: \u0026#34;键盘样式\u0026#34;, code: \u0026#34;{{ \u0026#34;\u0026lt;kbd\u0026gt;Ctrl\u0026lt;/kbd\u0026gt;\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;text\u0026#34; }, 26 27 // 布局排版 28 { name: \u0026#34;文字居左\u0026#34;, code: \u0026#34;{{ \u0026#34;{{\u0026lt; align left \\\u0026#34;文字居左\\\u0026#34; \u0026gt;}}\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;layout\u0026#34; }, 29 { name: \u0026#34;文字居中\u0026#34;, code: \u0026#34;{{ \u0026#34;{{\u0026lt; align center \\\u0026#34;文字居中\\\u0026#34; \u0026gt;}}\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;layout\u0026#34; }, 30 { name: \u0026#34;文字居右\u0026#34;, code: \u0026#34;{{ \u0026#34;{{\u0026lt; align right \\\u0026#34;文字居右\\\u0026#34; \u0026gt;}}\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;layout\u0026#34; }, 31 32 // 动态效果 33 { name: \u0026#34;基本摇晃效果\u0026#34;, code: \u0026#34;{{ \u0026#34;{{\u0026lt; shake effect=\\\u0026#34;shake\\\u0026#34; \u0026gt;}}这是基本的摇晃效果。{{\u0026lt; /shake \u0026gt;}}\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;effect\u0026#34; }, 34 { name: \u0026#34;剧烈摇晃效果\u0026#34;, code: \u0026#34;{{ \u0026#34;{{\u0026lt; shake effect=\\\u0026#34;shake-hard\\\u0026#34; \u0026gt;}}这个段落有剧烈摇晃效果。{{\u0026lt; /shake \u0026gt;}}\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;effect\u0026#34; }, 35 { name: \u0026#34;慢速摇晃效果\u0026#34;, code: \u0026#34;{{ \u0026#34;{{\u0026lt; shake effect=\\\u0026#34;shake-slow\\\u0026#34; \u0026gt;}}这个段落有慢速摇晃效果。{{\u0026lt; /shake \u0026gt;}}\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;effect\u0026#34; }, 36 37 // 其他功能可以在这里添加 38 { name: \u0026#34;仿Github样式\u0026#34;, code: \u0026#34;{{ \u0026#34;{{\u0026lt; github\\n name=\\\u0026#34;链接标题\\\u0026#34;\\n link=\\\u0026#34;#\\\u0026#34;\\n description=\\\u0026#34;内容描述\\\u0026#34;\\n color=\\\u0026#34;#F48201\\\u0026#34;\\n language=\\\u0026#34;Go\\\u0026#34;\\n\u0026gt;}}\u0026#34; | safeHTML }}\u0026#34;, category: \u0026#34;other\u0026#34; }, 39 ]; 40 41 const grid = document.getElementById(\u0026#39;shortcodesGrid\u0026#39;); 42 const notification = document.getElementById(\u0026#39;copyNotification\u0026#39;); 43 const categoryButtons = document.querySelectorAll(\u0026#39;.category-btn\u0026#39;); 44 45 // 计算各分类的短代码数量 46 const categoryCounts = { 47 all: shortcodes.length 48 }; 49 50 shortcodes.forEach(item =\u0026gt; { 51 if (!categoryCounts[item.category]) { 52 categoryCounts[item.category] = 0; 53 } 54 categoryCounts[item.category]++; 55 }); 56 57 // 更新分类按钮上显示的数量 58 categoryButtons.forEach(button =\u0026gt; { 59 const category = button.dataset.category; 60 const count = categoryCounts[category] || 0; 61 button.textContent = `${button.textContent} (${count})`; 62 }); 63 64 // 创建短代码块的函数 65 function createShortcodeBlocks(category = \u0026#39;all\u0026#39;) { 66 // 清空网格 67 grid.innerHTML = \u0026#39;\u0026#39;; 68 69 // 过滤短代码 70 const filteredShortcodes = category === \u0026#39;all\u0026#39; 71 ? shortcodes 72 : shortcodes.filter(item =\u0026gt; item.category === category); 73 74 // 创建短代码块 75 filteredShortcodes.forEach(item =\u0026gt; { 76 const block = document.createElement(\u0026#39;div\u0026#39;); 77 block.className = \u0026#39;shortcode-block\u0026#39;; 78 block.dataset.category = item.category; 79 80 const name = document.createElement(\u0026#39;h3\u0026#39;); 81 name.textContent = item.name; 82 83 const code = document.createElement(\u0026#39;pre\u0026#39;); 84 code.textContent = item.code; 85 86 block.appendChild(name); 87 block.appendChild(code); 88 89 // 添加点击复制功能 90 block.addEventListener(\u0026#39;click\u0026#39;, function() { 91 navigator.clipboard.writeText(item.code).then(() =\u0026gt; { 92 // 显示复制成功通知 93 notification.classList.add(\u0026#39;active\u0026#39;); 94 setTimeout(() =\u0026gt; { 95 notification.classList.remove(\u0026#39;active\u0026#39;); 96 }, 2000); 97 }); 98 }); 99 100 grid.appendChild(block); 101 }); 102 } 103 104 // 初始加载全部短代码 105 createShortcodeBlocks(); 106 107 // 添加分类按钮事件监听 108 categoryButtons.forEach(button =\u0026gt; { 109 button.addEventListener(\u0026#39;click\u0026#39;, function() { 110 // 移除所有按钮的active类 111 categoryButtons.forEach(btn =\u0026gt; btn.classList.remove(\u0026#39;active\u0026#39;)); 112 // 给当前点击的按钮添加active类 113 this.classList.add(\u0026#39;active\u0026#39;); 114 115 // 根据选中的分类显示短代码 116 const category = this.dataset.category; 117 createShortcodeBlocks(category); 118 }); 119 }); 120}); 121\u0026lt;/script\u0026gt; 122 123\u0026lt;style\u0026gt; 124.shortcodes-container { 125 max-width: 14400px; 126 padding: 10px; 127 margin-top: -3%; 128 transition: color 0.15s ease; 129 color: var(--body-text-color); 130} 131 132.shortcodes-intro { 133 margin-bottom: 30px; 134} 135 136.category-container { 137 margin-bottom: 20px; 138} 139 140.category-nav { 141 display: flex; 142 gap: 10px; 143 margin-bottom: 20px; 144 flex-wrap: wrap; 145} 146 147.category-btn { 148 padding: 8px 16px; 149 border: none; 150 border-radius: 4px; 151 background-color: #f0f0f0; 152 cursor: pointer; 153 transition: all 0.3s ease; 154 font-weight: 500; 155} 156 157.category-btn:hover { 158 background-color: #e0e0e0; 159} 160 161.category-btn.active { 162 background-color: #410445; 163 color: white; 164} 165 166.shortcodes-grid { 167 display: grid; 168 grid-template-columns: repeat(4, 1fr); 169 gap: 15px; 170} 171 172.shortcode-block { 173 background-color: #f9f8fa; 174 border: 1px solid #dee2e6; 175 border-radius: 6px; 176 padding: 15px; 177 cursor: pointer; 178 transition: all 0.3s ease; 179} 180 181.shortcode-block:hover { 182 box-shadow: 0 5px 8px rgba(201, 10, 188, 0.8); 183 transform: translateY(-2px); 184} 185 186.shortcode-block h3 { 187 margin-top: 0; 188 margin-bottom: 10px; 189 color: #333; 190} 191 192.shortcode-block pre { 193 margin: 0; 194 background-color: #ffffff; 195 padding: 5px; 196 border-radius: 4px; 197 white-space: pre-wrap; 198 word-break: break-all; 199 font-size: 14px; 200} 201 202[data-scheme=\u0026#34;dark\u0026#34;] { 203 .shortcode-block { 204 background-color: #410445; 205 border: 1px solid #5f0865; 206 } 207 .shortcode-block pre { 208 background-color: #222222; 209 } 210 .shortcode-block h3{ 211 color: #FFF8F8; 212 } 213 .shortcodes-container { 214 color: #FFF8F8; 215 } 216 .category-btn { 217 background-color: #333; 218 color: #ddd; 219 } 220 .category-btn:hover { 221 background-color: #444; 222 } 223 .category-btn.active { 224 background-color: #c90abc; 225 color: white; 226 } 227} 228 229.copy-notification { 230 position: fixed; 231 bottom: 20px; 232 left: 50%; 233 transform: translateX(-50%) translateY(100px); 234 background-color: rgba(0,0,0,0.8); 235 color: white; 236 padding: 10px 20px; 237 border-radius: 4px; 238 opacity: 0; 239 transition: all 0.3s ease; 240} 241 242.copy-notification.active { 243 transform: translateX(-50%) translateY(0); 244 opacity: 1; 245} 246 247@media (max-width: 1200px) { 248 .shortcodes-grid { 249 grid-template-columns: repeat(3, 1fr); 250 } 251} 252 253@media (max-width: 900px) { 254 .shortcodes-grid { 255 grid-template-columns: repeat(2, 1fr); 256 } 257} 258 259@media (max-width: 600px) { 260 .shortcodes-grid { 261 grid-template-columns: 1fr; 262 } 263 264 .category-nav { 265 justify-content: center; 266 } 267} 268\u0026lt;/style\u0026gt; 269{{ end }} 注意：要用 Go 模板中形如 {{ \u0026quot;\u0026lt;span class=\\\u0026quot;blur\\\u0026quot;\u0026gt;手动打码效果\u0026lt;/span\u0026gt;\u0026quot; | safeHTML }} 的安全函数样式才能避免短代码被渲染出来；有双引号配置的参数要转义内部引号（在 \u0026quot; 前面打上 \\）；要换行的直接在对应位置加上 \\n。\n首页欢迎栏 刚好我的短代码合集不知道放哪里好，又不想放在侧边栏，干脆就放在首页这个欢迎栏上好了。参考Hugo Stack 主题美化 - 阿琦同学，在 layouts/index.html 的 {{ $pag := .Paginate ($filtered) }} 后面加入：\n1\u0026lt;!-- 首页欢迎字幅板块 --\u0026gt; 2\u0026lt;div class=\u0026#34;welcome\u0026#34;\u0026gt; 3 \u0026lt;p style=\u0026#34;font-size: 2rem; text-align: center; font-weight: bold\u0026#34;\u0026gt; 4 \u0026lt;a href=\u0026#34;/shortcodes\u0026#34; title=\u0026#34;Link To Hugo Shortcodes Space\u0026#34;\u0026gt; 5 \u0026lt;span class=\u0026#34;shake\u0026#34;\u0026gt;👋\u0026lt;/span\u0026gt; 6 \u0026lt;span class=\u0026#34;jump-text1\u0026#34; \u0026gt; Welcome\u0026lt;/span\u0026gt; 7 \u0026lt;span class=\u0026#34;jump-text2\u0026#34;\u0026gt; To \u0026lt;/span\u0026gt; 8 \u0026lt;span class=\u0026#34;jump-text3\u0026#34; style=\u0026#34;color:#e99312\u0026#34;\u0026gt;if\u0026lt;/span\u0026gt;\u0026lt;span class=\u0026#34;jump-text4\u0026#34; style=\u0026#34;color:#e99312\u0026#34;\u0026gt;a\u0026lt;/span 9 \u0026gt;\u0026lt;span class=\u0026#34;jump-text5\u0026#34; style=\u0026#34;color:#e99312\u0026#34;\u0026gt;n\u0026lt;/span\u0026gt;\u0026lt;span class=\u0026#34;jump-text6\u0026#34; style=\u0026#34;color:#e99312\u0026#34;\u0026gt;t\u0026lt;/span 10 \u0026gt;\u0026lt;span class=\u0026#34;jump-text7\u0026#34; style=\u0026#34;color:#e99312\u0026#34;\u0026gt;ic\u0026lt;/span\u0026gt; 11 \u0026lt;span class=\u0026#34;jump-text8\u0026#34; style=\u0026#34;color:#e99312\u0026#34;\u0026gt;\u0026#39;s\u0026lt;/span\u0026gt; 12 \u0026lt;span class=\u0026#34;jump-text9\u0026#34; style=\u0026#34;color:#e99312\u0026#34;\u0026gt;Blog\u0026lt;/span\u0026gt; 13 \u0026lt;/a\u0026gt; 14 \u0026lt;/p\u0026gt; 15\u0026lt;/div\u0026gt; 16\u0026lt;!-- ------首页欢迎字幅板块------ --\u0026gt; 在 assets/scss/partials/sidebar.scss 加入：\n1//首页欢迎板块样式 2.welcome { 3 color: var(--card-text-color-main); 4 background: var(--card-background); 5 box-shadow: var(--shadow-l2); 6 border-radius: 30px; 7 display: inline-block; 8} 9 10// 👋emoji实现摆动效果 11.shake { 12 display: inline-block; 13 animation: shake 1s; 14 animation-duration: 1s; 15 animation-timing-function: ease; 16 animation-delay: 0s; 17 animation-iteration-count: 1; 18 animation-direction: normal; 19 animation-fill-mode: none; 20 animation-play-state: running; 21 animation-name: shake; 22 animation-timeline: auto; 23 animation-range-start: normal; 24 animation-range-end: normal; 25 animation-delay: 2s; 26 @keyframes shake { 27 0% { 28 transform: rotate(0); 29 } 30 25% { 31 transform: rotate(45deg) scale(1.2); 32 } 33 50% { 34 transform: rotate(0) scale(1.2); 35 } 36 75% { 37 transform: rotate(45deg) scale(1.2); 38 } 39 100% { 40 transform: rotate(0); 41 } 42 } 43} 44// 实现字符跳动动画 45.jump-text1 { 46 display: inline-block; 47 animation: jump 0.5s 1; 48} 49 50.jump-text2 { 51 display: inline-block; 52 animation: jump 0.5s 1; 53 animation-delay: 0.1s; 54} 55 56.jump-text3 { 57 display: inline-block; 58 animation: jump 0.5s 1; 59 animation-delay: 0.2s; 60} 61 62.jump-text4 { 63 display: inline-block; 64 animation: jump 0.5s 1; 65 animation-delay: 0.3s; 66} 67 68.jump-text5 { 69 display: inline-block; 70 animation: jump 0.5s 1; 71 animation-delay: 0.4s; 72} 73 74.jump-text6 { 75 display: inline-block; 76 animation: jump 0.5s 1; 77 animation-delay: 0.5s; 78} 79 80.jump-text7 { 81 display: inline-block; 82 animation: jump 0.5s 1; 83 animation-delay: 0.6s; 84} 85 86.jump-text8 { 87 display: inline-block; 88 animation: jump 0.5s 1; 89 animation-delay: 0.7s; 90} 91 92.jump-text9 { 93 display: inline-block; 94 animation: jump 0.5s 1; 95 animation-delay: 0.9s; 96} 97 98@keyframes jump { 99 0% { 100 transform: translateY(0); 101 } 102 50% { 103 transform: translateY(-20px); 104 } 105 100% { 106 transform: translateY(0); 107 } 108} 分类和标签页不分页 不修改之前分类页和标签页都会沿用 hugo.yaml 的全局分页配置 pagination: pagerSize: 3，有点难受。把 layouts/_default/list.html 里的所有内容删掉，换成：\n1{{ define \u0026#34;main\u0026#34; }} 2 \u0026lt;header\u0026gt; 3 \u0026lt;h3 class=\u0026#34;section-title\u0026#34;\u0026gt; 4 {{ if eq .Parent (.GetPage \u0026#34;/\u0026#34;) }} 5 {{ T \u0026#34;list.section\u0026#34; }} 6 {{ else }} 7 {{ .Parent.Title }} 8 {{ end }} 9 \u0026lt;/h3\u0026gt; 10 \u0026lt;div class=\u0026#34;section-card\u0026#34;\u0026gt; 11 \u0026lt;div class=\u0026#34;section-details\u0026#34;\u0026gt; 12 \u0026lt;h3 class=\u0026#34;section-count\u0026#34;\u0026gt;{{ T \u0026#34;list.page\u0026#34; (len .Pages) }}\u0026lt;/h3\u0026gt; 13 \u0026lt;h1 class=\u0026#34;section-term\u0026#34;\u0026gt;{{ .Title }}\u0026lt;/h1\u0026gt; 14 {{ with .Params.description }} 15 \u0026lt;h2 class=\u0026#34;section-description\u0026#34;\u0026gt;{{ . }}\u0026lt;/h2\u0026gt; 16 {{ end }} 17 \u0026lt;/div\u0026gt; 18 19 {{- $image := partialCached \u0026#34;helper/image\u0026#34; (dict \u0026#34;Context\u0026#34; . \u0026#34;Type\u0026#34; \u0026#34;section\u0026#34;) .RelPermalink \u0026#34;section\u0026#34; -}} 20 {{ if $image.exists }} 21 \u0026lt;div class=\u0026#34;section-image\u0026#34;\u0026gt; 22 {{ if $image.resource }} 23 {{- $Permalink := $image.resource.RelPermalink -}} 24 {{- $Width := $image.resource.Width -}} 25 {{- $Height := $image.resource.Height -}} 26 27 {{- if (default true .Page.Site.Params.imageProcessing.cover.enabled) -}} 28 {{- $thumbnail := $image.resource.Fill \u0026#34;120x120\u0026#34; -}} 29 {{- $Permalink = $thumbnail.RelPermalink -}} 30 {{- $Width = $thumbnail.Width -}} 31 {{- $Height = $thumbnail.Height -}} 32 {{- end -}} 33 34 \u0026lt;img src=\u0026#34;{{ $Permalink }}\u0026#34; 35 width=\u0026#34;{{ $Width }}\u0026#34; 36 height=\u0026#34;{{ $Height }}\u0026#34; 37 loading=\u0026#34;lazy\u0026#34;\u0026gt; 38 {{ else }} 39 \u0026lt;img src=\u0026#34;{{ $image.permalink }}\u0026#34; loading=\u0026#34;lazy\u0026#34; /\u0026gt; 40 {{ end }} 41 \u0026lt;/div\u0026gt; 42 {{ end }} 43 \u0026lt;/div\u0026gt; 44 \u0026lt;/header\u0026gt; 45 {{- $subsections := .Sections -}} 46 {{- $pages := .Pages | complement $subsections -}} 47 48 {{- if eq (len $pages) 0 -}} 49 {{/* If there are no normal pages, display subsections in list style, with pagination */}} 50 {{/* This happens with taxonomies like categories or tags */}} 51 {{- $pages = $subsections -}} 52 {{- $subsections = slice -}} 53 {{- end -}} 54 {{- with $subsections -}} 55 \u0026lt;aside\u0026gt; 56 \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;{{ T \u0026#34;list.subsection\u0026#34; (len $subsections) }}\u0026lt;/h2\u0026gt; 57 \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; 58 \u0026lt;div class=\u0026#34;article-list--tile\u0026#34;\u0026gt; 59 {{ range . }} 60 {{ partial \u0026#34;article-list/tile\u0026#34; (dict \u0026#34;context\u0026#34; . \u0026#34;size\u0026#34; \u0026#34;250x150\u0026#34; \u0026#34;Type\u0026#34; \u0026#34;section\u0026#34;) }} 61 {{ end }} 62 \u0026lt;/div\u0026gt; 63 \u0026lt;/div\u0026gt; 64 \u0026lt;/aside\u0026gt; 65 {{- end -}} 66 67 {{/* 检查是否是分类页面或标签页面 */}} 68 {{ $isCategoryPage := findRE \u0026#34;^/categories(/|/.+)\u0026#34; .RelPermalink }} 69 {{ $isTagPage := findRE \u0026#34;^/tags(/|/.+)\u0026#34; .RelPermalink }} 70 {{ if or $isCategoryPage $isTagPage }} 71 {{/* 分类和标签页面显示所有文章，不分页 */}} 72 \u0026lt;section class=\u0026#34;article-list--compact\u0026#34;\u0026gt; 73 {{ range $pages.ByDate.Reverse }} 74 {{ partial \u0026#34;article-list/compact\u0026#34; . }} 75 {{ end }} 76 \u0026lt;/section\u0026gt; 77 {{ else }} 78 {{/* 其他页面使用常规分页 */}} 79 {{ $paginator := .Paginate $pages }} 80 \u0026lt;section class=\u0026#34;article-list--compact\u0026#34;\u0026gt; 81 {{ range $paginator.Pages }} 82 {{ partial \u0026#34;article-list/compact\u0026#34; . }} 83 {{ end }} 84 \u0026lt;/section\u0026gt; 85 {{- partial \u0026#34;pagination.html\u0026#34; . -}} 86 {{ end }} 87 88 {{ partialCached \u0026#34;footer/footer\u0026#34; . }} 89{{ end }} 90{{ define \u0026#34;right-sidebar\u0026#34; }} 91 {{ partial \u0026#34;sidebar/right.html\u0026#34; (dict \u0026#34;Context\u0026#34; . \u0026#34;Scope\u0026#34; \u0026#34;homepage\u0026#34;) }} 92{{ end }} 看起来轻描淡写就改好了，实际上花了我老大劲才定位到问题，解决之后发现自己好蠢（）\n最后的碎碎念.. 还是看见别人好看的博客就手痒，于是又开始折腾起来。其实主要是移动端目录的问题困扰了我很久，改来改去都不尽人意，最后直接让 Claude 给我生成一版算了。接下来的日子估计会很忙，还是勉励下自己要抓紧学习呀。\n","date":"2025-03-30T15:12:25+08:00","image":"https://langminjeii.ifantic.de/post/meihua-2.png","permalink":"https://langminjeii.ifantic.de/post/hugo-stack%E3%81%AE%E7%BE%8E%E5%8C%96-ii/","title":"Hugo-stackの美化 II"},{"content":"指针 *T 是指向 T 类型值的指针，其零值为 nil。如：\nvar p *int \u0026amp; 操作符会生成一个指向其操作数的指针。如：\ni := 42 p = \u0026amp;i * 操作符表示指针指向的底层值：\nfmt.Println(*p) // 通过指针 p 读取 i *p = 21 // 通过指针 p 设置 i 这也就是通常所说的「解引用」或「间接引用」。\n注意：Go 没有指针运算。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 i, j := 42, 2701 7 8 p := \u0026amp;i // 指向 i 9 fmt.Println(*p) // 通过指针读取 i 的值 10 // output: 42 11 *p = 21 // 通过指针设置 i 的值 12 fmt.Println(i) // 查看 i 的值 13 // output: 21 14 p = \u0026amp;j // 指向 j 15 *p = *p / 37 // 通过指针对 j 进行除法运算 16 fmt.Println(j) // 查看 j 的值 17 // output: 73 18} 结构体（struct）就是一组 字段（field）\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Vertex struct { 6 X int 7 Y int 8} 9 10func main() { 11 fmt.Println(Vertex{5, 2}) // output: {5, 2} 12} 结构体字段可通过点号 . 来访问。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Vertex struct { 6 X int 7 Y int 8} 9 10func main() { 11 v := Vertex{1, 2} 12 v.X = 4 13 fmt.Println(v.X) // output: 4 14} 结构体字段可通过结构体指针来访问。如果我们有一个指向结构体的指针 p 那么可以通过 (*p).X 来访问其字段 X。 不过这么写太啰嗦了，所以语言也允许我们使用隐式解引用，直接写 p.X 就可以。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Vertex struct { 6 X int 7 Y int 8} 9 10func main() { 11 v := Vertex{1, 2} 12 p := \u0026amp;v 13 p.X = 1e9 14 fmt.Println(v) // output: {1000000000 2} 15} 使用 Name: 语法可以仅列出部分字段（字段名的顺序无关）；特殊的前缀 \u0026amp; 返回一个指向结构体的指针。以下面的代码为例，v2 = Vertex{X: 1} 里只指定了 X，那么 Y 就会置0。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Vertex struct { 6 X, Y int 7} 8 9var ( 10 v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体 11 v2 = Vertex{X: 1} // Y:0 被隐式地赋予零值 12 v3 = Vertex{} // X:0 Y:0 13 p = \u0026amp;Vertex{1, 2} // 创建一个 *Vertex 类型的结构体（指针） 14) 15 16func main() { 17 fmt.Println(v1, p, v2, v3) // output: {1 2} \u0026amp;{1 2} {1 0} {0 0} 18} 类型 [n]T 表示一个数组，它拥有 n 个类型为 T 的值。 var a [10]int 会将变量 a 声明为拥有 10 个整数的数组。数组的长度是其类型的一部分，因此数组不能改变大小。 不过没关系，Go 拥有更加方便的使用数组的方式。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 var a [2]string 7 a[0] = \u0026#34;Hello\u0026#34; 8 // a[1] = \u0026#34;World\u0026#34; 9 fmt.Println(a[0], a[1]) // output: Hello 10 fmt.Println(a) // output: [Hello ] 11 12 primes := [6]int{2, 3, 5, 7, 11, 13} 13 fmt.Println(primes) // output: [2 3 5 7 11 13] 14} 切片 每个数组的大小都是固定的。而切片则为数组元素提供了动态大小的、灵活的视角。 在实践中，切片比数组更常用。类型 []T 表示一个元素类型为 T 的切片。切片通过两个下标来界定，一个下界和一个上界，二者以冒号分隔： a[low : high]\n与python一样，它会选出一个半闭半开区间，包括第一个元素，但排除最后一个元素。以下表达式创建了一个切片，它包含 a 中下标从 1 到 3 的元素： a[1:4]\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 primes := [6]int{2, 3, 5, 7, 11, 13} 7 8 var s []int = primes[1:4] 9 fmt.Println(s) // output: [3 5 7] 10} 切片就像数组的引用，并不存储任何数据，它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素，和它共享底层数组的切片都会观测到这些修改。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 names := [4]string{ 7 \u0026#34;John\u0026#34;, 8 \u0026#34;Paul\u0026#34;, 9 \u0026#34;George\u0026#34;, 10 \u0026#34;Ringo\u0026#34;, 11 } 12 fmt.Println(names) // output: [John Paul George Ringo] 13 14 a := names[0:2] 15 b := names[1:3] 16 fmt.Println(a, b) // output: [John Paul] [Paul George] 17 18 b[0] = \u0026#34;XXX\u0026#34; 19 fmt.Println(a, b) // output: [John XXX] [XXX George] 20 fmt.Println(names) // output: [John XXX George Ringo] 21} 切片字面量：类似于没有长度的数组字面量。切片字面量的工作机制是：\n根据字面量中的元素数量，隐式创建一个对应的数组，数组的长度等于元素数量。 创建一个切片，该切片引用这个隐式创建的数组，切片的长度和容量都等于数组的长度。 这个切片变量可以像其他切片一样操作，比如追加元素、重新切片等，可能会触发底层数组的扩容或修改。 切片字面量的底层数组只能通过该切片来访问，没有其他变量引用这个数组，所以当切片本身被传递时，底层数组不会被复制，而是共享。这和其他切片操作的行为是一致的。这样可以让切片的初始化更加方便，不需要显式创建数组再转换，同时保持了底层数组的引用机制。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 q := []int{2, 3, 5, 7, 11, 13} 7 fmt.Println(q) // output: [2 3 5 7 11 13] 8 9 r := []bool{true, false, true, true, false, true} 10 fmt.Println(r) // output: [true false true true false true] 11 12 s := []struct { 13 i int 14 b bool 15 }{ 16 {2, true}, 17 {3, false}, 18 {5, true}, 19 {7, true}, 20 {11, false}, 21 {13, true}, 22 } 23 fmt.Println(s) // output: [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}] 24} 在进行切片时，你可以利用它的默认行为来忽略上下界。切片下界的默认值为 0，上界则是该切片的长度。对于数组 var a [10]int 来说，以下切片表达式和它是等价的：\na[0:10]、a[:10]、a[0:]、a[:]\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 s := []int{2, 3, 5, 7, 11, 13} 7 8 s = s[1:4] 9 fmt.Println(s) // output: [3 5 7] 10 11 s = s[:2] 12 fmt.Println(s) // output: [3 5] 13 14 s = s[1:] 15 fmt.Println(s) // output: [5] 16} 切片拥有 长度 和 容量：切片的长度就是它所包含的元素个数，切片的容量是从它的第一个元素开始数一直到其底层数组元素末尾的个数。切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。\n通过重新切片来扩展一个切片，给它提供足够的容量。 试着修改示例程序中的切片操作，向外扩展它的长度，看看会发生什么。（当然是会报错啦）\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 s := []int{2, 3, 5, 7, 11, 13} 7 printSlice(s) // output: len=6 cap=6 [2 3 5 7 11 13] 8 9 // 截取切片使其长度为 0 10 s = s[:0] 11 printSlice(s) // output: len=0 cap=6 [] 12 13 // 扩展其长度 14 s = s[:4] 15 printSlice(s) // output: len=4 cap=6 [2 3 5 7] 16 17 // 舍弃前两个值 18 s = s[2:] 19 printSlice(s) // output: len=2 cap=4 [5 7] 20} 21 22func printSlice(s []int) { 23 fmt.Printf(\u0026#34;len=%d cap=%d %v\\n\u0026#34;, len(s), cap(s), s) 24} 切片的零值是 nil，nil 切片的长度和容量为 0 且没有底层数组。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 var s []int 7 fmt.Println(s, len(s), cap(s)) // output: [] 0 0 8 if s == nil { 9 fmt.Println(\u0026#34;nil!\u0026#34;) // output: nil! 10 } 11} 切片可以用内置函数 make 来创建，这也是你创建动态数组的方式。make 函数会分配一个元素为零值的数组并返回一个引用了它的切片：\na := make([]int, 5) // len(a)=5\n要指定它的容量，需向 make 传入第三个参数：\nb := make([]int, 0, 5) // len(b)=0, cap(b)=5\nb = b[:cap(b)] // len(b)=5, cap(b)=5\nb = b[1:] // len(b)=4, cap(b)=4\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 a := make([]int, 5) 7 printSlice(\u0026#34;a\u0026#34;, a) // output: a len=5 cap=5 [0 0 0 0 0] 8 9 b := make([]int, 0, 5) 10 printSlice(\u0026#34;b\u0026#34;, b) // output: b len=0 cap=5 [] 11 12 c := b[:2] 13 printSlice(\u0026#34;c\u0026#34;, c) // output: c len=2 cap=5 [0 0] 14 15 d := c[2:5] 16 printSlice(\u0026#34;d\u0026#34;, d) // output: d len=3 cap=3 [0 0 0] 17} 18 19func printSlice(s string, x []int) { 20 fmt.Printf(\u0026#34;%s len=%d cap=%d %v\\n\u0026#34;, 21 s, len(x), cap(x), x) 22} 切片可以包含任何类型，当然也包括其他切片。\n点击显示代码 1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;strings\u0026#34; 6) 7 8func main() { 9 // 创建一个井字棋（经典游戏） 10 board := [][]string{ 11 []string{\u0026#34;_\u0026#34;, \u0026#34;_\u0026#34;, \u0026#34;_\u0026#34;}, 12 []string{\u0026#34;_\u0026#34;, \u0026#34;_\u0026#34;, \u0026#34;_\u0026#34;}, 13 []string{\u0026#34;_\u0026#34;, \u0026#34;_\u0026#34;, \u0026#34;_\u0026#34;}, 14 } 15 16 // 两个玩家轮流打上 X 和 O 17 board[0][0] = \u0026#34;X\u0026#34; 18 board[2][2] = \u0026#34;O\u0026#34; 19 board[1][2] = \u0026#34;X\u0026#34; 20 board[1][0] = \u0026#34;O\u0026#34; 21 board[0][2] = \u0026#34;X\u0026#34; 22 23 for i := 0; i \u0026lt; len(board); i++ { 24 fmt.Printf(\u0026#34;%s\\n\u0026#34;, strings.Join(board[i], \u0026#34; \u0026#34;)) 25 } 26 // output: X _ X 27 // output: O _ X 28 // output: _ _ O 29} 向切片追加元素时可以用内置的 append 函数：\nfunc append(s []T, vs ...T) []T\n内置函数的文档对该函数有详细的介绍。\nappend 的第一个参数 s 是一个元素类型为 T 的切片，其余类型为 T 的值将会追加到该切片的末尾。append 会返回一个包含原切片所有元素加上新添加元素的切片。当 s 的底层数组太小，不足以容纳所有给定的值时，它就会分配一个更大的数组。 返回的切片会指向这个新分配的数组。\n（要了解关于切片的更多内容，请阅读文章 Go 切片：用法和本质。）\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 var s []int 7 printSlice(s) // output: len=0 cap=0 [] 8 9 // 可在空切片上追加 10 s = append(s, 0) 11 printSlice(s) // output: len=1 cap=1 [0] 12 13 // 这个切片会按需增长 14 s = append(s, 1) 15 printSlice(s) // output: len=2 cap=2 [0 1] 16 17 // 可以一次性添加多个元素 18 s = append(s, 2, 3, 4) 19 printSlice(s) // output: len=5 cap=6 [0 1 2 3 4] 20} 21 22func printSlice(s []int) { 23 fmt.Printf(\u0026#34;len=%d cap=%d %v\\n\u0026#34;, len(s), cap(s), s) 24} 映射 for 循环的 range 形式可遍历切片或映射，每次迭代都会返回两个值。 第一个值为当前元素的下标，第二个值为该下标所对应元素的一份副本。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} 6 7func main() { 8 for i, v := range pow { 9 fmt.Printf(\u0026#34;2**%d = %d\\n\u0026#34;, i, v) 10 } 11} 点击显示输出结果 20 = 1\n21 = 2\n22 = 4\n23 = 8\n24 = 16\n25 = 32\n26 = 64\n27 = 128 可以将下标或值赋予 _ 来忽略 range 遍历的变量：\nfor i, _ := range pow for _, value := range pow\n若你只需要索引，忽略第二个变量即可：\nfor i := range pow\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 pow := make([]int, 10) 7 for i := range pow { 8 pow[i] = 1 \u0026lt;\u0026lt; uint(i) // == 2**i 9 } 10 for _, value := range pow { 11 fmt.Printf(\u0026#34;%d\\n\u0026#34;, value) 12 } 13} 点击显示输出结果 1\n2\n4\n8\n16\n32\n64\n128\n256\n512 map 映射将键映射到值，映射的零值为 nil 。nil 映射既没有键，也不能添加键。make 函数会返回给定类型的映射，并将其初始化备用。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Vertex struct { 6 Lat, Long float64 7} 8 9var m map[string]Vertex 10 11func main() { 12 m = make(map[string]Vertex) 13 m[\u0026#34;Bell Labs\u0026#34;] = Vertex{ 14 40.68433, -74.39967, 15 } 16 fmt.Println(m[\u0026#34;Bell Labs\u0026#34;]) // output: {40.68433 -74.39967} 17} 映射的字面量和结构体类似，只不过必须有键名。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Vertex struct { 6 Lat, Long float64 7} 8 9var m = map[string]Vertex{ 10 \u0026#34;Bell Labs\u0026#34;: Vertex{ 11 40.68433, -74.39967, 12 }, 13 \u0026#34;Google\u0026#34;: Vertex{ 14 37.42202, -122.08408, 15 }, 16} 17 18func main() { 19 fmt.Println(m) // output: map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}] 20} 若顶层类型只是一个类型名，那么你可以在字面量的元素中省略它。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5type Vertex struct { 6 Lat, Long float64 7} 8 9var m = map[string]Vertex{ 10 \u0026#34;Bell Labs\u0026#34;: {40.68433, -74.39967}, 11 \u0026#34;Google\u0026#34;: {37.42202, -122.08408}, 12} 13 14func main() { 15 fmt.Println(m) // output: map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}] 16} 在映射 m 中插入或修改元素：[key] = elem\n获取元素：elem = m[key]\n删除元素：delete(m, key)\n通过双赋值检测某个键是否存在：elem, ok = m[key]\n若 key 在 m 中，ok 为 true ；否则，ok 为 false。\n若 key 不在映射中，则 elem 是该映射元素类型的零值。\n注：若 elem 或 ok 还未声明，你可以使用短变量声明： elem, ok := m[key]\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 m := make(map[string]int) 7 8 m[\u0026#34;答案\u0026#34;] = 42 9 fmt.Println(\u0026#34;值：\u0026#34;, m[\u0026#34;答案\u0026#34;]) // output: 值： 42 10 11 m[\u0026#34;答案\u0026#34;] = 48 12 fmt.Println(\u0026#34;值：\u0026#34;, m[\u0026#34;答案\u0026#34;]) // output: 值： 48 13 14 delete(m, \u0026#34;答案\u0026#34;) 15 fmt.Println(\u0026#34;值：\u0026#34;, m[\u0026#34;答案\u0026#34;]) // output: 值： 0 16 17 v, ok := m[\u0026#34;答案\u0026#34;] 18 fmt.Println(\u0026#34;值：\u0026#34;, v, \u0026#34;是否存在？\u0026#34;, ok) // output: 值： 0 是否存在？ false 19} 函数也是值，可以像其他值一样传递。函数值可以用作函数的参数或返回值。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8func compute(fn func(float64, float64) float64) float64 { 9 return fn(3, 4) 10} 11 12func main() { 13 hypot := func(x, y float64) float64 { 14 return math.Sqrt(x*x + y*y) 15 } 16 fmt.Println(hypot(5, 12)) // output: 13 17 fmt.Println(compute(hypot)) // output: 5 18 fmt.Println(compute(math.Pow)) // output: 81 19} 执行流程与输出\n直接调用hypot(5,12): 计算：√(5² + 12²) = √169 = 13 → 输出13。 compute(hypot): 传入hypot到compute，执行hypot(3,4) → √(9 + 16) = √25 = 5 → 输出5。 compute(math.Pow): 传入math.Pow到compute，执行3^4 → 81 → 输出81。 Go 函数可以是一个闭包。闭包是一个函数值，它引用了其函数体之外的变量。 该函数可以访问并赋予其引用的变量值，换句话说，该函数被“绑定”到了这些变量。\n例如，函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func adder() func(int) int { 6 sum := 0 7 return func(x int) int { 8 sum += x 9 return sum 10 } 11} 12 13func main() { 14 pos, neg := adder(), adder() 15 for i := 0; i \u0026lt; 10; i++ { 16 fmt.Println( 17 pos(i), 18 neg(-2*i), 19 ) 20 } 21} 点击显示输出结果 0 0\n1 -2\n3 -6\n6 -12\n10 -20\n15 -30\n21 -42\n28 -56\n36 -72\n45 -90 小结 还是水水的一篇，只为了证明一下自己还在学（进度好像确实有点慢）。唔，在整其他的东西导致有点忙，希望下周能把新手教程全过完，然后参考一下别人的项目。\n","date":"2025-03-27T16:42:22+08:00","image":"https://langminjeii.ifantic.de/post/go-2.png","permalink":"https://langminjeii.ifantic.de/post/go-%E8%90%8C%E6%96%B0%E7%9A%84%E5%88%9D%E5%AD%A6%E4%B9%8B%E8%B7%AF-ii/","title":"Go 萌新的初学之路 II"},{"content":"包、变量与函数 程序从 main 包开始运行。可以通过导入路径 \u0026quot;math/rand\u0026quot; 来使用这个包，包名与导入路径的最后一个元素一致。例如，\u0026quot;math/rand\u0026quot; 包中的源码均以 package rand 语句开始。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math/rand\u0026#34; 6) 7 8func main() { 9 fmt.Println(\u0026#34;我最喜欢的数字是 \u0026#34;, rand.Intn(10)) 10} 在 Go 中，只有首字母大写的标识符才会被导出，也就是说小写的标识符在其他包中是不可见的。可以自定义package常量，不过没有必要：\n多此一举 1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8func main() { 9 const pi = math.Pi 10 fmt.Println(pi) 11} 输出： 3.141592653589793\n萌新的疑问 可以 import \u0026quot;math/rand\u0026quot; ，那么可不可以 import \u0026quot;math/rand\u0026quot; 呢？答案是不可以的，因为 math/rand 是标准库中真实存在的独立子包，而 Pi 是 math 包的导出常量，不属于包。以下是常用的子包：\nmath/rand：随机数相关 math/big：大数运算 math/cmplx：复数运算 函数可以返回任意数量的返回值。swap 函数返回了两个字符串。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func swap(x, y string) (string, string) { 6 return y, x 7} 8 9func main() { 10 a, b := swap(\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;) 11 fmt.Println(a, b) 12} 输出： world hello\nGo 的返回值可被命名，它们会被视作定义在函数顶部的变量。没有参数的 return 语句会直接返回已命名的返回值，也就是「裸」返回值。裸返回语句在长的函数中它们会影响代码的可读性。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func split(sum int) (x, y int) { 6 x = sum * 4 / 9 7 y = sum - x 8 return 9} 10 11func main() { 12 fmt.Println(split(17)) 13} 输出：7 10\nvar 语句用于声明一系列变量，可以出现在包或函数的层级。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5var c, python, java bool 6 7func main() { 8 var i int 9 fmt.Println(i, c, python, java) 10} 输出：0 false false false\n如果声明变量时提供了初始值，则类型可以省略；变量会从初始值中推断出类型。在这里通过fmt.Printf的%T打印变量的类型：\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5var i, j = 1, 2 6 7func main() { 8 var c, python, java = true, false, \u0026#34;no!\u0026#34; 9 fmt.Println(i, j, c, python, java) 10 fmt.Printf(\u0026#34;%T, %T, %T, %T, %T\u0026#34;, i, j, c, python, java) 11} 输出：\n1 2 true false no!\nint, int, bool, bool, string\n短变量声明操作符:=是用来创建具有适当名称和初始值的变量。使用这个操作符的主要目的是在函数中声明和初始化 局部变量 ，并缩小变量的范围。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 var i, j int = 1, 2 7 k := 3 8 c, python, java := true, false, \u0026#34;no!\u0026#34; 9 10 fmt.Println(i, j, k, c, python, java) 11 fmt.Printf(\u0026#34;%T, %T, %T, %T, %T, %T\u0026#34;, i, j, k, c, python, java) 12} 输出：\n1 2 3 true false no!\nint, int, int, bool, bool, string\nGo 的基本类型有：\nbool\nstring\nint int8 int16 int32 int64\nuint uint8 uint16 uint32 uint64 uintptr\nbyte // uint8 的别名\nrune // int32 的别名，表示一个 Unicode 码位\nfloat32 float64\ncomplex64 complex128 // 复数的64位浮点数表示\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math/cmplx\u0026#34; 6) 7 8var ( 9 ToBe bool = false 10 MaxInt uint64 = 1\u0026lt;\u0026lt;64 - 1 11 z complex128 = cmplx.Sqrt(-5 + 12i) 12) 13 14func main() { 15 fmt.Printf(\u0026#34;类型：%T 值：%v\\n\u0026#34;, ToBe, ToBe) 16 fmt.Printf(\u0026#34;类型：%T 值：%v\\n\u0026#34;, MaxInt, MaxInt) 17 fmt.Printf(\u0026#34;类型：%T 值：%v\\n\u0026#34;, z, z) 18} 输出：\n类型：bool 值：false\n类型：uint64 值：18446744073709551615\n类型：complex128 值：(2+3i)\nrune 类型是 Go 语言的一种特殊数字类型。在 builtin/builtin.go 文件中，它的定义：type rune = int32 ；官方对它的解释是：rune 是类型 int32 的别名，在所有方面都等价于它，用来区分字符值跟整数值。使用单引号定义，返回采用 UTF-8 编码的 Unicode 码点。Go 语言通过 rune 处理中文，支持国际化多语言。\nUTF-8 编码规则：\nASCII 字符（0-127）：1 字节 带变音符号的拉丁/希腊/西里尔字母：2 字节 常用汉字（CJK 统一表意文字）：3 字节 生僻字/历史文字/表情符号：4 字节 1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5) 6 7func main() { 8 s := \u0026#34;Go语言编程\u0026#34; 9 10 fmt.Println(len(\u0026#34;Go语言编程\u0026#34;)) // 输出：14 11 fmt.Println(len([]rune(\u0026#34;Go语言编程\u0026#34;))) // 输出：6 12 fmt.Println(s[0:8]) // 输出：Go语言 13 fmt.Println(s[0:7]) // 输出：Go语�� 14 fmt.Println(string([]rune(s)[:4])) // 输出：Go语言 15 16 // byte 17 fmt.Println([]byte(s)) // 输出：[71 111 232 175 173 232 168 128 231 188 150 231 168 139] 18 // rune 19 fmt.Println([]rune(s)) // 输出：[71 111 35821 35328 32534 31243] 20} 字符串由字符组成，字符的底层由字节组成，而一个字符串在底层的表示是一个字节序列。在 Go 语言中，字符可以被分成两种类型处理：对占 1 个字节的英文类字符，可以使用 byte （或者 unit8 ）；对占 1 ~ 4 个字节的其他字符，可以使用 rune （或者 int32 ），如中文、特殊符号等。\nGo 语言把字符分 byte 和 rune 两种类型处理。byte 是类型 unit8 的别名，用于存放占 1 字节的 ASCII 字符，如英文字符，返回的是字符原始字节。rune 是类型 int32 的别名，用于存放多字节字符，如占 3 字节的中文字符，返回的是字符 Unicode 码点值。\nstring、byte、rune三者间的关系：\n字符串在底层的表示是由单个字节组成的一个不可修改的字节序列，字节使用 UTF-8[1] 编码标识 Unicode[2] 文本。Unicode 文本意味着 .go 文件内可以包含世界上的任意语言或字符，该文件在任意系统上打开都不会乱码。UTF-8 是 Unicode 的一种实现方式，是一种针对 Unicode 可变长度的字符编码，它定义了字符串具体以何种方式存储在内存中。UFT-8 使用 1 ~ 4 为每个字符编码。\nGo 语言把字符分 byte 和 rune 两种类型处理。byte 是类型 unit8 的别名，用于存放占 1 字节的 ASCII 字符，如英文字符，返回的是字符原始字节。rune 是类型 int32 的别名，用于存放多字节字符，如占 3 字节的中文字符，返回的是字符 Unicode 码点值。如下图所示：\n常量的声明与变量类似，只不过使用 const 关键字。常量可以是字符、字符串、布尔值或数值，但不能用 := 语法声明。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5const Pi = 3.14 6 7func main() { 8 const world = \u0026#34;世界\u0026#34; 9 fmt.Println(\u0026#34;Hello\u0026#34;, world) 10 fmt.Println(\u0026#34;Happy\u0026#34;, Pi, \u0026#34;Day\u0026#34;) 11 12 const Truth = true 13 fmt.Println(\u0026#34;Go rules?\u0026#34;, Truth) 14 15 fmt.Printf(\u0026#34;%T, %T, %T\u0026#34;, world, Truth, Pi) 16} 输出：\nHello 世界\nHappy 3.14 Day\nGo rules? true\nstring, bool, float64\n数值常量是高精度的值。int 类型可以存储最大 64 位的整数，根据平台不同有时会更小。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5const Pi = 3.14 6 7func main() { 8 const world = \u0026#34;世界\u0026#34; 9 fmt.Println(\u0026#34;Hello\u0026#34;, world) 10 fmt.Println(\u0026#34;Happy\u0026#34;, Pi, \u0026#34;Day\u0026#34;) 11 12 const Truth = true 13 fmt.Println(\u0026#34;Go rules?\u0026#34;, Truth) 14 15 fmt.Printf(\u0026#34;%T, %T, %T\u0026#34;, world, Truth, Pi) 16} 输出：\n21\n0.2\n1.2676506002282295e+29\n在Go中，单引号括起来的是rune类型，也就是int32的别名，用来表示Unicode码点。而双引号括起来的才是字符串，比如\u0026quot;d\u0026quot;就是string类型。python转过来的真的有好多细节不适应 : (\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 v := \u0026#39;d\u0026#39; 7 fmt.Printf(\u0026#34;v is of type %T\\n\u0026#34;, v) 8 // 输出： v is of type int32 9} 流程控制语句：for、if、else、switch 和 defer Go 只有一种循环结构：for 循环。基本的 for 循环由三部分组成，它们用分号隔开：\n初始化语句：在第一次迭代前执行 条件表达式：在每次迭代前求值 后置语句：在每次迭代的结尾执行 初始化语句通常为一句短变量声明，该变量声明仅在 for 语句的作用域中可见。一旦条件表达式求值为 false，循环迭代就会终止。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 sum := 0 7 for i := 0; i \u0026lt; 10; i++ { 8 sum += i 9 } 10 fmt.Println(sum) 11} 输出：45 （1+2+3+4+5+6+7+8+9）\n初始化语句和后置语句是可选的 1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 sum := 1 7 for ; sum \u0026lt; 1000; { 8 sum += sum 9 fmt.Println(sum) 10 } 11 fmt.Println(sum) 12} 输出结果 2\n4\n8\n16\n32\n64\n128\n256\n512\n1024\n1024\n怎么写 Go 中的「while」语句呢？其实就是 for 循环去掉了分号：\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 sum := 1 7 for sum \u0026lt; 1000 { 8 sum += sum 9 fmt.Println(sum) 10 } 11 fmt.Println(sum) 12} 输出结果 2\n4\n8\n16\n32\n64\n128\n256\n512\n1024\n1024\nif 语句与 for 循环类似：\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8func sqrt(x float64) string { 9 if x \u0026lt; 0 { 10 return sqrt(-x) + \u0026#34;i\u0026#34; 11 } 12 return fmt.Sprint(math.Sqrt(x)) 13} 14 15func main() { 16 fmt.Println(sqrt(2), sqrt(-4)) 17} 输出：1.4142135623730951 2i\n和 for 一样，if 语句可以在条件表达式前执行一个简短语句。该语句声明的变量作用域仅在 if 之内。（在pow最后的 return 语句处使用 v ，会报错 undefined: v ）\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8func pow(x, n, lim float64) float64 { 9 if v := math.Pow(x, n); v \u0026lt; lim { 10 return v 11 } 12 return lim 13} 14 15func main() { 16 fmt.Println( 17 pow(3, 2, 10), 18 pow(3, 3, 20), 19 ) 20} 输出：9 20\n在 if 的简短语句中声明的变量同样可以在对应的任何 else 块中使用。（在 main 的 fmt.Println 调用开始前，两次对 pow 的调用均已执行并返回其各自的结果。）\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;math\u0026#34; 6) 7 8func pow(x, n, lim float64) float64 { 9 if v := math.Pow(x, n); v \u0026lt; lim { 10 return v 11 } else { 12 fmt.Printf(\u0026#34;%g \u0026gt;= %g\\n\u0026#34;, v, lim) 13 } 14 // can\u0026#39;t use v here, though 15 return lim 16} 17 18func main() { 19 fmt.Println( 20 pow(3, 2, 10), 21 pow(3, 3, 20), 22 ) 23} 输出：\n27 \u0026gt;= 20\n9 20\nswitch 语句是编写一连串 if - else 语句的简便方法。它运行第一个 case 值 值等于条件表达式的子句。\nGo 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的，不过 Go 只会运行选定的 case，而非之后所有的 case。 在效果上，Go 的做法相当于这些语言中为每个 case 后面自动添加了所需的 break 语句。在 Go 中，除非以 fallthrough 语句结束，否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量，且取值不限于整数。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;runtime\u0026#34; 6) 7 8func main() { 9 fmt.Print(\u0026#34;Go 运行的系统环境：\u0026#34;) 10 switch os := runtime.GOOS; os { 11 case \u0026#34;darwin\u0026#34;: 12 fmt.Println(\u0026#34;macOS.\u0026#34;) 13 case \u0026#34;linux\u0026#34;: 14 fmt.Println(\u0026#34;Linux.\u0026#34;) 15 default: 16 // freebsd, openbsd, 17 // plan9, windows... 18 fmt.Printf(\u0026#34;%s.\\n\u0026#34;, os) 19 } 20} 输出：Go 运行的系统环境：Linux.\nswitch 的 case 语句从上到下顺次执行，直到匹配成功时停止。如：\n1switch i { 2 case 0: 3 case f(): 4} 注意： 在 i==0 时，f 不会被调用。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;time\u0026#34; 6) 7 8func main() { 9 fmt.Println(\u0026#34;周六是哪天？\u0026#34;) 10 today := time.Now().Weekday() 11 switch time.Saturday { 12 case today + 0: 13 fmt.Println(\u0026#34;今天。\u0026#34;) 14 case today + 1: 15 fmt.Println(\u0026#34;明天。\u0026#34;) 16 case today + 2: 17 fmt.Println(\u0026#34;后天。\u0026#34;) 18 default: 19 fmt.Println(\u0026#34;很多天后。\u0026#34;) 20 } 21} 输出：\n周六是哪天？\n很多天后。\n无条件的 switch 同 switch true 一样。这种形式能将一长串 if-then-else 写得更加清晰。\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;time\u0026#34; 6) 7 8func main() { 9 t := time.Now() 10 switch { 11 case t.Hour() \u0026lt; 12: 12 fmt.Println(\u0026#34;早上好！\u0026#34;) 13 case t.Hour() \u0026lt; 17: 14 fmt.Println(\u0026#34;下午好！\u0026#34;) 15 default: 16 fmt.Println(\u0026#34;晚上好！\u0026#34;) 17 } 18} 输出：晚上好！\ndefer 语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值，但直到外层函数返回前该函数都不会被调用。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 defer fmt.Println(\u0026#34;world\u0026#34;) 7 8 fmt.Println(\u0026#34;hello\u0026#34;) 9} 输出：\nhello\nworld\n推迟调用的函数调用会被压入一个栈中。 当外层函数返回时，被推迟的调用会按照后进先出的顺序调用。\n1package main 2 3import \u0026#34;fmt\u0026#34; 4 5func main() { 6 fmt.Println(\u0026#34;counting\u0026#34;) 7 8 for i := 0; i \u0026lt; 10; i++ { 9 defer fmt.Println(i) 10 } 11 12 fmt.Println(\u0026#34;done\u0026#34;) 13} 输出结果 counting\ndone\n9\n8\n7\n6\n5\n4\n3\n2\n1\n0\n小结 作为一个 python 爱好者，转到 go 后在许多小角落都会有些迷茫，比如大小写敏感、单双引号不通用等等。但是还是记录一下自己的学习进程，敦促自己尽快打好 go 的基础，之后打算找几个项目练练手。\n","date":"2025-03-22T20:03:52+08:00","image":"https://langminjeii.ifantic.de/post/go-1.png","permalink":"https://langminjeii.ifantic.de/post/go-%E8%90%8C%E6%96%B0%E7%9A%84%E5%88%9D%E5%AD%A6%E4%B9%8B%E8%B7%AF/","title":"Go 萌新的初学之路"},{"content":"首先你需要确定要使用的优选域名。你可以使用自建的优选域名，也可以使用网友公益维护的优选域名。更多优选内容参考 CloudFlare优选。\n为 Workers 项目使用优选域名 由于我的文件快递柜是用 Worker 搭的，因此本教程演示的 Worker 使用的自定义域名示例为 files.ifantic.me 。\n设置自定义域 CNAME 记录至优选域名 给你将要使用的域名 ifantic.me，添加一个 CNAME 类型的解析记录，名称为所需的自定义域示例 files，目标为优选域名示例 visa.cn （你想选哪个就选哪个）。点击保存：\n重要提示：不要打开小黄云（Cloudflare 的代理功能）！！！\n给 Workers 项目添加路由 选中 ifantic.me 域名后，左侧选择 Workers 路由 \u0026gt; 添加路由； 路由填入 Worker 项目最终使用的自定义域 files.ifantic.me/*，Worker 选中对应的Worker项目名后点击保存即可。 注意自定义域末尾必须加上 /*\n，也就是 files.ifantic.me/* ！！！\n为 Pages 项目使用优选域名 本教程演示的 Pages 使用我的博客，自定义域名示例为 blog.ifantic.me。\n为 Pages 项目添加自定义域 先给 Pages 项目添加自定义域，等待自定义域生效。\n使用华为云国际版-云解析服务 DNS 注册华为云国际版-云解析服务 DNS（国际版不需要实名，如果注册时频繁跳转国内版本，请打开全局代理模式注册）。\n注册华为云国际版 设置您的安全手机(跳过即可) 开通华为云 跳转到完善信息页面说明注册已完成，不用管他，无视绑定提示即可。 添加自定义域至云解析服务 DNS 点击前往云解析服务 DNS，点击 公网域名 点击右上角 创建公网域名 填入需要优选的 Pages 自定义域名 blog.ifantic.me，然后点击 确定 点击 blog.ifantic.me 的管理解析 可能有人发现了，我说的是 blog.ifantic.me 的解析，怎么图中变成了 ifantic.me 的解析呢？待会你就会知道了，你先照着帖子配置下去 🤷 点击右上角的 添加记录集 新添加一个 CNAME 记录，线路类型为全网默认，记录值为 Pages 项目分配的域名 hb-81t.pages.dev。 再添加一个 CNAME 记录，线路类型为地域解析 \u0026gt; 中国大陆，记录值为优选域名 visa.cn。 将第一条 NS 记录的4条 NS 记录值复制出来保存，或者你就用我的。 1ns1.huaweicloud-dns.com 2ns1.huaweicloud-dns.cn 3ns1.huaweicloud-dns.net 4ns1.huaweicloud-dns.org 设置自定义域NS 记录至华为云 NS 记录 踩坑 当初我配置的R2图床是用自定义域 static.blog.ifantic.me，但是用了这个pages加速博客之后，图床会报错用不了。华为云cdn折腾了一天，在网上找了一番也没有解决办法，只能把R2换成 static.houhuayuan.me，将就着用吧 🤷\n有哪位大佬知道的，欢迎评论交流一下~\n参考链接 使用优选域名加速-Workers-Pages-项目\n","date":"2025-03-07T16:58:18+08:00","image":"https://langminjeii.ifantic.de/post/cloudflare.webp","permalink":"https://langminjeii.ifantic.de/post/%E5%8A%A0%E9%80%9F%E6%88%91%E7%9A%84%E5%8D%9A%E5%AE%A2/","title":"加速我的博客"},{"content":"博客刚弄好，还是有不少问题的。比如图片不能点击放大，也没有评论功能（虽然没什么人会评论吧）。于是乎想着整一个Waline评论系统，其实Waline文档写的挺详细的，不过还是因自身需求做些改动水一下好了，感兴趣的可以查阅：\nVercel 部署 | Waline 添加Waline评论系统 博客搭建完成之后，可以添加一个评论系统。这里选用的是 Waline，因为 Hugo Stack 主题支持 Waline，所以配置起来也会方便不少。同时Waline只需几个步骤，就可以在你的网站中启用 Waline 提供评论服务，并对评论配置tg通知。\nLeanCloud 设置 (数据库) 登录 或 注册 LeanCloud 国际版 并进入 控制台\n点击 创建应用 并起一个你喜欢的名字 (选择免费的开发版):\n进入应用，选择左下角的 设置 -\u0026gt; 应用 Key。你可以看到你的 APP ID, APP Key 和 Master Key。请记录它们，以便后续使用。\nVercel 部署 (服务端) vercel部署\n输入一个你喜欢的项目名称并点击 Create 继续\n此时 Vercel 会基于 Waline 模板帮助你新建并初始化仓库，仓库名为你之前输入的项目名。\n一两分钟后，满屏的烟花会庆祝你部署成功。此时点击 Go to Dashboard 可以跳转到应用的控制台。\n点击顶部的 Settings - Environment Variables 进入环境变量配置页，并配置三个环境变量 LEAN_ID, LEAN_KEY 和 LEAN_MASTER_KEY 。它们的值分别对应上一步在 LeanCloud 中获得的 APP ID, APP KEY, Master Key。\n添加 Telegram 通知 Telegram 通知通过 Telegram bot 机器人实现，需要配置以下几个环境变量:\nTG_BOT_TOKEN: Telegram 机器人用于访问 HTTP API 的 token，通过 @BotFather 创建机器人获取，必填。 TG_CHAT_ID: 接收消息对象的 chat_id，可以是单一用户、频道、群组，通过 @userinfobot 获取，必填。 AUTHOR_EMAIL: 博主邮箱，用来区分发布的评论是否是博主本身发布的。如果是博主发布的则不进行提醒通知。 SITE_NAME: 网站名称，用于在消息中显示。 SITE_URL: 网站地址，用于在消息中显示。 TG_TEMPLATE: Telegram 使用的通知模板，变量与具体格式可参见下文的通知模板。未配置则使用默认模板。 如果你想让评论要经审核才发布的话，可以加上以下变量：\nCOMMENT_AUDIT\t- true\n开启后评论需要经过管理员审核后才能显示，所以建议在评论框默认文字上提供提示。\n环境变量配置完成并保存后点击 Redeploy 按钮进行重新部署，让刚才设置的环境变量生效。\n此时会跳转到 Overview 界面开始部署，等待片刻后 STATUS 会变成 Ready。此时请点击 Visit ，即可跳转到部署好的网站地址，此地址即为你的服务端地址。\n但是由于vercel.app被污染了，所以你需要添加自定义域（在cf托管的即可）。\n主题启用 Waline 在 hugo.yaml 配置文件里找到这一区块：\n1params: 2 comments: 3 enabled: true 4 provider: 将 enabled 改 true，provider 改为 waline，然后在 comments.waline 区块设置 waline 的相关配置：\n1# Waline client configuration see: https://waline.js.org/en/reference/component.html 2waline: 3 serverURL: your-server-url 4 lang: zh-CN 5 pageview: true 6 emoji: 7 - https://unpkg.com/@waline/emojis@1.0.1/weibo 8 requiredMeta: 9 - name 10 - email 11 locale: 12 admin: Admin 13 placeholder: \u0026#34;快来说点什么吧~~（评论经审核后显示喵）\u0026#34; serverURL 改为你的 vercel 的服务端地址即可。\n部署完成后，请访问 \u0026lt;serverURL\u0026gt;/ui/register 进行注册。首个注册的人会被设定成管理员。\n管理员登陆后，即可看到评论管理界面。在这里可以修改、标记或删除评论。用户也可通过评论框注册账号，登陆后会跳转到自己的档案页。\nfancybox灯箱导入使图片能够点击放大 修改 config.toml 或者 hugo.yaml 我使用的是stack主题，配置文件为 hugo.yaml，在params中添加：\n1params: 2 fancybox: true 创建并修改 render-image.html 创建路径为 /layouts/_default/_markup/render-image.html，填入以下内容：\n1{{if .Page.Site.Params.fancybox }} 2\u0026lt;div class=\u0026#34;post-img-view\u0026#34;\u0026gt; 3\u0026lt;a data-fancybox=\u0026#34;gallery\u0026#34; href=\u0026#34;{{ .Destination | safeURL }}\u0026#34;\u0026gt; 4\u0026lt;img src=\u0026#34;{{ .Destination | safeURL }}\u0026#34; alt=\u0026#34;{{ .Text }}\u0026#34; {{ with .Title}} title=\u0026#34;{{ . }}\u0026#34;{{ end }} /\u0026gt; 5\u0026lt;/a\u0026gt; 6\u0026lt;/div\u0026gt; 7{{ end }} 在 footer.html 添加内容 在 layouts\\partials\\article\\components\\footer.html 中添加\n1{{if .Page.Site.Params.fancybox }} 2\u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 3\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css\u0026#34; /\u0026gt; 4\u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 5{{ end }} 即可启用灯箱，舒舒服服地放大图片啦！\n取消 markdown 严格换行 参考hugo换行 - Bboysoul\u0026rsquo;s Blog，不修改之前一定要在markdown换两行才能在博文里换行，实在是太烦了。\n其实只要在 hugo.yaml 里面对应的选项里添加： hardWraps: true\n1markup: 2 goldmark: 3 renderer: 4 hardWraps: true 文章添加密码 依旧是找找有没有大神的作业能参考，找来找去大差不差，也不知道哪个是源头，姑且贴一个远古的博客链接罢：\nHugo系列(3.1) - LoveIt主题美化与博客功能增强 · 第二章 - 雨临Lewis\n但是呢，并不适用于hugo-theme-stack，要不输入正确的密码后变成空白页还是打不开，要不就报错不能用。没办法，食過返尋味，再一次施展AI大法，在 layouts/_default/single.html 中找到 {{ define \u0026quot;main\u0026quot; }} 和 {{ partial \u0026quot;article/article.html\u0026quot; . }} 的中间部分，粘贴代码：\n1\u0026lt;!-- 以上代码不变 --\u0026gt; 2{{ define \u0026#34;main\u0026#34; }} 3 {{ if .Params.password }} 4 \u0026lt;div class=\u0026#34;post-password-protection\u0026#34;\u0026gt; 5 \u0026lt;script\u0026gt; 6 (function(){ 7 // Store the password from the front matter 8 const correctPassword = {{ .Params.password }}; 9 // Prompt for password 10 const enteredPassword = prompt(\u0026#39;请输入文章密码：\u0026#39;); 11 12 // Check if password is correct 13 if (enteredPassword !== correctPassword) { 14 alert(\u0026#39;密码错误！\u0026#39;); 15 // Redirect back or close window 16 if (history.length \u0026lt;= 1) { 17 window.opener = null; 18 window.open(\u0026#39;\u0026#39;, \u0026#39;_self\u0026#39;); 19 window.close(); 20 } else { 21 history.back(); 22 } 23 } 24 })(); 25 \u0026lt;/script\u0026gt; 26 \u0026lt;/div\u0026gt; 27 {{ end }} 28 29 {{ partial \u0026#34;article/article.html\u0026#34; . }} 30 \u0026lt;!-- 以上下代码不变 --\u0026gt; 然后在你要加密的文章顶部信息处加入密码即可：\n1--- 2title: 随笔 3password: test 4--- 测试链接（密码：test）\n然后我又发现一个问题，输错密码在返回的时候会短暂的闪现出加密文章的界面。再次狠狠地CPU Claude3.7，顺便把一次性检验与永久检验做出来了。把整个 single.html 改成如下内容：\n1{{ define \u0026#34;body-class\u0026#34; }} 2 article-page 3 {{/* Widget logic remains unchanged */}} 4 {{- $HasWidgetNotTOC := false -}} 5 {{- $TOCWidgetEnabled := false -}} 6 {{- range .Site.Params.widgets.page -}} 7 {{- if ne .type \u0026#34;toc\u0026#34; -}} 8 {{ $HasWidgetNotTOC = true -}} 9 {{- else -}} 10 {{ $TOCWidgetEnabled = true -}} 11 {{- end -}} 12 {{- end -}} 13 14 {{- $TOCManuallyDisabled := eq .Params.toc false -}} 15 {{- $TOCEnabled := and (not $TOCManuallyDisabled) $TOCWidgetEnabled -}} 16 {{- $hasTOC := ge (len .TableOfContents) 100 -}} 17 {{- .Scratch.Set \u0026#34;TOCEnabled\u0026#34; (and $TOCEnabled $hasTOC) -}} 18 19 {{- .Scratch.Set \u0026#34;hasWidget\u0026#34; (or $HasWidgetNotTOC (and $TOCEnabled $hasTOC)) -}} 20{{ end }} 21 22{{ define \u0026#34;main\u0026#34; }} 23 {{ if .Params.password }} 24 \u0026lt;style\u0026gt; 25 /* Hide content by default when password protection is enabled */ 26 .article-content, .article-header, .article-footer, .article-image, 27 .related-content, .article-links, #comments { 28 display: none; 29 } 30 /* Only show content when authorized class is added */ 31 .password-authorized .article-content, 32 .password-authorized .article-header, 33 .password-authorized .article-footer, 34 .password-authorized .article-image, 35 .password-authorized .related-content, 36 .password-authorized .article-links, 37 .password-authorized #comments { 38 display: block; 39 } 40 \u0026lt;/style\u0026gt; 41 \u0026lt;div class=\u0026#34;post-password-protection\u0026#34;\u0026gt; 42 \u0026lt;script\u0026gt; 43 (function(){ 44 // Generate a unique key for this article 45 const articleId = \u0026#34;{{ .File.UniqueID | default .Permalink }}\u0026#34;; 46 const storageKey = \u0026#34;article_auth_\u0026#34; + articleId; 47 const correctPassword = \u0026#34;{{ .Params.password }}\u0026#34;; 48 // Check if persistent authentication is enabled (default: false) 49 const persistAuth = {{ .Params.persistAuth | default false }}; 50 51 let isAuthorized = false; 52 53 // Only check localStorage if persistent auth is enabled 54 if (persistAuth) { 55 isAuthorized = localStorage.getItem(storageKey) === \u0026#34;true\u0026#34;; 56 } 57 58 if (!isAuthorized) { 59 // Prompt for password 60 const enteredPassword = prompt(\u0026#39;请输入文章密码：\u0026#39;); 61 62 // 用户点击了取消按钮 63 if (enteredPassword === null) { 64 if (history.length \u0026lt;= 1) { 65 window.opener = null; 66 window.open(\u0026#39;\u0026#39;, \u0026#39;_self\u0026#39;); 67 window.close(); 68 } else { 69 history.back(); 70 } 71 return; 72 } 73 74 // 用户输入了密码但不正确 75 if (enteredPassword !== correctPassword) { 76 alert(\u0026#39;密码错误！\u0026#39;); 77 if (history.length \u0026lt;= 1) { 78 window.opener = null; 79 window.open(\u0026#39;\u0026#39;, \u0026#39;_self\u0026#39;); 80 window.close(); 81 } else { 82 history.back(); 83 } 84 return; 85 } 86 87 // 密码正确 88 if (persistAuth) { 89 localStorage.setItem(storageKey, \u0026#34;true\u0026#34;); 90 } 91 isAuthorized = true; 92 } 93 94 // If we reach here, user is authorized 95 document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { 96 document.body.classList.add(\u0026#39;password-authorized\u0026#39;); 97 }); 98 })(); 99 100 \u0026lt;/script\u0026gt; 101 \u0026lt;/div\u0026gt; 102 {{ end }} 103 104 {{ partial \u0026#34;article/article.html\u0026#34; . }} 105 106 {{ if .Params.links }} 107 {{ partial \u0026#34;article/components/links\u0026#34; . }} 108 {{ end }} 109 110 {{ partial \u0026#34;article/components/related-content\u0026#34; . }} 111 112 {{ if not (eq .Params.comments false) }} 113 {{ partial \u0026#34;comments/include\u0026#34; . }} 114 {{ end }} 115 116 {{ partialCached \u0026#34;footer/footer\u0026#34; . }} 117 118 {{ partialCached \u0026#34;article/components/photoswipe\u0026#34; . }} 119{{ end }} 120 121{{ define \u0026#34;right-sidebar\u0026#34; }} 122 {{ if .Scratch.Get \u0026#34;hasWidget\u0026#34; }}{{ partial \u0026#34;sidebar/right.html\u0026#34; (dict \u0026#34;Context\u0026#34; . \u0026#34;Scope\u0026#34; \u0026#34;page\u0026#34;) }}{{ end}} 123{{ end }} 就把加密文章的问题解决啦！如果你想用一次性验证的话，就用上面那个默认密码设置就好。想永久身份验证的话，就可以添加以下配置：\n1--- 2title: 测试 3password: test 4persistAuth: true 5--- 永久验证测试链接（密码：test）\n上面的两个测试链接都有樱花特效哦，具体配置见下面\n2025.4.7 更新 上面的加密方法其实只是图一乐而已啦！\n想要真正比较安全的加密，可以看看这篇文章 ~\n把文章从主页和归档中隐藏 上面整出来的这两篇文章仅仅只是为了测试，放在首页和归档页好像不太好。好在官方有解决办法，在文章的设置里加上：\n1--- 2title: \u0026#34;测试\u0026#34; 3description: 4hidden: true 5--- 但是我发现这么操作的话，底部卜算子的统计和热力图还是会检索到这篇文章。于是把参数改一下：\n1--- 2title: \u0026#34;测试\u0026#34; 3description: 4_build: 5 list: false # 不在列表（主页/归档）中显示 6 render: true # 仍生成最终页面 7--- 这样的话热力图和底下的统计信息也没把他们算进去了，除非你公布链接，不然它们就像销声匿迹了一样 ~\n加载进度条 参考博客切换到STACK 主题 - 一不留神的博客，在 layouts/partials/footer/custom.html 后面加入：\n1\u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/gh/zhixuan2333/gh-blog@v0.1.0/js/nprogress.min.js\u0026#34; integrity=\u0026#34;sha384-bHDlAEUFxsRI7JfULv3DTpL2IXbbgn4JHQJibgo5iiXSK6Iu8muwqHANhun74Cqg\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 2\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdn.jsdelivr.net/gh/zhixuan2333/gh-blog@v0.1.0/css/nprogress.css\u0026#34; integrity=\u0026#34;sha384-KJyhr2syt5+4M9Pz5dipCvTrtvOmLk/olWVdfhAp858UCa64Ia5GFpTN7+G4BWpE\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; /\u0026gt; 3\u0026lt;script\u0026gt; 4 NProgress.start(); 5 document.addEventListener(\u0026#34;readystatechange\u0026#34;, () =\u0026gt; { 6 if (document.readyState === \u0026#34;interactive\u0026#34;) NProgress.inc(0.8); 7 if (document.readyState === \u0026#34;complete\u0026#34;) NProgress.done(); 8 }); 9\u0026lt;/script\u0026gt; 分类卡片添加图片 在 content\\categories 下添加分类文件夹，如 blog 。然后在文件夹里创建一个 _index.md ，输入以下内容：\n1--- 2title: \u0026#34;Blog\u0026#34; 3descript: \u0026#34;博客\u0026#34; 4image: \u0026#34;blog.png\u0026#34; 5weight: 4 6--- 其中 image 就是你在该分类文件夹下的图片名称，然后 descript 参数发现好像没什么用 🤷‍♂️\n归档页分类卡片缩放动画 参考Hugo Stack 魔改美化 - Naive Koala，在 /assets/scss/custom.scss 中加入以下代码：\n1/*-----------归档页面----------*/ 2//归档页面卡片缩放 3.article-list--tile article { 4 transition: .6s ease; 5} 6 7.article-list--tile article:hover { 8 transform: scale(1.03, 1.03); 9} 移除相关文章中的遮罩 这个相关文章的图片有一层上黑下透的遮罩，我只能说丑爆了好吗 🤷‍♂️\n按照这篇文章的做法：如何优雅的从 Hexo 转移 Blog 到 Hugo - SDLMoe，在 assets/scss/partials/layout/article.scss 中删除以下代码：\n1\u0026amp;.has-image { 2 .article-details { 3 padding: 20px; 4 background: linear-gradient(0deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.75) 100%); 5 } 6} 然后进一步移除 assets/ts/main.ts 中的 30-59 行，就搞定啦！\n樱花特效 无意间撞到【Web】博客、个人网站背景美化的几个方法（sakura / canvas-nest / particles）- 星野睡不醒这篇文章，看别人弄出来的效果有点心动呢\n只需要在主页插入：\n1\u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;https://cdn.jsdelivr.net/gh/Ukenn2112/UkennWeb@3.0/index/web.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 不过弄了之后觉得有点花里胡哨的，不确定我有哪篇帖子适合（姑且放测试帖子里吧，具体链接在上面\n返回顶部按钮 在阅读长文章时，如果想返回顶部没有快捷按钮则是很不方便的，所以添加一个按钮\n在 /layouts/partials/footer/custom.html 里面添加如下代码：\n1\u0026lt;!--返回顶部按钮 --\u0026gt; 2\u0026lt;a href=\u0026#34;#\u0026#34; id=\u0026#34;back-to-top\u0026#34; title=\u0026#34;返回顶部\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; 3 4\u0026lt;!--返回顶部CSS --\u0026gt; 5\u0026lt;style\u0026gt; 6 #back-to-top { 7 display: none; 8 position: fixed; 9 bottom: 20px; 10 right: 55px; 11 width: 55px; 12 height: 55px; 13 border-radius: 7px; 14 background-color: rgba(64, 158, 255, 0.5); 15 box-shadow: var(--shadow-l2); 16 font-size: 30px; 17 text-align: center; 18 line-height: 50px; 19 cursor: pointer; 20 } 21 22 #back-to-top:before { 23 content: \u0026#39; \u0026#39;; 24 display: inline-block; 25 position: relative; 26 top: 0; 27 transform: rotate(135deg); 28 height: 10px; 29 width: 10px; 30 border-width: 0 0 2px 2px; 31 border-color: var(--back-to-top-color); 32 border-style: solid; 33 } 34 35 #back-to-top:hover:before { 36 border-color: #2674e0; 37 } 38 39 /* 在屏幕宽度小于 768 像素时，钮位置调整 */ 40 @media screen and (max-width: 768px) { 41 #back-to-top { 42 bottom: 20px; 43 right: 20px; 44 width: 40px; 45 height: 40px; 46 font-size: 10px; 47 } 48 } 49 50 /* 在屏幕宽度大于等于 1024 像素时，按钮位置调整 */ 51 @media screen and (min-width: 1024px) { 52 #back-to-top { 53 bottom: 20px; 54 right: 40px; 55 } 56 } 57 58 /* 在屏幕宽度大于等于 1280 像素时，按钮位置调整 */ 59 @media screen and (min-width: 1280px) { 60 #back-to-top { 61 bottom: 20px; 62 right: 55px; 63 } 64 } 65 66 /* 目录显示时，隐藏按钮 */ 67 @media screen and (min-width: 1536px) { 68 #back-to-top { 69 visibility: hidden; 70 } 71 } 72\u0026lt;/style\u0026gt; 73 74\u0026lt;!--返回顶部JS --\u0026gt; 75\u0026lt;script\u0026gt; 76 function backToTop() { 77 document.documentElement.scrollIntoView({ 78 behavior: \u0026#39;smooth\u0026#39;, 79 }) 80 } 81 82 window.onload = function () { 83 let scrollTop = 84 this.document.documentElement.scrollTop || this.document.body.scrollTop 85 let totopBtn = this.document.getElementById(\u0026#39;back-to-top\u0026#39;) 86 if (scrollTop \u0026gt; 0) { 87 totopBtn.style.display = \u0026#39;inline\u0026#39; 88 } else { 89 totopBtn.style.display = \u0026#39;none\u0026#39; 90 } 91 } 92 93 window.onscroll = function () { 94 let scrollTop = 95 this.document.documentElement.scrollTop || this.document.body.scrollTop 96 let totopBtn = this.document.getElementById(\u0026#39;back-to-top\u0026#39;) 97 if (scrollTop \u0026lt; 200) { 98 totopBtn.style.display = \u0026#39;none\u0026#39; 99 } else { 100 totopBtn.style.display = \u0026#39;inline\u0026#39; 101 totopBtn.addEventListener(\u0026#39;click\u0026#39;, backToTop, false) 102 } 103 } 104\u0026lt;/script\u0026gt; 添加右下角联系小气泡按钮 是一个日本公司提供的服务Channel.io，网页气泡是个入口，实际聊天可以在它们的App里完成。配置过程和山茶花舍说的一样，在官网注册完之后，点击小齿轮 – General – Manage Plug-in – Install plug-in – 点击JavaScript并复制框里的代码，粘贴到 layouts/partials/footer/custom.html 就可以啦。具体可以参考这篇博客：\nHugo Stack主题装修笔记 - 第三夏尔 | Third Shire\n如果嫌刚开始弹出来的那个气泡很烦的话，可以在设置里把他关掉：\n图片轮播 照抄大佬的博客Hugo | 在文章中插入轮播图片 - 小球飞鱼，在 layout/shortcodes 文件夹中创建 imgloop.html 短代码模板：\n1{{ if .Site.Params.enableimgloop }} 2 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/css/swiper.min.css\u0026#34;\u0026gt; 3 \u0026lt;!-- Swiper --\u0026gt; 4 \u0026lt;div class=\u0026#34;swiper-container\u0026#34;\u0026gt; 5 \u0026lt;div class=\u0026#34;swiper-wrapper\u0026#34;\u0026gt; 6 {{$itItems := split (.Get 0) \u0026#34;,\u0026#34;}} 7 {{range $itItems }} 8 \u0026lt;div class=\u0026#34;swiper-slide\u0026#34;\u0026gt; 9 \u0026lt;img src=\u0026#34;{{.}}\u0026#34; alt=\u0026#34;\u0026#34;\u0026gt; 10 \u0026lt;/div\u0026gt; 11 {{end}} 12 \u0026lt;/div\u0026gt; 13 \u0026lt;!-- Add Pagination --\u0026gt; 14 \u0026lt;div class=\u0026#34;swiper-pagination\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 15 \u0026lt;/div\u0026gt; 16 17 \u0026lt;script src=\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/js/swiper.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 18 \u0026lt;!-- Initialize Swiper --\u0026gt; 19 \u0026lt;script\u0026gt; 20 var swiper = new Swiper(\u0026#39;.swiper-container\u0026#39;, { 21 pagination: \u0026#39;.swiper-pagination\u0026#39;, 22 paginationClickable: true, 23 //自动调节高度 24 autoHeight: true, 25 //键盘左右方向键控制 26 keyboardControl : true, 27 //鼠标滑轮控制 28 mousewheelControl : true, 29 //自动切换 30 //autoplay : 5000, 31 //懒加载 32 lazyLoading : true, 33\tlazyLoadingInPrevNext : true, 34\t//无限循环 35\tloop : true, 36 }); 37 \u0026lt;/script\u0026gt; 38{{ end }} 在 assets/scss/cutom.scss 中加入如下代码：\n1.swiper-container { 2 max-width: 820px; 3 margin: 2em auto; 4 5} 6.swiper-slide { 7 text-align: center; 8 font-size: 18px; 9 background-color: #fff; 10 /* Center slide text vertically */ 11 display: flex; 12 justify-content: center; 13 align-items: center; 14 img { 15 margin: 0 !important; 16 } 17} 最后在 hugo.yaml 的 params 配置下加入 enableimgloop: true就可以啦！\n鼠标烟花特效 参考Hugo | 记录MemE主题美化过程 - Zoe\u0026rsquo;s Dumpster.，在 layouts/partials/footer/components/script.html 最后加入以下代码：\n1\u0026lt;!--鼠标点击特效，烟花效应--\u0026gt; 2\u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/gh/ZhaoUncle/image@main/static/mouse-click.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 3\u0026lt;canvas width=\u0026#34;1777\u0026#34; height=\u0026#34;841\u0026#34; style=\u0026#34;position: fixed; left: 0px; top: 0px; z-index: 2147483647; pointer-events: none;\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; 固定代码块高度 参考hugo stack 主题美化 - Yelle🦋 ，把以下内容添加到 assets/scss/partials/article.scss ：\n1 .article-content { 2 .highlight { 3 padding: var(--card-padding); 4 pre { 5 margin: initial; 6 padding: var(--card-padding); 7 margin: 0; 8 width: auto; 9 max-height: 20em; 10 scrollbar-width: none; 11 /* Firefox */ 12 \u0026amp;::-webkit-scrollbar { 13 display: auto; 14 /* Chrome Safari */ 15 } 16 } 17 } 18 } 但是我发现代码块有个问题：行号和代码都有滚动条。这怎么回事呢？查来查去翻来覆去踌躇了三天，又问了ai也搞不定。最后才发现代码块与行号居然是分开滚动的。还有这种事？迅速找到 hugo.yaml 的配置里有这么个参数：\n1markup: 2 highlight: 3 lineNumbersInTable: true 4 # lineNumbersInTable：使用表来格式化行号和代码, 而不是标签。这个属性一般设置为 true. 把这个 lineNumbersInTable 改成false后，代码块与行号就同步了，行号的滚动条已经消失了。然而新的问题又出现了：复制代码的时候行号也会被带上，鼠标点击选中的时候行号也会被选进去。找到了解决办法：\n小白hugo博客装修笔记（2）- B1ain\u0026rsquo;s Blog\n1.首先解决手动选中内容复制带行号的问题\n在 /assets/scss/custom.scss 文件中添加如下内容，将行号设定为不可选中\n1\t// 禁止复制行号 2 .highlight .ln { 3 user-select: none; 4 } 2.解决copy按钮复制带行号的问题\n修改 /assets/ts/main.ts 文件中的复制按钮逻辑：\n1highlights.forEach(highlight =\u0026gt; { 2 const copyButton = document.createElement(\u0026#39;button\u0026#39;); 3 copyButton.innerHTML = copyText; 4 copyButton.classList.add(\u0026#39;copyCodeButton\u0026#39;); 5 highlight.appendChild(copyButton); 6 7 const codeBlock = highlight.querySelector(\u0026#39;code[data-lang]\u0026#39;); 8 if (!codeBlock) return; 9 10 copyButton.addEventListener(\u0026#39;click\u0026#39;, () =\u0026gt; { 11 // 创建一个临时容器来克隆代码块的内容 12 const tempCodeBlock = codeBlock.cloneNode(true) as HTMLElement; 13 14 // 删除行号，行号的元素是 \u0026lt;span class=\u0026#34;ln\u0026#34;\u0026gt; 15 const lineNumbers = tempCodeBlock.querySelectorAll(\u0026#39;.ln\u0026#39;); 16 lineNumbers.forEach(lineNumber =\u0026gt; lineNumber.remove()); 17 18 // 获取没有行号的纯文本内容 19 const codeText = tempCodeBlock.textContent; 20 21 navigator.clipboard.writeText(codeText || \u0026#39;\u0026#39;) 22 // navigator.clipboard.writeText(codeBlock.textContent) 23 .then(() =\u0026gt; { 24 copyButton.textContent = copiedText; 25 26 setTimeout(() =\u0026gt; { 27 copyButton.textContent = copyText; 28 }, 1000); 29 }) 30 .catch(err =\u0026gt; { 31 alert(err) 32 console.log(\u0026#39;Something went wrong\u0026#39;, err); 33 }); 34 }); 35}); 什么乱七八糟的bug\n文字统计 在 layouts/partials/footer/footer.html 里增加以下代码，参考：\n小白hugo博客装修笔记（1）- B1ain\u0026rsquo;s Blog\n1\u0026lt;!-- Add total page and word count time --\u0026gt; 2\u0026lt;section class=\u0026#34;totalcount\u0026#34;\u0026gt; 3 {{$scratch := newScratch}} 4 {{ range (where .Site.Pages \u0026#34;Kind\u0026#34; \u0026#34;page\u0026#34; )}} 5 {{$scratch.Add \u0026#34;total\u0026#34; .WordCount}} 6 {{ end }} 7 📝{{$scratch.Get \u0026#34;total\u0026#34; }}字 · 8 📖{{ len (where .Site.RegularPages \u0026#34;Section\u0026#34; \u0026#34;post\u0026#34;) }}篇文章 9\u0026lt;/section\u0026gt; 访客量统计 在 layouts/partials/footer/footer.html 里第一部分 \u0026lt;footer class=\u0026quot;site-footer\u0026quot;\u0026gt; 中的 \u0026lt;section class=\u0026quot;powerby\u0026quot;\u0026gt; 最后参考hugo+Stack 搭建个人博客 - Hyrtee\u0026rsquo;s Blog，增加以下代码：\n1\u0026lt;!-- insert busuanzi --\u0026gt; 2{{ if .Site.Params.busuanzi.enable -}} 3\u0026lt;div class=\u0026#34;busuanzi-footer\u0026#34;\u0026gt; 4\u0026lt;span id=\u0026#34;busuanzi_container_site_pv\u0026#34;\u0026gt; 5 本站总访问量\u0026lt;span id=\u0026#34;busuanzi_value_site_pv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;次 6\u0026lt;/span\u0026gt; 7\u0026lt;span id=\u0026#34;busuanzi_container_site_uv\u0026#34;\u0026gt; 8 本站访客数\u0026lt;span id=\u0026#34;busuanzi_value_site_uv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;人次 9\u0026lt;/span\u0026gt; 10\u0026lt;/div\u0026gt; 11{{- end -}} 预览的时候统计数据会很夸张，不过部署之后就会显示真实的数据了。\n热力图 热力图 由 椒盐豆豉 设计，由 Yelle 改进。新建 layouts/shortcodes/heatmap.html：\n1\u0026lt;div id=\u0026#34;heatmap\u0026#34; style=\u0026#34; 2 max-width: 1900px; 3 height: 180px; 4 padding: 2px; 5 text-align: center; 6 \u0026#34; 7\u0026gt;\u0026lt;/div\u0026gt; 8\u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/echarts@5.3.0/dist/echarts.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 9\u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; 10 var chartDom = document.getElementById(\u0026#39;heatmap\u0026#39;); 11 var myChart = echarts.init(chartDom); 12 window.onresize = function() { 13 myChart.resize(); 14 }; 15 var option; 16 17 // echart heatmap data seems to only support two elements tuple 18 // it doesn\u0026#39;t render when each item has 3 value 19 // it also only pass first 2 elements when reading event param 20 // so here we build a map to store additional metadata like link and title 21 // map format {date: [{wordcount, link, title}]} 22 // for more information see https://blog.douchi.space/hugo-blog-heatmap 23 var dataMap = new Map(); 24 {{ range ((where .Site.RegularPages \u0026#34;Type\u0026#34; \u0026#34;post\u0026#34;)) }} 25 var key = {{ .Date.Format \u0026#34;2006-01-02\u0026#34; }}; 26 var value = dataMap.get(key); 27 var wordCount = {{ .WordCount }}; 28 var link = {{ .RelPermalink}}; 29 var title = {{ .Title }}; 30 31 // multiple posts in same day 32 if (value == null) { 33 dataMap.set(key, [{wordCount, link, title}]); 34 } else { 35 value.push({wordCount, link, title}); 36 } 37 {{- end -}} 38 39 var data = []; 40 // sum up the word count 41 for (const [key, value] of dataMap.entries()) { 42 var sum = 0; 43 for (const v of value) { 44 sum += v.wordCount; 45 } 46 data.push([key, (sum / 1000).toFixed(1)]); 47 } 48 49 var startDate = new Date(); 50 var year_Mill = startDate.setFullYear((startDate.getFullYear() - 1)); 51 var startDate = +new Date(year_Mill); 52 var endDate = +new Date(); 53 54 var dayTime = 3600 * 24 * 1000; 55 startDate = echarts.format.formatTime(\u0026#39;yyyy-MM-dd\u0026#39;, startDate); 56 endDate = echarts.format.formatTime(\u0026#39;yyyy-MM-dd\u0026#39;, endDate); 57 58 // change date range according to months we want to render 59 function heatmap_width(months){ 60 var startDate = new Date(); 61 var mill = startDate.setMonth((startDate.getMonth() - months)); 62 var endDate = +new Date(); 63 startDate = +new Date(mill); 64 65 endDate = echarts.format.formatTime(\u0026#39;yyyy-MM-dd\u0026#39;, endDate); 66 startDate = echarts.format.formatTime(\u0026#39;yyyy-MM-dd\u0026#39;, startDate); 67 68 var showmonth = []; 69 showmonth.push([ 70 startDate, 71 endDate 72 ]); 73 return showmonth 74 }; 75 76 function getRangeArr() { 77 const windowWidth = window.innerWidth; 78 if (windowWidth \u0026gt;= 600) { 79 return heatmap_width(12); 80 } else if (windowWidth \u0026gt;= 400) { 81 return heatmap_width(9); 82 } else { 83 return heatmap_width(6); 84 } 85 } 86 87 option = { 88 title: { 89 top: 0, 90 left: \u0026#39;center\u0026#39;, 91 text: \u0026#39;博客热力图\u0026#39; 92 }, 93 tooltip: { 94 hideDelay: 1000, 95 enterable: true, 96 formatter: function (p) { 97 const date = p.data[0]; 98 const posts = dataMap.get(date); 99 var content = `${date}`; 100 for (const [i, post] of posts.entries()) { 101 content += \u0026#34;\u0026lt;br\u0026gt;\u0026#34;; 102 var link = post.link; 103 var title = post.title; 104 var wordCount = (post.wordCount / 1000).toFixed(1); 105 content += `\u0026lt;a href=\u0026#34;${link}\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;${title} | ${wordCount} k\u0026lt;/a\u0026gt;` 106 } 107 return content; 108 } 109 }, 110 visualMap: { 111 min: 0, 112 max: 10, 113 type: \u0026#39;piecewise\u0026#39;, 114 orient: \u0026#39;horizontal\u0026#39;, 115 left: \u0026#39;center\u0026#39;, 116 top: 30, 117 118 inRange: { 119 // [floor color, ceiling color] 120 color: [\u0026#39;#7aa8744c\u0026#39;, \u0026#39;#7AA874\u0026#39; ] 121 }, 122 splitNumber: 4, 123 text: [\u0026#39;千字\u0026#39;, \u0026#39;\u0026#39;], 124 showLabel: true, 125 itemGap: 20, 126 }, 127 calendar: { 128 top: 80, 129 left: 20, 130 right: 4, 131 cellSize: [\u0026#39;auto\u0026#39;, 13], 132 range: getRangeArr(), 133 itemStyle: { 134 color: \u0026#39;#F1F1F1\u0026#39;, 135 borderWidth: 1.5, 136 borderColor: \u0026#39;#fff\u0026#39;, 137 }, 138 yearLabel: { show: false }, 139 // the splitline between months. set to transparent for now. 140 splitLine: { 141 lineStyle: { 142 color: \u0026#39;rgba(0, 0, 0, 0.0)\u0026#39;, 143 // shadowColor: \u0026#39;rgba(0, 0, 0, 0.5)\u0026#39;, 144 // shadowBlur: 5, 145 // width: 0.5, 146 // type: \u0026#39;dashed\u0026#39;, 147 } 148 } 149 }, 150 series: { 151 type: \u0026#39;heatmap\u0026#39;, 152 coordinateSystem: \u0026#39;calendar\u0026#39;, 153 data: data, 154 } 155 }; 156 myChart.setOption(option); 157 myChart.on(\u0026#39;click\u0026#39;, function(params) { 158 if (params.componentType === \u0026#39;series\u0026#39;) { 159 // open the first post on the day 160 const post = dataMap.get(params.data[0])[0]; 161 const link = window.location.origin + post.link; 162 window.open(link, \u0026#39;_blank\u0026#39;).focus(); 163 } 164}); 165\u0026lt;/script\u0026gt; 然后我发现在暗黑模式下颜色显示会有点问题，于是我在 text: '博客热力图', 的下面加了一段代码改了一下颜色：\n1textStyle: { 2 color: \u0026#39;#c0a3e5\u0026#39; 3} 感觉加载热力图的时候有点慢，干脆把 https://cdn.jsdelivr.net/npm/echarts@5.3.0/dist/echarts.min.js 下载下来放在 static/js 文件夹里，然后把 \u0026lt;script src=\u0026quot;.../echarts.min.js\u0026quot;\u0026gt;\u0026lt;/script\u0026gt; 改成：\n1\u0026lt;script src=\u0026#34;/js/echarts.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 聊天气泡也能这么处理哦！\n自定义emoji 又是大神的文章：Hugo | 为博客添加FF14表情包 - 小球飞鱼。首先建立 static/emoji 文件夹，之后在 layouts/shortcodes 下建立 emoji.html 模板，写入如下内容：\n1{{ $name := .Get \u0026#34;name\u0026#34; }} 2\u0026lt;img 3 src=\u0026#34;/emoji/{{ $name }}.{{ with .Get \u0026#34;ext\u0026#34; }}{{ . }}{{ else }}webp{{ end }}\u0026#34; 4 title=\u0026#34;{{ with .Get \u0026#34;title\u0026#34; }}{{ . }}{{ else }}{{ $name }}{{ end }}\u0026#34; 5 alt=\u0026#34;{{ with .Get \u0026#34;alt\u0026#34; }}{{ . }}{{ else }}{{ $name }}{{ end }}\u0026#34; 6 {{ with .Get \u0026#34;width\u0026#34; }} width=\u0026#34;{{ . }}\u0026#34;{{ end }} 7 {{ with .Get \u0026#34;height\u0026#34; }} height=\u0026#34;{{ . }}\u0026#34;{{ end }} 8/\u0026gt; 模板中限定图片默认后缀为webp，如果使用webp之外格式的图片，就需要写明ext=\u0026ldquo;格式名\u0026rdquo;。alt 参数在HTML的 \u0026lt;img\u0026gt; 标签中用于提供图像的替代文本描述。它的作用和用途包括：\n无障碍性 - 当用户使用屏幕阅读器等辅助技术浏览网页时，屏幕阅读器会朗读alt文本，帮助视障用户理解图像内容。\n图像无法加载时的显示 - 当图像因网络问题、链接错误等原因无法加载时，浏览器会显示alt文本代替图像。\nSEO优化 - 搜索引擎使用alt文本来理解图像内容，有助于提高网页的搜索引擎优化。\n符合Web标准 - HTML规范要求所有img标签都应该有alt属性，以符合Web内容无障碍指南(WCAG)。\n在模板中，alt参数默认使用 $name 变量的值，但也可以通过 .Get \u0026quot;alt\u0026quot; 获取自定义的alt文本。\n然后我发现多个emoji表情与文字并排的时候不在一个水平线上，很膈应，于是修改 assets/scss/custom.scss，在最后加上：\n1.article-page .main-article .article-content img[width$=\u0026#34;36\u0026#34;], 2.article-page .main-article .article-content img[width$=\u0026#34;30\u0026#34;], 3.article-page .main-article .article-content img[width$=\u0026#34;40\u0026#34;], 4.article-page .main-article .article-content img[width$=\u0026#34;48\u0026#34;], 5.article-page .main-article .article-content img[width$=\u0026#34;52\u0026#34;], 6.article-page .main-article .article-content img[width$=\u0026#34;60\u0026#34;], 7.article-page .main-article .article-content img[width$=\u0026#34;72\u0026#34;], 8.article-page .main-article .article-content img[width=\u0026#34;100\u0026#34;] { 9 margin-top: -5px; 10 border-radius: 13%; 11 display: inline-block; 12 vertical-align: middle; 13} 要注意限制 emoji 的设置尺寸，不然大图片也会沿用这个设置。咋一看有点像屎山代码，凑近一看确实是欸！算了，又不是不能用，先这么用着 应用：\n1{{\u0026lt; emoji name=\u0026#34;lazy\u0026#34; width=\u0026#34;60\u0026#34; height=\u0026#34;60\u0026#34; title=\u0026#34;lazy\u0026#34; \u0026gt;}} 示例： 彩蛋 顺便弄个脚本获取 static/emoji/ 下的所有文件：\n1import os 2 3def generate_emoji_file_list(): 4 # 获取当前目录下的所有文件 5 files = [f for f in os.listdir(\u0026#39;.\u0026#39;) if os.path.isfile(f)] 6 7 # 排序文件名（可选） 8 files.sort() 9 10 # 创建格式化的文本 11 formatted_text = \u0026#34;\u0026#34; 12 for i, file in enumerate(files, 1): 13 formatted_text += f\u0026#34;emoji{i}: \\\u0026#34;/emoji/{file}\\\u0026#34;,\\n\u0026#34; 14 15 # 将格式化的文本保存到文件 16 with open(\u0026#34;emoji_files.txt\u0026#34;, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: 17 f.write(formatted_text) 18 19 print(f\u0026#34;已成功保存 {len(files)} 个文件名到 emoji_files.txt\u0026#34;) 20 21if __name__ == \u0026#34;__main__\u0026#34;: 22 generate_emoji_file_list() 可以把表情放到碎碎念页面里去呢 ~\n玩转短代码 参考了大神的总结：Hugo|在Stack主题上可行的短代码们 - 眠于水月间\n以下是新增的显示效果 好喜欢蓝色！\n这个短代码只在电脑端生效\n一些手动打码效果！\n但总之换行的话就加个空标签。\n数据删除！数据删除！\n但总之换行的话就加个空标签。\n点击显示 让我看看！ 我挑的配色很好看吧！\n好喜欢蓝色（再次）（再次）\n但总之换行的话就加个空标签。\n文字居左\n文字居中\n文字居右\nCtrl+Alt+Del |\nCtrl+Alt+Del 这个原来的键盘样式确实有点丑呢\n可以在这里插入链接假装是卡片式链接。\n好像不能插入图片？\n好像是的并不是 链接标题 链接内容描述 可以当标签用？ Warning：需要双括号。\ninfo：这是一条信息。\nnote：可以标注一下，但是没必要。\ntip：在示例里胡说八道会使观看者会心一笑。\n2024 past First Time 我是萌新 2025 now Second Time 我还是萌新 瀑布流图片 嵌入PDF 《H庄园的午餐》 [英] 阿加莎·克里斯蒂 \u0026lt; 9.0 \u003e 一个英国版的红玫瑰与白玫瑰的故事。年轻美丽的埃莉诺•卡莱尔平静地站在被告席上。她是H庄园女主人韦尔曼太太的侄女，被控谋杀了她的情敌——H庄园门房的女儿玛丽•杰拉德。证据确凿：埃莉诺准备了那份致命午餐，也只有她拥有作案动机和时机。然而，在那个充满敌意的法庭上，只有一个人依然认为埃莉诺直到被证明有罪之前是清白的。赫尔克里•波洛挡在了埃莉诺和绞刑台之间…… book John Doe\u0026nbsp;\u0026nbsp;\u0026nbsp;2023-09-12 14:30 这是左边的消息内容。 2023-09-12 14:45\u0026nbsp;\u0026nbsp;\u0026nbsp;Alice 这是右边的消息内容，测试长长长长长长长长长长长长长长长长长长长长长长长长度。 发现聊天气泡与图片轮播都不能折叠起来，不然就会报错。找了一圈都没找到解决办法，就这样吧 ，又不是不能用\n打分短代码（支持半颗星） 大神们给出的打分短代码只能按总分十颗星、一颗一颗星去打分，感觉有点太累赘，而且那个星星图案也不好看:\n那就参考前人的智慧魔改一下吧。新建 layouts/shortcodes/rate.html ，在里面写入：\n1\u0026lt;div class=\u0026#34;db-card-subject\u0026#34;\u0026gt; 2 \u0026lt;div class=\u0026#34;db-card-content\u0026#34;\u0026gt; 3 \u0026lt;div class=\u0026#34;rating\u0026#34;\u0026gt; 4 \u0026lt;span class=\u0026#34;description\u0026#34;\u0026gt;{{ .Get \u0026#34;description\u0026#34; }}\u0026lt;/span\u0026gt; 5 \u0026lt;span class=\u0026#34;rating_nums\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;span class=\u0026#34;allstardark\u0026#34;\u0026gt;\u0026lt;span class=\u0026#34;allstarlight\u0026#34; style=\u0026#34;width:{{ .Get \u0026#34;rate\u0026#34; }}0%\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;span class=\u0026#34;rating_float\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;/span\u0026gt; 6 \u0026lt;span class=\u0026#34;rating_float_num\u0026#34;\u0026gt;{{ .Get \u0026#34;rate_float\u0026#34; }}\u0026lt;/span\u0026gt; 7 \u0026lt;/div\u0026gt; 8 \u0026lt;/div\u0026gt; 9\u0026lt;/div\u0026gt; 在 assets/scss/custom.scss 后面加入：\n1/* db-card -------- start*/ 2.db-card{margin:2.5rem 3rem;background:var(--card-background);border-radius: 7px;box-shadow: 0 6px 10px 0 #00000053;} 3.db-card-subject{display: flex;align-items:flex-start;line-height:1.6;padding:12px;position:relative;} 4.dark .db-card{background:var(--card-background);} 5.db-card-content {flex:1 1 auto;} 6.db-card-post {width: 100px;margin-right: 15px;display: flex;flex: 0 0 auto;} 7.db-card-title {margin-bottom: 3px;font-size: 14px;color: var(--card-text-color-main);;} 8.db-card-title a{text-decoration: none!important} 9.db-card-abstract,.db-card-comment{font-size:13px;overflow: auto;max-height:10rem;color: var(--card-text-color-main);;} 10.db-card-cate{position: absolute;top:0;right:0;background:#f99b01;padding:1px 8px;font-size:small;font-style:italic;border-radius:0 8px 0 8px;text-transform:capitalize;} 11.db-card-post img{width: 100px!important;height: 150px!important;border-radius: 4px;-o-object-fit: cover;object-fit: cover;} 12.rating{margin: 0 0 5px;font-size:13px;line-height: 1;display: flex;align-items: center;} 13.rating .allstardark{position:relative;color: #f99b01;height: 16px;width: 80px;background-size: auto 100%;margin-right: 8px;background-repeat: repeat;background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==); 14} 15.rating .allstarlight{position: absolute;left: 0;color: #f99b01;height:16px;overflow: hidden;background-size: auto 100%;background-repeat: repeat;background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);} 16@media (max-width:550px) { 17\t.db-card{margin:0.8rem 1rem;} 18\t.db-card-comment{display: none;} 19} 20/* db-card -------- end */ 21 22/* Add this to your custom CSS file */ 23.rating_score { 24 margin-left: 10px; 25 color: #ff9900; 26 font-weight: bold; 27} 28 29.description { 30 margin-right: 10px; /* Adds space between description and stars */ 31 display: inline-block; /* Ensures margin works properly */ 32 font-size: 16px; /* Increases font size */ 33} 34 35/* Increase star size */ 36.allstardark { 37 display: inline-block; 38 width: 100px; /* Increased from 75px */ 39 height: 10px; /* Increased from 15px */ 40 background: url(\u0026#39;/images/star-empty.png\u0026#39;) repeat-x; 41 background-size: 10px 10px; /* Increased from 15px 15px */ 42 position: relative; 43 vertical-align: middle; /* Aligns stars with text */ 44} 45 46.allstarlight { 47 display: inline-block; 48 height: 10px; /* Increased from 15px */ 49 background: url(\u0026#39;/images/star-filled.png\u0026#39;) repeat-x; 50 background-size: 10px 10px; /* Increased from 15px 15px */ 51 position: absolute; 52 top: 0; 53 left: 0; 54} 55 56/* Style the rating score */ 57.rating_float_num { 58 margin-left: 2px; /* Add space after stars */ 59 font-size: 16px; /* Match font size */ 60 vertical-align: middle; /* Align with stars */ 61 line-height: 0; 62} 63 64.rating .allstardark{position:relative;color: #f99b01;height: 16px;width: 80px;background-size: auto 100%;margin-right: 8px;background-repeat: repeat;background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==); 65} 66.rating .allstarlight{position: absolute;left: 0;color: #f99b01;height:16px;overflow: hidden;background-size: auto 100%;background-repeat: repeat;background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);} 最后效果：\nClaude 8.5 ChatGPT 7.2 使用：\n1//实际使用时应为双括号 2{\u0026lt; rate rate=9 rate_float=\u0026#34;8.5\u0026#34; description=\u0026#34;Claude\u0026#34; \u0026gt;} 3 4{\u0026lt; rate rate=7 rate_float=\u0026#34;7.2\u0026#34; description=\u0026#34;ChatGPT\u0026#34; \u0026gt;} 写在最后 参考链接 首推非常详尽的 Hugo 介绍：\nHUGO - 风月 Vercel 部署 - Waline\nhugo换行 - Bboysoul’s Blog\nHugo系列(3.1) - LoveIt主题美化与博客功能增强 · 第二章 - 雨临Lewis\n博客切换到STACK 主题 - 一不留神的博客\nHugo Stack 魔改美化 - Naive Koala\n如何优雅的从 Hexo 转移 Blog 到 Hugo - SDLMoe\n【Web】博客、个人网站背景美化的几个方法（sakura / canvas-nest / particles）- 星野睡不醒\n给 Hugo 加一点好玩的功能 - 山茶花舍\nHugo Stack主题装修笔记 - 第三夏尔 | Third Shire\nHugo | 在文章中插入轮播图片 - 小球飞鱼\nHugo | 记录MemE主题美化过程 - Zoe’s Dumpster.\nhugo stack 主题美化 - Yelle🦋\n小白hugo博客装修笔记（2）- B1ain’s Blog\n小白hugo博客装修笔记（1）- B1ain’s Blog\nhugo+Stack 搭建个人博客 - Hyrtee’s Blog\n如何给 Hugo 博客添加热力图 - 椒盐豆豉\nhugo stack 主题美化 2 - Yelle🦋\nHugo|在Stack主题上可行的短代码们 - 眠于水月间\n碎碎念 刚开始这篇文章只是记录一下我为博客添加了评论系统和灯箱，后面装修的东西就越来越多了，什么多语言、字体、文章加密等等这些，都是我刚开始就想搞但一直没搞定的东西。花了大半个月终于把博客装修的差不多了，折腾一下还是挺有乐趣的，有几天为了解决文章加密和多语言的问题还熬了夜。（虽然最后也不是我解决的呜呜呜太菜了）为了我这破破的博客也是值得的吧，暂时应该不会大改了。刚好这几天天气不错，出去耍耍，走！\n","date":"2025-02-28T16:46:21+08:00","image":"https://langminjeii.ifantic.de/post/meihua-1.png","permalink":"https://langminjeii.ifantic.de/post/hugo-stack%E3%81%AE%E7%BE%8E%E5%8C%96/","title":"Hugo-stackの美化"},{"content":"项目地址：\nhttps://github.com/vastsa/FileCodeBox\n前期准备：\n一个cloudflare账号 一个huggingface账号 创建dockerfile 注册好huggingface后，点击头像新建一个空间：\n选择用docker部署：\n然后划到最底，点击左下角的Create Space， 新建一个dockerfile\n填入以下内容，然后点击Commit new file to main 1FROM lanol/filecodebox:beta 2 3RUN apt-get update \u0026amp;\u0026amp; apt-get install -y \\ 4 python3-pip \\ 5 git \\ 6 \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* 7 8RUN pip3 install --no-cache-dir huggingface_hub datasets 9 10RUN useradd -m -u 1000 user 11 12WORKDIR /app 13 14ENV HOME=/home/user \\ 15 PATH=/home/user/.local/bin:$PATH \\ 16 HF_HOME=/app/data/hf_cache \\ 17 PYTHONUNBUFFERED=1 18 19RUN mkdir -p /app/data \u0026amp;\u0026amp; \\ 20 chown -R user:user /app/data 21 22COPY sync_data.sh /app/ 23RUN chmod +x /app/sync_data.sh \u0026amp;\u0026amp; \\ 24 chown user:user /app/sync_data.sh 25 26USER user 27 28EXPOSE 12345 29 30ENTRYPOINT [\u0026#34;/app/sync_data.sh\u0026#34;] 创建sync_data.sh文件 1#!/bin/bash 2 3# 检查环境变量 4if [ -z \u0026#34;$HF_TOKEN\u0026#34; ] || [ -z \u0026#34;$DATASET_ID\u0026#34; ]; then 5 echo \u0026#34;Starting without backup functionality - missing HF_TOKEN or DATASET_ID\u0026#34; 6 exec python main.py 7fi 8 9# 登录HuggingFace (使用环境变量方式避免交互问题) 10export HUGGING_FACE_HUB_TOKEN=$HF_TOKEN 11 12# 同步函数 13sync_data() { 14 while true; do 15 echo \u0026#34;Starting sync process at $(date)\u0026#34; 16 17 # 创建临时压缩文件 18 cd /app 19 timestamp=$(date +%Y%m%d_%H%M%S) 20 backup_file=\u0026#34;backup_${timestamp}.tar.gz\u0026#34; 21 22 tar -czf \u0026#34;/tmp/${backup_file}\u0026#34; data/ 23 24 python3 -c \u0026#34; 25from huggingface_hub import HfApi 26import os 27def manage_backups(api, repo_id, max_files=50): 28 files = api.list_repo_files(repo_id=repo_id, repo_type=\u0026#39;dataset\u0026#39;) 29 backup_files = [f for f in files if f.startswith(\u0026#39;backup_\u0026#39;) and f.endswith(\u0026#39;.tar.gz\u0026#39;)] 30 backup_files.sort() 31 32 if len(backup_files) \u0026gt;= max_files: 33 files_to_delete = backup_files[:(len(backup_files) - max_files + 1)] 34 for file_to_delete in files_to_delete: 35 try: 36 api.delete_file(path_in_repo=file_to_delete, repo_id=repo_id, repo_type=\u0026#39;dataset\u0026#39;) 37 print(f\u0026#39;Deleted old backup: {file_to_delete}\u0026#39;) 38 except Exception as e: 39 print(f\u0026#39;Error deleting {file_to_delete}: {str(e)}\u0026#39;) 40try: 41 api = HfApi() 42 api.upload_file( 43 path_or_fileobj=\u0026#39;/tmp/${backup_file}\u0026#39;, 44 path_in_repo=\u0026#39;${backup_file}\u0026#39;, 45 repo_id=\u0026#39;${DATASET_ID}\u0026#39;, 46 repo_type=\u0026#39;dataset\u0026#39; 47 ) 48 print(\u0026#39;Backup uploaded successfully\u0026#39;) 49 50 manage_backups(api, \u0026#39;${DATASET_ID}\u0026#39;) 51except Exception as e: 52 print(f\u0026#39;Backup failed: {str(e)}\u0026#39;) 53\u0026#34; 54 # 清理临时文件 55 rm -f \u0026#34;/tmp/${backup_file}\u0026#34; 56 57 # 设置同步间隔 58 SYNC_INTERVAL=${SYNC_INTERVAL:-7200} 59 echo \u0026#34;Next sync in ${SYNC_INTERVAL} seconds...\u0026#34; 60 sleep $SYNC_INTERVAL 61 done 62} 63 64# 恢复函数 65restore_latest() { 66 echo \u0026#34;Attempting to restore latest backup...\u0026#34; 67 python3 -c \u0026#34; 68try: 69 from huggingface_hub import HfApi 70 import os 71 72 api = HfApi() 73 files = api.list_repo_files(\u0026#39;${DATASET_ID}\u0026#39;, repo_type=\u0026#39;dataset\u0026#39;) 74 backup_files = [f for f in files if f.startswith(\u0026#39;backup_\u0026#39;) and f.endswith(\u0026#39;.tar.gz\u0026#39;)] 75 76 if backup_files: 77 latest = sorted(backup_files)[-1] 78 api.hf_hub_download( 79 repo_id=\u0026#39;${DATASET_ID}\u0026#39;, 80 filename=latest, 81 repo_type=\u0026#39;dataset\u0026#39;, 82 local_dir=\u0026#39;/tmp\u0026#39; 83 ) 84 os.system(f\u0026#39;tar -xzf /tmp/{latest} -C /app\u0026#39;) 85 os.remove(f\u0026#39;/tmp/{latest}\u0026#39;) 86 print(f\u0026#39;Restored from {latest}\u0026#39;) 87 else: 88 print(\u0026#39;No backup found\u0026#39;) 89except Exception as e: 90 print(f\u0026#39;Restore failed: {str(e)}\u0026#39;) 91\u0026#34; 92} 93 94# 主程序 95( 96 # 尝试恢复 97 restore_latest 98 99 # 启动同步进程 100 sync_data \u0026amp; 101 102 # 启动主应用 103 exec python main.py 104) 2\u0026gt;\u0026amp;1 | tee -a /app/data/backup.log 然后划到最底，点击左下角的Commit new file to main\n修改README.md 跟上面创建新文件一样，新建README.md文件，填入：\n1--- 2title: Filebox 3emoji: 🚀 4colorFrom: pink 5colorTo: pink 6sdk: docker 7pinned: false 8app_port: 12345 9--- 10 11Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference 创建dataset数据库 记住你的数据库地址，格式：huggingface用户名称/dataset名称\n创建HF-token 划到最底下点击Create token保存并记住你的token\n在后台添加环境变量 查看你的dataset 对应填入 DATASET_ID\nHF_TOKEN -\u0026gt; #你的token\nDATASET_ID -\u0026gt; # 用户名/数据集名称\nSYNC_INTERVAL -\u0026gt; # 同步时间（秒钟，推荐填3600）\n点击App就能看到你的项目已经运行起来啦！\n完成后可以在后端改密码，可外接webdav进行东西存储，重启spaces会拉取dataset数据库中的文件进行同步，不用担心数据丢失啦\n1后端地址：/#/admin 2 3后台默认密码：FileCodeBox2023 在cloudflare进行反代 然后划到最底下点击部署\n然后点击编辑代码 ， 填入以下内容：\n1export default { 2 async fetch(request, env) { 3 const url = new URL(request.url); 4 url.host = \u0026#39;你的地址\u0026#39;; # 注意地址不要有https:// 5 return fetch(new Request(url, request)) 6 } 7} 查看你项目的地址：\n然后点击右边的部署，再在worker的设置里填入自定义域域名，就可以啦！\n","date":"2025-02-24T16:38:34+08:00","image":"https://langminjeii.ifantic.de/post/codebox.png","permalink":"https://langminjeii.ifantic.de/post/%E4%BD%BF%E7%94%A8huggingface%E9%83%A8%E7%BD%B2filecodebox/","title":"使用HuggingFace部署FileCodeBox"},{"content":"准备工作 确保你已经有GitHub账号和cloudflare账号 一个托管在cf的域名 安装Git与Hugo 安装Git Git官网 https://git-scm.com/downloads 一路下一步即可 安装Hugo hugo 有两个版本，标准版和扩展版，推荐使用扩展版。\nGithub Releases https://github.com/gohugoio/hugo/releases 下载解压\n我选择extended版本，如： hugo_0.144.0_windows-amd64.zip\n放入指定目录如： D:\\hugo\\hugo.exe 添加系统变量\nwin+R输入sysdm.cpl → 高级 → 环境变量 → 系统变量找到Path变量 → 编辑 → 新建 → 将hugo.exe的目录加入，如： D:\\hugo 确认安装成功\ncmd中输入hugo version,输出以下即为成功 1C:\\Users\\User\u0026gt;hugo version 2hugo v0.144.0-b289b17c433aa8ebf8c73ebbaf4bed973ac8e4d5+extended windows/amd64 BuildDate=2025-02-17T16:22:31Z VendorInfo=gohugoio 新建Hugo博客 新建一个目录，用来存放博客的根目录，如： D:\\hugo，之后都将这个目录称为根目录。 cmd命令，其中blog可以自定义，也是生成的文件夹名 1hugo new site blog 应用Hugo主题 来到根目录下，cmd命令： 1git clone https://github.com/CaiJimmy/hugo-theme-stack/ themes/hugo-theme-stack 可以看到如下文件结构\n1blog/ 2├── archetypes/ 该目录包含新内容的模板，查看[详细资料](https://gohugo.io/content-management/archetypes/) 3│ └── default.md 4├── assets/ 该目录包含通常通过资产管道传递的全局资源。这包括图像、CSS、Sass、JavaScript 和 TypeScript 等资源。查看[详细资料](https://gohugo.io/hugo-pipes/introduction/) 5├── content/ 该目录包含标记文件（通常是 Markdown）和构成站点内容的页面资源。查看[详细资料](https://gohugo.io/content-management/organization/) 6├── data/ 该目录包含数据文件（JSON、TOML、YAML 或 XML），用于增强内容、配置、本地化和导航。查看[详细资料](https://gohugo.io/content-management/data-sources/) 7├── i18n/ 该目录包含多语言站点的翻译表。查看[详细资料](https://gohugo.io/content-management/multilingual/) 8├── layouts/ layouts 目录包含用于将内容、数据和资源转换为完整网站的模板。查看[详细资料](https://gohugo.io/templates/) 9├── static/ 该目录包含的文件将在您构建站点时复制到公共目录。例如：、 和验证站点所有权的文件。在引入页面捆绑包和资产管道之前，该目录还用于图像、CSS 和 JavaScript。 10├── themes/ 该目录包含一个或多个主题，每个主题都位于其自己的子目录中 11└── hugo.toml 站点配置，可能分为多个子目录和文件。对于配置最少的项目或不需要在不同环境中表现不同的项目，在项目根目录中命名的单个配置文件就足够了。查看[详细资料](https://gohugo.io/getting-started/configuration/#configuration-directory) 配置\u0026amp;美化 来到根目录\\themes\\hugo-theme-stack\\exampleSite文件夹下，将hugo.yaml文件复制至根目录下，如果根目录下有别的配置文件如hugo.toml/JSON，将其删掉。然后打开hugo.yaml，根据模板进行修改。 为了打造更符合自己心意的站点，可能需要diy很多配置，所以可以将主题的资源复制到个人站点。在根目录文件夹下，cmd命令： 1xcopy themes\\hugo-theme-stack\\archetypes archetypes\\ /E /I 2xcopy themes\\hugo-theme-stack\\assets assets\\ /E /I 3xcopy themes\\hugo-theme-stack\\data data\\ /E /I 4xcopy themes\\hugo-theme-stack\\i18n i18n\\ /E /I 5xcopy themes\\hugo-theme-stack\\layouts layouts\\ /E /I 其中favicon网站图标favicon.ico需要放到根目录\\static下，然后配置是favicon: /favicon.ico 其中avatar侧栏头像avatar.png需要放到根目录\\assets\\img下，然后配置是src: img/avatar.png 如果需要搜索和归档栏，需要在主题模板根目录\\themes\\hugo-theme-stack\\exampleSite\\content\\page中找到对应的.md文件并放到根目录同样的文件夹下如:根目录\\content\\page\\search\\index.md并按需修改。 接下来是样式的美化，供参考，根目录\\assets\\scss下新建custom.scss，并编辑： 1//---------------------------------------------------- 2// 页面基本配色 3:root { 4 // 全局顶部边距 5 --main-top-padding: 30px; 6 // 全局卡片圆角 7 --card-border-radius: 25px; 8 // 标签云卡片圆角 9 --tag-border-radius: 8px; 10 // 卡片间距 11 --section-separation: 40px; 12 // 全局字体大小 13 --article-font-size: 1.8rem; 14 // 行内代码背景色 15 --code-background-color: #f8f8f8; 16 // 行内代码前景色 17 --code-text-color: #e96900; 18 // 暗色模式下样式 19 \u0026amp;[data-scheme=\u0026#34;dark\u0026#34;] { 20 // 行内代码背景色 21 --code-background-color: #f8f8f814; 22 // 行内代码前景色 23 --code-text-color: #e96900; 24 // 暗黑模式下背景色 25 //--body-background: #000; 26 // 暗黑模式下卡片背景色 27 //--card-background: hsl(225 13% 8% / 1); 28 } 29 } 30 //------------------------------------------------------ 31 // 修复引用块内容窄页面显示问题 32 a { 33 word-break: break-all; 34 } 35 36 code { 37 word-break: break-all; 38 } 39 40 //--------------------------------------------------- 41 // 文章封面高度 42 .article-list article .article-image img { 43 width: 100%; 44 height: 200px !important; 45 object-fit: cover; 46 47 @include respond(md) { 48 height: 250px !important; 49 } 50 51 @include respond(xl) { 52 height: 285px !important; 53 } 54 } 55 56 //-------------------------------------------------- 57 // 文章内容图片圆角阴影 58 .article-page .main-article .article-content { 59 img { 60 max-width: 96% !important; 61 height: auto !important; 62 border-radius: 8px; 63 } 64 } 65 66 //------------------------------------------------ 67 // 文章内容引用块样式 68 .article-content { 69 blockquote { 70 border-left: 6px solid #d57b5e !important; 71 background: #ffefdf; 72 } 73 } 74 [data-scheme=\u0026#34;dark\u0026#34;] { 75 .article-content { 76 blockquote { 77 border-left: 6px solid #d57b5e !important; 78 background: #d57c5e54; 79 } 80 } 81 } 82 // --------------------------------------- 83 // 代码块样式修改 84 .highlight { 85 max-width: 102% !important; 86 background-color: var(--pre-background-color); 87 padding: var(--card-padding); 88 position: relative; 89 border-radius: 20px; 90 margin-left: -7px !important; 91 margin-right: -12px; 92 box-shadow: var(--shadow-l1) !important; 93 94 \u0026amp;:hover { 95 .copyCodeButton { 96 opacity: 1; 97 } 98 } 99 100 // keep Codeblocks LTR 101 [dir=\u0026#34;rtl\u0026#34;] \u0026amp; { 102 direction: ltr; 103 } 104 105 pre { 106 margin: initial; 107 padding: 0; 108 margin: 0; 109 width: auto; 110 } 111 } 112 113 // light模式下的代码块样式调整 114 [data-scheme=\u0026#34;light\u0026#34;] .article-content .highlight { 115 background-color: #fff9f3; 116 } 117 118 [data-scheme=\u0026#34;light\u0026#34;] .chroma { 119 color: #ff6f00; 120 background-color: #fff9f3cc; 121 } 122 123 124 125 //------------------------------------------- 126 // 设置选中字体的区域背景颜色 127 //修改选中颜色 128 ::selection { 129 color: #fff; 130 background: #34495e; 131 } 132 133 a { 134 text-decoration: none; 135 color: var(--accent-color); 136 137 \u0026amp;:hover { 138 color: var(--accent-color-darker); 139 } 140 141 \u0026amp;.link { 142 color: #4288b9ad; 143 font-weight: 600; 144 padding: 0 2px; 145 text-decoration: none; 146 cursor: pointer; 147 148 \u0026amp;:hover { 149 text-decoration: underline; 150 } 151 } 152 } 153 154 //------------------------------------------------- 155 //文章封面高度更改 156 .article-list article .article-image img { 157 width: 100%; 158 height: 150px; 159 object-fit: cover; 160 161 @include respond(md) { 162 height: 200px; 163 } 164 165 @include respond(xl) { 166 height: 305px; 167 } 168 } 169 170 //--------------------------------------------------- 171 // 全局页面布局间距调整 172 .main-container { 173 min-height: 100vh; 174 align-items: flex-start; 175 padding: 0 15px; 176 gap: var(--section-separation); 177 padding-top: var(--main-top-padding); 178 179 @include respond(md) { 180 padding: 0 37px; 181 } 182 } 183 184 //-------------------------------------------------- 185 //页面三栏宽度调整 186 .container { 187 margin-left: auto; 188 margin-right: auto; 189 190 .left-sidebar { 191 order: -3; 192 max-width: var(--left-sidebar-max-width); 193 } 194 195 .right-sidebar { 196 order: -1; 197 max-width: var(--right-sidebar-max-width); 198 199 /// Display right sidebar when min-width: lg 200 @include respond(lg) { 201 display: flex; 202 } 203 } 204 205 \u0026amp;.extended { 206 @include respond(md) { 207 max-width: 1024px; 208 --left-sidebar-max-width: 25%; 209 --right-sidebar-max-width: 22% !important; 210 } 211 212 @include respond(lg) { 213 max-width: 1280px; 214 --left-sidebar-max-width: 20%; 215 --right-sidebar-max-width: 30%; 216 } 217 218 @include respond(xl) { 219 max-width: 1453px; //1536px; 220 --left-sidebar-max-width: 15%; 221 --right-sidebar-max-width: 25%; 222 } 223 } 224 225 \u0026amp;.compact { 226 @include respond(md) { 227 --left-sidebar-max-width: 25%; 228 max-width: 768px; 229 } 230 231 @include respond(lg) { 232 max-width: 1024px; 233 --left-sidebar-max-width: 20%; 234 } 235 236 @include respond(xl) { 237 max-width: 1280px; 238 } 239 } 240 } 241 242 //------------------------------------------------------- 243 //全局页面小图片样式微调 244 .article-list--compact article .article-image img { 245 width: var(--image-size); 246 height: var(--image-size); 247 object-fit: cover; 248 border-radius: 17%; 249 } 250 251 //---------------------------------------------------- 252 //固定代码块的高度 253 .article-content { 254 .highlight { 255 padding: var(--card-padding); 256 pre { 257 width: auto; 258 max-height: 20em; 259 } 260 } 261 } 262 263 //-------------------------------------------------- 264 // 修改首页搜索框样式 265 .search-form.widget input { 266 font-size: 1.5rem; 267 padding: 44px 25px 19px; 268 } 269 270 271 272//-------------------------------------------------- 273//归档页面双栏 274/* 归档页面两栏 */ 275@media (min-width: 1024px) { 276 .article-list--compact { 277 display: grid; 278 grid-template-columns: 1fr 1fr; 279 background: none; 280 box-shadow: none; 281 gap: 1rem; 282 283 article { 284 background: var(--card-background); 285 border: none; 286 box-shadow: var(--shadow-l2); 287 margin-bottom: 8px; 288 border-radius: 16px; 289 } 290 } 291 } 配置说明 hugo.yaml 字段说明：\nbaseurl 博客的URL languageCode 语言代码，例如zh-cn theme 主题 paginate 每页显示的文章数量 title 网站名称 copyright 网站底部的个性化说明 DefaultContentLanguage 默认显示的语言 hasCJKLanguage CJK字数统计，如果编码是 zh-cn，需要改成 true languages 多语言设置，如不需要只需要把别的多余的语言删除即可 services/googleAnalytics Google分析代码 params/favicon 站点logo params/footer/since 创建博客的年份 params/dateFormat/published 发布时间格式 params/dateFormat/lastUpdated 最后更新时间格式 params/sidebar/emoji 头像右下角的emoji params/sidebar/subtitle 位于头像下面的副标题 params/sidebar/avatar/src 头像图片位置，相对 assets/ 目录 menu/social 社交信息配置 上传至GitHub并用Cloudflare Pages部署 GitHub 配置 在 Github 中创建新的代码仓库，由于我们将使用 Cloudflare Pages 部署网站而非直接使用 Github，因此仓库可以设为私密状态，之后将本地代码提交到代码仓库中：\n1git init 2git config --global user.name \u0026#34;\u0026lt;用户名\u0026gt;\u0026#34; 3git config --global user.email \u0026#34;\u0026lt;邮箱\u0026gt;\u0026#34; 4git remote add origin https://github.com/\u0026lt;用户名\u0026gt;/\u0026lt;GitHub仓库名\u0026gt;.git 5 6git add --all 7git commit -m \u0026#34;first commit\u0026#34; 8git push origin master Cloudflare Pages 部署 打开 Cloudflare 官网，并登录你的 Cloudflare 账户。 进入 Workers 和 Pages 页面并点击创建 选择 Pages 选项卡，并点击连接到 git 选择 github 并登录选择刚才创建的代码仓库 配置构建设置：生产分支选择 master，框架预设选择 Hugo，需要注意的是在环境变量这一栏中需要配置 HUGO_VERSION 为当前 Hugo 最新的版本号，如图。 之后点击保存并部署，就可以完成自动化部署配置，此后你对博客的所有更新只需提交到 github 的 master 分支， Cloudflare 将会自动拉取最新代码并进行部署操作。 然后在刚刚创建的 Cloudflare Pages 中的自定义域选项卡中绑定你在cf托管的域名，此后就可以通过你的域名来访问博客啦 新建文章 新建的文章会根据 archetypes/default.md 模板创建在 content/ 目录下\n1hugo new content content/post/my-first-post.md 之后在你的 content/post 文件夹中会创建你的第一篇文章，打开后可以发现它的 draft = true，说明它还是一篇草稿，此时即使发布了你的网站，该文章也不会被浏览者看见。但是你可以通过在根目录下cmd命令启动 Hugo 服务，并看到所有草稿文章：\n1hugo server -D 编辑时可以构建本地博客实时预览，默认网址 http://localhost:1313/\n使用本地图片 后面有用 CloudFlare R2 作图床的教程，不太推荐使用本地图片。\n点击显示 Hugo 会根据你内容文件的路径以及 permalinks 配置来生成 URL 和文件夹结构\n例如：\n1├── post 2│ └── blog 3│ └── images 4│ └── test.png 5│ └── first.md 编译以后：\n1├── public 2│ └── p 3│ └── first 4│ └── index.html 5│ └── post 6│ └── blog 7│ └── images 8│ └── test.png 会导致本地图片的相对路径改变，所以在使用本地图片时应相对 content/ 路径\n在 first.md 中使用\n1![](/post/blog/images/test.png) 搭建 Cloudflare R2 免费图床 平时写博客都是使用md格式，图片需要有外链。 为什么用 CloudFlare R2 有白嫖额度 免费CDN 绑定域名不需要备案 购买 R2 虽说是购买，但是我们的赛博大善人 Cloudflare 也给了相当多的免费额度，对于一个小博客来说绰绰有余。就算是超出了免费额度，续费也就一角钱一个G，相比腾讯云和阿里云的对象存储要便宜得多。\n命名存储桶； 选择一个离博客浏览人群近点的位置，为了方便我就选了亚太地区。 创建存储桶。 创建完存储桶，但是不允许公共 URL 访问的，要进一步的设置。\n点击设置：\n到设置页面 之后往下拉，找到 R2.dev 子域 ，点击允许访问。\n设置完后，就可以在对象页面 添加图片啦。上传后，点击对应的图片文件，如下图：\n就能找到 Cloudflare 给你分配的图片链接啦 :bili_062:\n在R2存储桶中的设置里找到自定义域，输入你博客域名的子域名，用cdn、static或者你想用什么都行。\n使用PicGo上传图片到R2 创建R2 API令牌 然后点击右下角的 创建 API 令牌\n务必保存好这两个，下面用到\n配置PicGo 安装PicGo详细步骤，看这里\n安装S3插件 配置s3 配置完成就能愉快的上传图片啦！让我们大声说： 谢谢赛博菩萨\n致谢与参考 感谢 @TheSmallHanCat 于 https://linux.do/t/topic/225250 中帮忙薅的 .me 域名\n参考： https://yukari.cyou/blog/#%E9%85%8D%E7%BD%AE%E7%BE%8E%E5%8C%96 https://imhy.top/p/hugo-stack%E4%B8%BB%E9%A2%98%E4%BD%BF%E7%94%A8%E5%92%8C%E7%BE%8E%E5%8C%96%E9%85%8D%E7%BD%AE/#%E6%B7%BB%E5%8A%A0-stack-%E4%B8%BB%E9%A2%98 https://blog.huacai.one/post/3 ","date":"2025-02-19T18:00:00+08:00","image":"https://langminjeii.ifantic.de/post/hugo.jpg","permalink":"https://langminjeii.ifantic.de/post/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0/","title":"我的第一篇文章"}]