Files
blog/categories/tech/index.html
T
2026-06-19 13:28:19 +08:00

274 lines
39 KiB
HTML

<!DOCTYPE html><html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="Personal blog by biss."><link rel="icon" href="/images/Bi.ico"><script defer src="https://umami.biss.click/script.js" data-website-id="3d97b310-b241-4fc6-bc9c-68e317fc7d42"></script><script>window.SITE_CONFIG = {"site":{"url":"https://blog.biss.click","name":"Bi's Blog","description":"Personal blog by biss.","language":"zh-CN","domain":"blog.biss.click","slogan":"好奇心转化为代码的空间。","startTime":"2025-01-01T00:00:00+08:00","copyrightStartYear":2025,"license":"内容采用 CC BY-NC-SA 4.0 协议"},"author":{"name":"biss","email":"bishsh2006@gmail.com","github":"https://github.com/bishshi","avatar":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"comments":{"envId":"https://comment.biss.click","region":"","script":"https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js"},"analytics":{"umami":{"script":"https://umami.biss.click/script.js","websiteId":"3d97b310-b241-4fc6-bc9c-68e317fc7d42","statsEndpoint":"https://blogumami.biss.click"}},"talks":{"apiUrl":"https://mm.biss.click/api/echo/page","pageSize":30,"homeLimit":6,"cacheKey":"talksCache","cacheTimeKey":"talksCacheTime","cacheDurationMs":1800000,"fallbackAvatar":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"search":{"apiKey":"2uxQzk7eD2GBCljdSmIiuU3ause7UK9n","server":{"host":"typesense.biss.click","port":"443","protocol":"https"},"indexName":"blogs","searchParams":{"query_by":"title,content","highlight_full_fields":"title,content","per_page":8,"num_typos":1,"typo_tokens_threshold":1,"prefix":true},"ui":{"maxRetries":10,"retryDelay":100,"animationDuration":300,"maxInitRetries":30,"placeholder":"输入关键词寻找故事..."}},"externalLinks":{"waitSeconds":10,"whitelist":["blog.biss.click","biss.click","github.com","localhost","127.0.0.1"]}};</script><script>
(() => {
const storageKey = 'site-theme';
const savedTheme = localStorage.getItem(storageKey);
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.dataset.theme = savedTheme || systemTheme;
})();
</script><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/instantsearch.css/themes/reset-min.css"><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css"><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css"><title>技术 | Bi&#39;s Blog</title><link rel="stylesheet" href="/_astro/BaseLayout.D7kFxpxP.css"></head> <body> <header class="site-header"> <nav class="nav"> <a class="brand" href="/">Bi&#39;s Blog</a> <button class="nav-menu-toggle" type="button" aria-label="打开菜单" aria-expanded="false" data-nav-toggle> <i class="fa-solid fa-bars nav-menu-open" aria-hidden="true"></i> <i class="fa-solid fa-xmark nav-menu-close" aria-hidden="true"></i> </button> <div class="nav-menu" data-nav-menu> <div class="nav-links nav-center"> <a href="/archives/"><i class="fa-solid fa-box-archive nav-icon" aria-hidden="true"></i><span>归档</span></a><a href="/categories/"><i class="fa-solid fa-folder-open nav-icon" aria-hidden="true"></i><span>分类</span></a><a href="/tags/"><i class="fa-solid fa-tags nav-icon" aria-hidden="true"></i><span>标签</span></a><a href="/shuoshuo/"><i class="fa-regular fa-comments nav-icon" aria-hidden="true"></i><span>说说</span></a><a href="/about/"><i class="fa-solid fa-heart nav-icon" aria-hidden="true"></i><span>关于</span></a><a href="/link/"><i class="fa-solid fa-link nav-icon" aria-hidden="true"></i><span>友链</span></a> </div> <div class="nav-links nav-actions"> <button class="nav-random" type="button" onclick="randomPost()" aria-label="前往随机文章" title="前往随机文章"> <i class="fa-solid fa-shuffle nav-icon" aria-hidden="true"></i> <span>随机文章</span> </button> <button class="nav-search search-typesense-trigger" type="button" aria-label="搜索" title="搜索"> <i class="fa-solid fa-magnifying-glass nav-icon" aria-hidden="true"></i> <span>搜索</span> </button> <button class="theme-toggle" type="button" aria-label="切换夜间模式" title="切换夜间模式" data-theme-toggle> <i class="fa-solid fa-sun theme-toggle-sun nav-icon" aria-hidden="true"></i> <i class="fa-solid fa-moon theme-toggle-moon nav-icon" aria-hidden="true"></i> </button> </div> </div> </nav> </header> <main> <section class="content-wrap"> <header class="page-header-card"> <p class="page-header-eyebrow">Bi&#39;s Blog</p> <h1>技术</h1> <p class="page-header-subtitle">共 21 篇文章</p> </header> <div class="post-list"> <article class="post-card"> <a href="/posts/3f8de22/"> <time datetime="2026-04-25T15:00:00.000Z">2026/04/25</time> <h2>GPT Image2 初试</h2> <p>这篇文章介绍了作者最近试用gpt img2之后的一些直观感受。文章没有从技术角度展开,而是以普通用户的体验为主,聊了它适合做什么、哪些地方比以前的AI绘图更顺手、哪些地方依然不太稳定,以及在日常做头像、壁纸、封面图和改图时的一些实际体会。整体来看,它确实比以前更听话一些,但也还没有到可以完全省心的地步。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/OpenAI/">#OpenAI</a><a href="/tags/AI/">#AI</a><a href="/tags/%E5%9B%BE%E5%83%8F%E7%94%9F%E6%88%90/">#图像生成</a> </div> </article><article class="post-card"> <a href="/posts/8712baaa/"> <time datetime="2026-04-19T13:46:25.000Z">2026/04/19</time> <h2>电子价签玩法</h2> <p>这篇文章介绍了作者在逛咸鱼时发现并购买了两个改造二手电子价签的过程,这些电子价签由tsl0922开发的开源固件提供支持,具备日历、传图、文字功能,并且可以在网页端进行便捷的配置。尽管开源版固件功能有限,作者选择自行编写所需功能,目前已经实现了待办事项和倒计时功能,并能生成图片后上传至网络。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> </div> </article><article class="post-card"> <a href="/posts/9b2ef00a/"> <time datetime="2026-04-15T19:34:23.000Z">2026/04/16</time> <h2>OpenGL- Cohen-Sutherland 编码裁剪算法</h2> <p>这篇文章介绍了Cohen-Sutherland编码裁剪算法,这是一种在计算机图形学中常用的图像裁剪方法。裁剪的目的是从一个大场景中提取所需信息,例如在浏览地图时放大显示特定区域。Cohen-Sutherland算法通过空间划分和编码方案来判断线段与矩形窗口的位置关系,从而决定线段的哪部分在窗口内、哪部分在窗口外。算法的核心在于计算线段端点的区域编码,并根据编码值来确定线段的裁剪结果。文章还提供了具体的代码实现,包括初始化设置、窗口大小调整和主函数等,展示了如何在实际应用中使用该算法进行图像裁剪。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a><a href="/categories/study/">学习</a> <a href="/tags/OpenGL/">#OpenGL</a> </div> </article><article class="post-card"> <a href="/posts/7207243b/"> <time datetime="2026-04-11T19:01:02.000Z">2026/04/12</time> <h2>OpenGL-直线的扫描转换</h2> <p>这篇文章介绍了三种直线扫描转换算法:DDA算法、中点画线算法和Bresenham算法。 DDA算法通过计算直线起点和终点的坐标差值来确定步数和增量,依次绘制直线上的点。算法实现简单,适用于任意直线,但精度较低。 中点画线算法仅适用于斜率为0或1的直线,通过计算中点和斜率来确定直线上的像素点,效率较高。 Bresenham算法根据直线的斜率和误差项来确定像素点的位置,适用于任意直线,且精度高,但实现稍复杂。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a><a href="/categories/study/">学习</a> <a href="/tags/OpenGL/">#OpenGL</a> </div> </article><article class="post-card"> <a href="/posts/437a5198/"> <time datetime="2026-04-11T18:20:34.000Z">2026/04/12</time> <h2>OpenGL-基础程序</h2> <p>这篇文章详细介绍了OpenGL的基础程序,包括代码展示和代码解释两部分。代码展示部分呈现了一个简单的OpenGL程序,涵盖了初始化与窗口管理、状态设置与缓冲区操作、矩阵与坐标变换以及几何图形绘制等关键步骤。代码解释则将这些步骤按照功能类别进行了归纳,帮助读者更好地理解OpenGL的渲染流程。通过这个示例,读者可以初步掌握OpenGL的基本操作和概念。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a><a href="/categories/study/">学习</a> <a href="/tags/OpenGL/">#OpenGL</a> </div> </article><article class="post-card"> <a href="/posts/c4477b0c/"> <time datetime="2026-04-02T04:52:16.000Z">2026/04/02</time> <h2>配置OpenGL环境</h2> <p>这篇文章详细介绍了配置OpenGL环境所需步骤,包括安装Visual Studio、CMake和GLFW等关键组件,并提供了具体的配置方法和测试代码,旨在帮助读者顺利开始计算机图形学的学习之旅。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a><a href="/categories/study/">学习</a> <a href="/tags/OpenGL/">#OpenGL</a> </div> </article><article class="post-card"> <a href="/posts/b559997d/"> <time datetime="2026-03-17T13:13:08.000Z">2026/03/17</time> <h2>opencv应用-算术运算</h2> <p>这篇文章介绍了如何使用OpenCV进行图像的加法运算。首先,通过包含OpenCV和iostream头文件,并使用std命名空间,开始编写代码。主函数中,读取两张图片,检查是否成功加载,并确保它们的尺寸相同。如果尺寸不同,将第二张图片调整为与第一张相同的大小。然后,调用add函数对两幅图片进行加法运算,并将结果存储在result矩阵中。最后,显示原始图片和加法运算的结果,并等待按键关闭窗口。代码已上传至自建的git仓库。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/opencv/">#opencv</a> </div> </article><article class="post-card"> <a href="/posts/e8f95ead/"> <time datetime="2026-03-15T09:56:51.000Z">2026/03/15</time> <h2>opencv应用-基础操作</h2> <p>这篇文章介绍了使用C++和OpenCV库进行图像处理的基础操作,包括图片的读取与展示、像素的读取与修改、图像的基本变换(缩放、旋转、平移、翻转)、以及图像通道的分离与合并。文章详细讲解了每个操作的代码实现,并展示了如何通过OpenCV库提供的函数来完成这些任务。这对于初学者来说是非常有用的,因为它帮助他们理解如何使用OpenCV进行图像处理。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/opencv/">#opencv</a> </div> </article><article class="post-card"> <a href="/posts/6e3332b/"> <time datetime="2026-03-14T11:48:36.000Z">2026/03/14</time> <h2>配置opencv</h2> <p>这篇文章介绍了在Ubuntu和Windows系统上配置OpenCV的过程。在Ubuntu系统中,通过包管理器安装了Visual Studio、GCC、G++、OpenCV以及Python的OpenCV绑定。而在Windows系统中,则通过安装Visual Studio并下载OpenCV的Windows安装包进行配置,包括将安装路径添加到系统环境变量中。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/opencv/">#opencv</a> </div> </article><article class="post-card"> <a href="/posts/7baa41fc/"> <time datetime="2026-02-25T11:09:23.000Z">2026/02/25</time> <h2>自建bitwarden服务</h2> <p>这篇文章介绍了如何自建Bitwarden服务,以替代浏览器自带的密码管理器。Bitwarden是一款开源、端到端加密的密码管理器,支持多种操作系统和设备,具有安全存储、自动同步、一键填充、生成随机密码和检测弱密码等功能。文章详细描述了部署Bitwarden服务的步骤,包括安装Vaultwarden、配置Nginx反向代理以及设置域名和数据卷等。通过这些步骤,用户可以轻松搭建一个功能强大的密码管理服务器,提高个人信息安全。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> </div> </article><article class="post-card"> <a href="/posts/56f57c0b/"> <time datetime="2026-02-23T10:49:17.000Z">2026/02/23</time> <h2>自建renovate-bot</h2> <p>这篇文章介绍了如何自建Renovate Bot以自动更新依赖,并通过Docker Compose进行部署。首先,文章提供了Renovate Bot的Docker Compose文件配置,包括使用特定平台(gitea)、设置Git API地址、生成Token以及启用自动发现功能。接着,建议创建一个专门用于管理Renovate Bot的账户,并为其生成登录Token。最后,通过在系统的crontab中设置定时任务,可以自动化运行更新流程。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/gitea/">#gitea</a> </div> </article><article class="post-card"> <a href="/posts/d2c8521/"> <time datetime="2026-02-07T12:30:39.000Z">2026/02/07</time> <h2>将博客仓库转移到gitea</h2> <p>这篇文章详细介绍了如何将博客仓库从现有的平台转移到Gitea。首先,确保已经成功安装了Gitea。接着,文章重点讲解了action文件的修改,包括检查分支、缓存项目、安装Node和Hexo、安装依赖、清理文件树、生成静态文件并压缩、部署到本地仓库以及Webhook的配置。这些步骤确保了博客能够顺利迁移到Gitea,并且保持了原有的功能和结构。需要注意的是,文章中的action文件包含了一些具体的指令和变量,如ref: master、cache-node-modules等,这些在实际操作时需要替换为具体的值。此外,文章还提到了Webhook的配置,这是一个可选但推荐的自定义事件触发器,用于在特定事件发生时自动触发部署流程。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/gitea/">#gitea</a> </div> </article><article class="post-card"> <a href="/posts/34725d47/"> <time datetime="2026-02-07T06:32:04.000Z">2026/02/07</time> <h2>安装gitea</h2> <p>这篇文章详细介绍了如何在2C2G的服务器上安装Gitea,包括获取二进制文件、创建用户和工作目录、创建系统服务、创建数据库以及安装Runner等步骤。Gitea作为一个轻量级的自助Git服务,适合用于托管网站源码。文章还提供了关于如何配置反向代理和数据库的信息,以及如何使用Docker运行Gitea Runner以实现GitHub Actions功能。通过这些步骤,用户可以在自己的服务器上成功部署Gitea,并利用其进行版本控制和自动化工作流程。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/gitea/">#gitea</a> </div> </article><article class="post-card"> <a href="/posts/f287c563/"> <time datetime="2026-02-05T13:14:16.000Z">2026/02/05</time> <h2>添加typesense搜索</h2> <p>这篇文章介绍了如何在博客中集成Typesense搜索引擎,包括安装Typesense、添加数据集以及同步数据的详细步骤。首先,通过命令行安装Typesense并配置相关参数,如API密钥和数据目录。接着,安装必要的npm包,并在Hexo博客中配置搜索功能,指定存储搜索数据的JSON文件路径。然后,编写了一个同步脚本,用于读取XML文件、解析数据、检查或创建集合以及导入数据到Typesense。最后,在博客底部添加了InstantSearch和Typesense的JavaScript库,并根据实际情况修改了配置。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a><a href="/categories/site-notes/">建站手札</a> </div> </article><article class="post-card"> <a href="/posts/66e66374/"> <time datetime="2025-10-05T15:18:00.000Z">2025/10/05</time> <h2>Adguard和Openclash共存</h2> <p>这篇文章介绍了如何在已经安装了 OpenWrt 系统并配置好 OpenClash 和 AdguardHome 的情况下,实现这两者的共存。首先简要说明了 AdguardHome 和 OpenClash 的原理,它们都是通过劫持 DNS 来达到特定目的。然后分别介绍了两种配置方式:一种是在 AdguardHome 中将上游 DNS 服务器设置为 127.0.0.1:7874,这种方式相对简单;另一种是将 OpenClash 中的 DNS 指向 AdguardHome,但可能存在拦截失败的情况。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/AdguardHome/">#AdguardHome</a><a href="/tags/Openclash/">#Openclash</a><a href="/tags/Openwrt/">#Openwrt</a> </div> </article><article class="post-card"> <a href="/posts/69b16001/"> <time datetime="2025-10-05T12:18:00.000Z">2025/10/05</time> <h2>Adguard规则分享</h2> <p>这篇文章分享了关于Adguardhome广告拦截工具的使用经验,特别强调了规则的重要性。作者提到,要达到更好的拦截效果,需要搭配有效的规则,并详细介绍了两个关键方面:一是综合过滤规则的分享,二是广告过滤规则订阅中心的集成。通过添加这些规则,使用Adblocktester工具可以达到74分的拦截效果,认为这基本够用。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/AdguardHome/">#AdguardHome</a><a href="/tags/Openwrt/">#Openwrt</a> </div> </article><article class="post-card"> <a href="/posts/b57500e9/"> <time datetime="2025-09-28T17:10:18.000Z">2025/09/29</time> <h2>在Openwrt上安装AdguardHome</h2> <p>这篇文章介绍了在OpenWrt上安装AdguardHome的过程。首先,作者提到由于luci-app-adguardhome软件源的问题,选择了其他途径获取软件包并进行安装。接着,详细说明了安装步骤,包括下载、上传和运行安装命令。在配置部分,作者强调了更新核心版本的重要性,并提供了配置AdguardHome的具体步骤,如关闭路由器自带DNS/DHCP服务器,并建议设置AdguardHome的DHCP服务器。最后,作者提醒用户在完成配置后点击“保存并应用”,并通过AdGuardHome Web界面进行验证。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/Openwrt/">#Openwrt</a><a href="/tags/AdguardHome/">#AdguardHome</a> </div> </article><article class="post-card"> <a href="/posts/2b2fb1a7/"> <time datetime="2025-09-02T21:00:00.000Z">2025/09/03</time> <h2>在Openwrt中安装OpenClash</h2> <p>这篇文章详细介绍了在OpenWrt系统中安装OpenClash的过程。作者通过Web管理页面和终端命令两种方式进行了安装,并强调了推荐启用Smart内核。安装完成后,作者提到了配置步骤,包括下载内核和导入配置文件,最后表达了对于zashboard界面简洁美观的个人喜好。整篇文章内容连贯,条理清晰,为读者提供了完整的安装指南。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/Openwrt/">#Openwrt</a><a href="/tags/Openclash/">#Openclash</a> </div> </article><article class="post-card"> <a href="/posts/8802e397/"> <time datetime="2025-08-31T20:15:00.000Z">2025/09/01</time> <h2>Cudy TR3000刷入Openwrt系统</h2> <p>这篇文章介绍了如何将Cudy TR3000路由器刷入OpenWrt系统。首先,作者提供了下载中间固件和OpenWrt固件的方法。接着,详细说明了刷入过程的步骤,包括访问不同的IP地址进行固件刷入,并提到了OpenWrt的默认登录密码是root。整个过程旨在使路由器能够支持科学上网功能。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/Cudy-TR3000/">#Cudy-TR3000</a> </div> </article><article class="post-card"> <a href="/posts/59c8572a/"> <time datetime="2025-08-26T14:27:00.000Z">2025/08/26</time> <h2>搭建Owncloud并集成Onlyoffice</h2> <p>这篇文章详细介绍了如何搭建Owncloud网盘服务并集成Onlyoffice文档编辑器。首先,作者描述了搭建过程中的基本步骤,包括安装网盘和进行系统调优,其中强调了Redis缓存配置和Cron任务设置的重要性。接着,作者提到了在1panel应用商店安装Onlyoffice的过程,并说明了如何将其反向代理至个人域名。最后,文章指导用户在Owncloud中搜索并安装Onlyoffice插件,确保了整个集成过程的顺利完成。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/%E7%BD%91%E7%AB%99/">#网站</a> </div> </article><article class="post-card"> <a href="/posts/bb18d851/"> <time datetime="2025-08-19T10:43:16.000Z">2025/08/19</time> <h2>为1Panel添加自己想要的应用</h2> <p>这篇文章详细介绍了如何为1Panel添加自定义应用的过程,包括预准备工作、创建文件和配置应用声明文件等步骤。首先,作者强调了Fork仓库和创建新分支的重要性。接着,文章逐步讲解了如何在应用目录下创建所需的文件和子目录,如halo、logo.png、data.yml、README.md等。特别详细地说明了应用声明文件data.yml的构成,包括基本信息、应用标签、类型等关键配置。此外,还解释了关于端口字段和type字段的具体使用方法,提供了实际安装时填写form表单的示例。整个过程不仅清晰易懂,而且为开发者提供了一套完整的操作指南,有助于他们更好地理解和实施应用添加到1Panel的过程。</p> </a> <div class="meta-list"> <a href="/categories/tech/">技术</a> <a href="/tags/1panel/">#1panel</a> </div> </article> </div> </section> </main> <footer class="site-footer" aria-label="站点页脚"> <div class="footer-inner"> <section class="footer-top" aria-label="站点信息"> <a class="footer-brand" href="/" aria-label="Bi's Blog 首页"> <img src="/images/Bi.ico" alt="" loading="lazy"> <span>Bi&#39;s Blog</span> </a> <p class="footer-slogan">好奇心转化为代码的空间。</p> </section> <section class="footer-contact" aria-label="联系方式"> <p>联系方式</p> <div class="footer-socials"> <a href="mailto:bishsh2006@gmail.com" aria-label="发送邮件" title="发送邮件"> <i class="fa-solid fa-envelope" aria-hidden="true"></i> </a><a href="/rss.xml" aria-label="订阅 RSS" title="订阅 RSS"> <i class="fa-solid fa-rss" aria-hidden="true"></i> </a><a href="https://github.com/bishshi" target="_blank" rel="noreferrer" aria-label="GitHub" title="GitHub"> <i class="fa-brands fa-github" aria-hidden="true"></i> </a> </div> <nav class="footer-contact-links" aria-label="页脚链接"> <a href="/archives/">发展历程</a> <span aria-hidden="true">|</span><a href="/privacy/">隐私政策</a> </nav> </section> <section class="footer-bottom" aria-label="版权信息"> <div> <p>© 2025 - 2026 Bi&#39;s Blog</p> <p>blog.biss.click | 内容采用 CC BY-NC-SA 4.0 协议</p> </div> <div class="footer-meta"> <p>本站已运行:<span id="site-runtime">计算中...</span></p> <p> <a href="https://fastly.com/" target="_blank" rel="noreferrer">Fastly</a> <span aria-hidden="true">|</span><a href="https://astro.build/" target="_blank" rel="noreferrer">ASTRO</a> </p> </div> </section> </div> </footer> <aside class="hitokoto-card" aria-label="一言" data-hitokoto-card data-astro-cid-4cgfuthx> <div class="hitokoto-card__head" data-astro-cid-4cgfuthx> <button class="hitokoto-card__toggle" type="button" aria-label="展开一言" aria-expanded="true" title="一言" data-hitokoto-toggle data-astro-cid-4cgfuthx> <i class="fa-solid fa-quote-left" aria-hidden="true" data-astro-cid-4cgfuthx></i> <span data-astro-cid-4cgfuthx>一言</span> </button> <div class="hitokoto-card__actions" data-astro-cid-4cgfuthx> <button class="hitokoto-card__refresh" type="button" aria-label="换一句" title="换一句" data-hitokoto-refresh data-astro-cid-4cgfuthx> <i class="fa-solid fa-rotate" aria-hidden="true" data-astro-cid-4cgfuthx></i> </button> <button class="hitokoto-card__collapse" type="button" aria-label="收起一言" title="收起" data-hitokoto-collapse data-astro-cid-4cgfuthx> <i class="fa-solid fa-chevron-down" aria-hidden="true" data-astro-cid-4cgfuthx></i> </button> </div> </div> <a class="hitokoto-card__text" href="https://hitokoto.cn/" target="_blank" rel="noreferrer" data-hitokoto-text data-astro-cid-4cgfuthx>
获取中...
</a> <p class="hitokoto-card__from" data-hitokoto-from data-astro-cid-4cgfuthx>来自 Hitokoto</p> </aside> <script type="module">(()=>{const o=document.querySelector("[data-hitokoto-card]"),s=o?.querySelector("[data-hitokoto-toggle]"),r=o?.querySelector("[data-hitokoto-text]"),l=o?.querySelector("[data-hitokoto-from]"),k=o?.querySelector("[data-hitokoto-refresh]"),m=o?.querySelector("[data-hitokoto-collapse]");if(!(o instanceof HTMLElement)||!(s instanceof HTMLButtonElement)||!(r instanceof HTMLAnchorElement)||!(l instanceof HTMLElement))return;const g="https://yiyan.biss.click/api?encode=json",u="hitokoto-card-cache",y=360*60*1e3,d={sentence:"生活明朗,万物可爱。",source:"本地缓存",url:"https://hitokoto.cn/"},L=["/","/index.html"].includes(window.location.pathname);function c(t){o.classList.toggle("is-collapsed",!t),s.setAttribute("aria-expanded",String(t)),s.setAttribute("aria-label",t?"收起一言":"展开一言")}function f(){try{const t=localStorage.getItem(u);if(!t)return null;const e=JSON.parse(t);return!e||typeof e.expiresAt!="number"||e.expiresAt<=Date.now()?null:e.data}catch{return null}}function x(t){try{localStorage.setItem(u,JSON.stringify({data:t,expiresAt:Date.now()+y}))}catch{}}function i(t){r.textContent=t.sentence,r.href=t.url||"https://hitokoto.cn/",l.textContent=t.source?`出自 ${t.source}`:"来自 Hitokoto"}async function h({force:t=!1}={}){if(!t){const e=f();if(e){i(e);return}}o.classList.add("is-loading"),r.textContent="获取中...";try{const e=await fetch(g);if(!e.ok)throw new Error(`Hitokoto request failed: ${e.status}`);const n=await e.json(),a=n.hitokoto||n.content||n.sentence||n.text,S=typeof a=="string"&&a.trim()?a.trim():d.sentence,E=typeof n.from=="string"&&n.from.trim()?n.from.trim():"Hitokoto",w=n.uuid?`https://hitokoto.cn/?uuid=${encodeURIComponent(n.uuid)}`:"https://hitokoto.cn/",p={sentence:S,source:E,url:w};x(p),i(p)}catch(e){console.error(e),i(f()||d)}finally{o.classList.remove("is-loading")}}c(L),s.addEventListener("click",()=>{c(o.classList.contains("is-collapsed"))}),k?.addEventListener("click",t=>{t.stopPropagation(),c(!0),h({force:!0})}),m?.addEventListener("click",t=>{t.stopPropagation(),c(!1)}),o.addEventListener("click",t=>{o.classList.contains("is-collapsed")&&(t.target instanceof Element&&t.target.closest("a, button")||c(!0))}),h()})();</script> <div class="custom-context-menu" data-context-menu role="menu" aria-hidden="true"> <div class="context-menu-nav" aria-label="页面导航"> <button type="button" data-context-action="back" aria-label="后退上一页" title="后退上一页"> <i class="fa-solid fa-arrow-left" aria-hidden="true"></i> </button> <button type="button" data-context-action="forward" aria-label="前进下一页" title="前进下一页"> <i class="fa-solid fa-arrow-right" aria-hidden="true"></i> </button> <button type="button" data-context-action="reload" aria-label="刷新页面" title="刷新页面"> <i class="fa-solid fa-rotate-right" aria-hidden="true"></i> </button> <a href="/" aria-label="返回首页" title="返回首页"> <i class="fa-solid fa-house" aria-hidden="true"></i> </a> </div> <div class="context-menu-group" data-selection-group hidden> <button type="button" data-context-action="copy-selection" data-selection-item> <i class="fa-regular fa-copy" aria-hidden="true"></i> <span>复制选中文字</span> </button> <button type="button" data-context-action="search-selection" data-selection-item> <i class="fa-brands fa-microsoft" aria-hidden="true"></i> <span>用 Bing 搜索</span> </button> <button type="button" data-context-action="quote-selection" data-selection-item data-comments-item> <i class="fa-regular fa-comment-dots" aria-hidden="true"></i> <span>引用到评论区</span> </button> </div> <div class="context-menu-group" data-image-group hidden> <button type="button" data-context-action="copy-image"> <i class="fa-regular fa-image" aria-hidden="true"></i> <span>复制图片</span> </button> <button type="button" data-context-action="open-image"> <i class="fa-solid fa-up-right-from-square" aria-hidden="true"></i> <span>新页面打开图片</span> </button> <button type="button" data-context-action="copy-image-url"> <i class="fa-solid fa-link" aria-hidden="true"></i> <span>复制图片地址</span> </button> </div> <div class="context-menu-group"> <a href="/archives/"> <i class="fa-solid fa-box-archive" aria-hidden="true"></i> <span>时间线</span> </a> <a href="/categories/"> <i class="fa-solid fa-folder" aria-hidden="true"></i> <span>分类</span> </a> <a href="/tags/"> <i class="fa-solid fa-hashtag" aria-hidden="true"></i> <span>标签</span> </a> <button type="button" data-context-action="comments" data-comments-item> <i class="fa-regular fa-comment-dots" aria-hidden="true"></i> <span>跳转评论区</span> </button> <button type="button" data-context-action="print"> <i class="fa-solid fa-print" aria-hidden="true"></i> <span>打印页面</span> </button> <a href="/cc/"> <i class="fa-solid fa-circle-info" aria-hidden="true"></i> <span>网站版权</span> </a> </div> </div> <script>
(() => {
const menu = document.querySelector('[data-context-menu]');
if (!menu) return;
const selectionItems = [...menu.querySelectorAll('[data-selection-item]')];
const selectionGroup = menu.querySelector('[data-selection-group]');
const commentsItems = [...menu.querySelectorAll('[data-comments-item]')];
const imageGroup = menu.querySelector('[data-image-group]');
const focusableSelector = 'a[href], button:not([disabled])';
let lastSelectionText = '';
let lastImage = null;
const isNativeContextTarget = (target) =>
target instanceof Element &&
Boolean(target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]'));
const getCommentsTarget = () =>
document.querySelector('.twikoo-comments, #twikoo-wrap, #twikoo, .tk-comments-container');
const getSelectionText = () => String(window.getSelection?.() || '').trim();
const getImageTarget = (target) => {
if (!(target instanceof Element)) return null;
const image = target.closest('img');
return image instanceof HTMLImageElement ? image : null;
};
const getImageUrl = () => {
if (!lastImage) return '';
return lastImage.currentSrc || lastImage.src || lastImage.getAttribute('src') || '';
};
const copyText = async (text) => {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.inset = '0 auto auto -9999px';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
};
const setMenuState = () => {
const hasSelection = lastSelectionText.length > 0;
const hasComments = Boolean(getCommentsTarget());
const hasImage = Boolean(getImageUrl());
selectionItems.forEach((item) => {
item.hidden = !hasSelection;
});
if (selectionGroup) selectionGroup.hidden = !hasSelection;
commentsItems.forEach((item) => {
item.hidden = !hasComments || (item.hasAttribute('data-selection-item') && !hasSelection);
});
if (imageGroup) imageGroup.hidden = !hasImage;
};
const closeMenu = () => {
menu.classList.remove('is-open');
menu.setAttribute('aria-hidden', 'true');
};
const openMenu = (x, y) => {
setMenuState();
menu.classList.add('is-open');
menu.setAttribute('aria-hidden', 'false');
const rect = menu.getBoundingClientRect();
const gap = 10;
const left = Math.min(Math.max(gap, x), window.innerWidth - rect.width - gap);
const top = Math.min(Math.max(gap, y), window.innerHeight - rect.height - gap);
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
};
const jumpToComments = () => {
const target = getCommentsTarget();
target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
const quoteToComments = () => {
jumpToComments();
window.setTimeout(() => {
const textarea = document.querySelector('#twikoo textarea, .twikoo-comments textarea, .tk-input textarea');
if (!(textarea instanceof HTMLTextAreaElement) || !lastSelectionText) return;
const quote = lastSelectionText
.split(/\r?\n/)
.map((line) => `> ${line}`)
.join('\n');
const current = textarea.value.trim();
textarea.value = current ? `${current}\n\n${quote}\n` : `${quote}\n`;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
}, 500);
};
const searchSelection = () => {
if (!lastSelectionText) return;
const url = `https://www.bing.com/search?q=${encodeURIComponent(lastSelectionText)}`;
window.open(url, '_blank', 'noopener,noreferrer');
};
const copyImage = async () => {
const imageUrl = getImageUrl();
if (!imageUrl) return;
try {
if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined') {
throw new Error('Image clipboard is not available.');
}
const response = await fetch(imageUrl, { mode: 'cors' });
const blob = await response.blob();
await navigator.clipboard.write([
new ClipboardItem({ [blob.type || 'image/png']: blob }),
]);
} catch {
await copyText(new URL(imageUrl, window.location.href).toString());
}
};
const openImage = () => {
const imageUrl = getImageUrl();
if (!imageUrl) return;
window.open(new URL(imageUrl, window.location.href).toString(), '_blank', 'noopener,noreferrer');
};
document.addEventListener('contextmenu', (event) => {
if (isNativeContextTarget(event.target)) return;
lastSelectionText = getSelectionText();
lastImage = getImageTarget(event.target);
event.preventDefault();
openMenu(event.clientX, event.clientY);
});
document.addEventListener('click', (event) => {
const actionTarget = event.target instanceof Element ? event.target.closest('[data-context-action]') : null;
if (!actionTarget || !menu.contains(actionTarget)) {
closeMenu();
return;
}
const action = actionTarget.dataset.contextAction;
closeMenu();
if (action === 'back') history.back();
if (action === 'forward') history.forward();
if (action === 'reload') location.reload();
if (action === 'copy-selection' && lastSelectionText) copyText(lastSelectionText);
if (action === 'search-selection') searchSelection();
if (action === 'quote-selection') quoteToComments();
if (action === 'copy-image') copyImage();
if (action === 'open-image') openImage();
if (action === 'copy-image-url') copyText(new URL(getImageUrl(), window.location.href).toString());
if (action === 'comments') jumpToComments();
if (action === 'print') window.print();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closeMenu();
if (event.key === 'Tab' && menu.classList.contains('is-open')) {
const items = [...menu.querySelectorAll(focusableSelector)].filter((item) => !item.closest('[hidden]'));
if (items.length === 0) return;
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
});
window.addEventListener('scroll', closeMenu, { passive: true });
window.addEventListener('resize', closeMenu);
})();
</script> <script src="https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0"></script> <script src="https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script> <script src="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script> <script src="/js/random.js"></script> <script src="/js/search/typesense-search.js"></script> <script src="/js/external-links.js"></script> <script>
(() => {
const nav = document.querySelector('.nav');
const toggle = document.querySelector('[data-nav-toggle]');
const menu = document.querySelector('[data-nav-menu]');
if (!nav || !toggle || !menu) return;
function setMenu(open) {
nav.classList.toggle('is-open', open);
toggle.setAttribute('aria-expanded', String(open));
toggle.setAttribute('aria-label', open ? '关闭菜单' : '打开菜单');
}
toggle.addEventListener('click', () => {
setMenu(!nav.classList.contains('is-open'));
});
menu.addEventListener('click', (event) => {
if (event.target instanceof Element && event.target.closest('a')) setMenu(false);
});
window.addEventListener('resize', () => {
if (window.innerWidth > 720) setMenu(false);
});
})();
(() => {
const storageKey = 'site-theme';
const root = document.documentElement;
const button = document.querySelector('[data-theme-toggle]');
const media = window.matchMedia('(prefers-color-scheme: dark)');
const getSystemTheme = () => (media.matches ? 'dark' : 'light');
const getTheme = () => localStorage.getItem(storageKey) || getSystemTheme();
function applyTheme(theme) {
root.dataset.theme = theme;
button?.setAttribute('aria-label', theme === 'dark' ? '切换到日间模式' : '切换到夜间模式');
button?.setAttribute('title', theme === 'dark' ? '切换到日间模式' : '切换到夜间模式');
}
applyTheme(getTheme());
button?.addEventListener('click', () => {
const nextTheme = root.dataset.theme === 'dark' ? 'light' : 'dark';
localStorage.setItem(storageKey, nextTheme);
applyTheme(nextTheme);
});
media.addEventListener('change', () => {
if (!localStorage.getItem(storageKey)) applyTheme(getSystemTheme());
});
})();
(() => {
const runtime = document.getElementById('site-runtime');
if (!runtime) return;
const start = new Date(window.SITE_CONFIG.site.startTime).getTime();
const pad = (value) => String(value).padStart(2, '0');
function updateRuntime() {
const diff = Math.max(0, Date.now() - start);
const days = Math.floor(diff / 86400000);
const hours = Math.floor((diff / 3600000) % 24);
const minutes = Math.floor((diff / 60000) % 60);
const seconds = Math.floor((diff / 1000) % 60);
runtime.textContent = `${days}${pad(hours)}${pad(minutes)}${pad(seconds)} 秒`;
}
updateRuntime();
window.setInterval(updateRuntime, 1000);
})();
</script> </body> </html>