「Mikusa Annual Issue」作为一档主攻博客小修小补的节目,如果不体现针对博客现状具体的改进方案,继续像以前一样水的话,长久下去恐怕难以服众。我从 2019 年起使用 VOID 主题到现在也快有 5 个年头了,虽说 VOID 已经有 3 年多没有更新了,但她依旧是我用得最舒服的主题,没有之一。A 酱牛逼!只是时移事迁,我也有想要在博客上增加的小东西。往年我可以说「我并不会任何编程语言,无法自行为博客添加其他功能」,所以可以冠冕堂皇地水一点简单的东西。但今年情况不一样了。ChatGPT 等 AI 的出现,让我等不懂代码之辈也能写出代码来。

于是乎,在 ChatGPT 和 New Bing 两位老师(主要是 Bing 老师,GPT 老师收费太高了)的帮助下,本代码小白,为博客添加了如下功能。

代码框复制按钮

众所周知,初之音是一个以开箱和水文为主的生活类博客,但在博主购买 NAS 继而写了一连串 NAS 相关的教程之后,水文的代码含量急剧升高。因此,为了方便大伙一键复制这些代码,我参考「亚灿网志」关于《代码块增加复制按钮》的内容,又从别处抄了个复制图标,简单实现了这一功能。可使用过程中我才发现,亚灿的代码并不支持手机端复制。

我尝试询问了下 ChatGPT,她这么说到:

document.execCommand 兼容性:document.execCommand 在现代浏览器中逐渐被废弃,不同浏览器对其的支持也有差异。您可以考虑使用更现代的 Clipboard API 来执行复制操作。

另外,这个脚本似乎还不支持PJAX,需要刷新页面才能使用。我又问 GPT 加上了 PJAX 重载,同时还加上了夜间模式。

以及复制提示:

总之,在同 GPT 进行多番友好而激烈的探讨之后,修缮过的代码框复制按钮的 JS 部分如下:

// 复制处理函数
function copyHandle(content, successMessage = "复制成功", errorMessage = "复制失败") {
    navigator.clipboard.writeText(content)
        .then(() => {
            VOID.alert(successMessage);
        })
        .catch((error) => {
            console.error(errorMessage, error);
            VOID.alert(errorMessage);
        });
}


// 点击事件,通过事件委托处理
function addClickListener() {
    $('.clipboard').on('click', function () {
        copyHandle($(this).next().text());
    });
}

// 初始化剪贴板功能
function loadClipboard() {
    $('pre').prepend('<div class="clipboard"><svg aria-hidden="true" role="img" class="clipboard-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" style="display: inline-block; user-select: none; vertical-align: text-bottom;"><path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path></svg></div>');

    // 重新绑定事件处理程序
    addClickListener();
}


// 在页面加载时设置事件
$(document).ready(() => {
    loadClipboard();
});

增加了夜间模式的 CSS 部分:

    /* 剪切板 */
    .clipboard {
        position: absolute;
        top: 3px;
        right: 10px;
        color: #22252b;
        z-index: 100;
        text-align: center;
        cursor: pointer;
        line-height: 18px;
    }
    body.theme-dark .clipboard {
        color: white;
    }

以及最重要的 PJAX 重载函数。如果有使用 PJAX 的话,别忘了在 VOID 主题设置的「PJAX 重载函数」中填入这个函数:

loadClipboard();

你只需在 VOID 主题设置的「head 标签输出内容」中用 <script> </script><style> </style> 分别包住 JSCSS 代码,就可以拥有这个复制按钮啦!

首页头图动效

首页头图动效是从贰岛博客那里抄过来的,你可以现在返回本站首页(仅PC端)查看一下效果。

具体是在 VOID/includes/banner.php 最后的 <?php elseif($this->is('index')): ?> 条件判断中添加一段脚本,详细代码如下:

<?php elseif($this->is('index')): ?>
        <?php
            $title = Helper::options()->title;
            if($setting['indexBannerTitle']!='') $title = $setting['indexBannerTitle'];
            $subtitle = Helper::options()->description;
            if($setting['indexBannerSubtitle']!='') $subtitle = $setting['indexBannerSubtitle'];
        ?>
        <div class="banner-title index<?php if(!empty($banner)) echo ' force-normal'; ?>">
            <h1 class="post-title"><span class="brand"><span><?php echo $title; ?></span></span><br><span class="subtitle"><?php echo $subtitle; ?></span></h1>
        </div>

        <!-- 首页头图动效开始 -->
        <script>
            detect = document.querySelector(".index");
            banner_img = document.querySelector("#banner > img");
            banner_img.style.width = "110%";
            banner_img.style.left = "-5%";

            detect.addEventListener("mouseenter", function(n) {
                this.x = n.clientX, banner_img.style.transition = "none"
            });

            detect.addEventListener("mousemove", function(n) {
                this._x = n.clientX;
                n = 0 - (this._x - this.x) / -30;
                banner_img.style.transform = "translate(" + n + "px, 0px)"
            });
            detect.addEventListener("mouseleave", function(n) {
                banner_img.style.transition = ".3s", banner_img.style.transform = "translate(0,0)"
            });
        </script>
        <!-- 首页头图动效结束 -->

    <?php endif; ?>
</div>

只是几天没见他又更新了,我已经抄过来了,嘿嘿。

首先是一段 css

    #scrollButton {
        position: absolute;
        left: 50%;
        bottom: 10px;
        color: white;
        transform: translateX(-50%);
    }

    #scrollButton:hover {
        filter: drop-shadow(2px 5px 6px white);
    }

    #scrollButton.rotated {
        transform: rotateX(180deg);
        /* Rotates the button 180 degrees around the X-axis */
        transition: transform 0.5s ease;
        /* Smooth transition for rotation */
    }

    .lazy-wrap:not(.no-banner) {
        min-height: 0vh;
        filter: brightness(1);
        transition: min-height 0.5s ease, filter 1s ease;
    }

再是一段脚本,也是在 VOID/includes/banner.php 最后的 <?php elseif($this->is('index')): ?> 条件判断中添加:

<!-- 首页头图放大动效 -->
        <?php if (!Utils::isMobile()): ?>

            <div id="scrollButton" aria-label="Scroll Down">
                <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="white" height="15px"
                    width="15px" version="1.1" id="Layer_1" viewBox="0 0 330 330" xml:space="preserve">
                    <path id="XMLID_225_"
                        d="M325.607,79.393c-5.857-5.857-15.355-5.858-21.213,0.001l-139.39,139.393L25.607,79.393  c-5.857-5.857-15.355-5.858-21.213,0.001c-5.858,5.858-5.858,15.355,0,21.213l150.004,150c2.813,2.813,6.628,4.393,10.606,4.393  s7.794-1.581,10.606-4.394l149.996-150C331.465,94.749,331.465,85.251,325.607,79.393z" />
                </svg>
            </div>
            <script>
                document.getElementById('scrollButton').addEventListener('click', function () {
                    var lazyWrap = document.querySelector('.lazy-wrap:not(.no-banner)');
                    var button = document.getElementById('scrollButton');

                    if (lazyWrap) {
                        lazyWrap.style.minHeight = lazyWrap.style.minHeight === '89vh' ? '' : '89vh';
                        lazyWrap.style.filter = lazyWrap.style.filter === 'brightness(1.3)' ? '' : 'brightness(1.3)';
                    }

                    // Toggle the 'rotated' class on the button
                    button.classList.toggle('rotated');
                });
            </script>

        <?php endif; ?>

页脚一言

VOID 早期的版本自带了「一言」,后来移除了。就像这样:

早期的 VOID 也超好看!
早期的 VOID 也超好看!

我想着添加回来。官网的 使用示例 是这样的:

我们假设您的网页中存在一个块级元素用于显示一言的文本内容,且我们想让它能跳转到一言的指定页面用于后续的收藏、反馈。

<!-- 请注意,以下的示例包含超链接,您可能需要手动配置样式使其不变色。如果您嫌麻烦,可以移除。 -->
<p id="hitokoto">
<a href="#" id="hitokoto_text">:D 获取中...</a>
</p>

那我们可以在 <script></script> 中 或者 .js 文件中使用我们的接口:

<!-- 本例不能添加链接内容,放在此处只是因为此接口比较方便,也许能够解决大部分的需求-->
<script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script>

或者使用 Fetch API

// 请注意此 Web API 的兼容性,
// 不支持 IE, iOS Safari < 10.1,
// 完整支持列表参考:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
fetch('https://v1.hitokoto.cn')
  .then(response => response.json())
  .then(data => {
    const hitokoto = document.querySelector('#hitokoto_text')
    hitokoto.href = `https://hitokoto.cn/?uuid=${data.uuid}`
    hitokoto.innerText = data.hitokoto
  })
  .catch(console.error)

我最终选择了 Fetch API 的方案。但这样出现的一言没有出处,我希望的一言格式是像早期 VOID 那样带出处的格式。在询问了 New Bing 之后,这段代码改成了这样:

fetch('https://v1.hitokoto.cn')
  .then(response => response.json())
  .then(data => {
    const hitokoto = document.querySelector('#hitokoto_text')
    hitokoto.href = `https://hitokoto.cn/?uuid=${data.uuid}`
    hitokoto.innerText = data.hitokoto + " —— " + "「" + data.from + "」"
  })
  .catch(console.error)

一言的效果就会是这样:

你指尖跃动的电光,是我此生不变的信仰,唯我超电磁炮永世长存。 —— 「某科学的超电磁炮」

可这样还不够,这是一条带链接的一言,可以跳转到一言官网。说实话我不希望是这样的效果,我只要文字就够了。继续询问了 New Bing 之后,引用一言的代码改成了这样:

<p id="hitokoto"><span id="hitokoto_text">少女祈祷中...</span></p>

打开 VOID 主题文件夹,在 /VOID/includes/里找到 footer.php ,在「感谢陪伴」下面插入这段代码:

<p>感谢陪伴:<span id="uptime"></span></p>
<p><span id="hitokoto_text">少女祈祷中...</span></p>

那既然不需要链接,原来的脚本就改成了这样:

function loadHitokoto() {
    fetch('https://v1.hitokoto.cn/?c=a&c=c')
        .then(response => response.json())
        .then(data => {
            const hitokoto = document.querySelector('#hitokoto_text')
            hitokoto.innerText = data.hitokoto + " —— " + "「" + data.from + "」"
        })
  .catch(console.error)
}

这还没结束。因为我启用了 VOID 的 PJAX 功能,所以需要让这段脚本能在切换页面的时候跟着重载。同时,我还希望能在一言服务中断的时候,能输出错误提示而不是显示「少女祈祷中...」。于是在 New Bing 的帮助下,这段脚本就适配上了 PJAX

// 定义一个函数来加载一言
function loadHitokoto() {
    // 使用 fetch API 从一言服务器获取数据
    fetch('https://v1.hitokoto.cn/?c=a&c=c')
        // 当响应到达时,将其解析为 JSON
        .then(response => response.json())
        // 当数据被解析时,更新 #hitokoto_text 元素的文本
        .then(data => {
            const hitokoto = document.querySelector('#hitokoto_text')
            hitokoto.innerText = data.hitokoto + " —— " + "「" + data.from + "」"
        })
        // 如果在上述过程中出现错误,将 #hitokoto_text 元素的文本更改为错误消息
        .catch(error => {
            console.error(error);
            const hitokoto = document.querySelector('#hitokoto_text')
            hitokoto.innerText = "一言服务失效啦~";
        });
}
$(document).ready(function () { loadHitokoto(); });

最后,在 VOID 主题设置中添加一言的重载函数:

loadHitokoto();

表情包脚本

关于 VOID 的表情包,在《Mikusa Yearly Issue 2》一文中我有照猫画虎地模仿其他博主自行增加了一些表情(其实我很好奇那些代码是如何生成的),现在那些表情我用腻了,全手动再添加不仅费时费力且不符合本次 Issue 的主旨。我通过 New Bing 写了一段脚本,把这一过程尽可能地自动化了:

# 创建一个空的ArrayList来存储结果
$jsonArray = New-Object System.Collections.ArrayList

# 遍历当前目录下的所有.png文件
Get-ChildItem -Filter "*.png" | ForEach-Object {
    # $_ 是一个特殊变量,表示当前正在处理的对象(在这里,它表示当前正在处理的文件)

    # 获取当前文件的文件名(不包括扩展名)
    $oldName = $_.BaseName

    # 获取当前文件的扩展名
    $extension = $_.Extension

    # 使用URL编码对文件名进行编码
    $newName = [System.Web.HttpUtility]::UrlEncode($oldName)

    # 删除文件名中的%
    $newName = $newName.Replace("%", "")

    # 将文件名转换为大写
    $newName = $newName.ToUpper()

    # 将文件名和扩展名合并,得到新的文件名
    $newName = $newName + $extension

    # 重命名文件
    Rename-Item -Path $_.Name -NewName $newName

    # 创建一个Hashtable来存储当前文件的信息
    $jsonObject = New-Object PSObject

    # 添加"icon"字段到Hashtable中
    $jsonObject | Add-Member -MemberType NoteProperty -Name "icon" -Value ("<img class=`"biaoqing`" data-src=`"/usr/themes/VOID/assets/libs/owo/biaoqing/mihoyo/$newName`">")

    # 添加"data"字段到Hashtable中,!($oldName)前面的斜杠 \ 请手动删除,会被 VOID 转义
    $jsonObject | Add-Member -MemberType NoteProperty -Name "data" -Value (":\!($oldName)")

    # 添加"text"字段到Hashtable中
    $jsonObject | Add-Member -MemberType NoteProperty -Name "text" -Value ("$oldName")

    # 将当前文件的信息添加到结果中
    $jsonArray.Add($jsonObject) | Out-Null
}

# 将结果转换为JSON格式,并输出到名为owo.json的文件中
$jsonArray | ConvertTo-Json | Out-File -FilePath owo.json

现在只需手动准备好表情图片,比如上米游社扒一些藿藿。表情包文件名称最好为图中所示格式。

然后新建一个文本文档,把上面的脚本填入其中,修改 .txt 后缀为 .ps1

Win 11的话自带终端。右键当前目录打开终端,输入

.\owo.ps1

回车后,表情文件名就自动修改成了删除 % 后的 URL 编码格式,同时生成符合 VOID 表情格式的 JSON 文件。

[
  {
    "icon": "<img class=\"biaoqing\" data-src=\"/usr/themes/VOID/assets/libs/owo/biaoqing/mihoyo/E897BFE897BF_E5A5BDE7979B.png\">",
    "data": ":/!(藿藿_好痛)",
    "text": "藿藿_好痛"
  },
  {
    "icon": "<img class=\"biaoqing\" data-src=\"/usr/themes/VOID/assets/libs/owo/biaoqing/mihoyo/E897BFE897BF_E68D8FE884B8.png\">",
    "data": ":/!(藿藿_捏脸)",
    "text": "藿藿_捏脸"
  },
  {
    "icon": "<img class=\"biaoqing\" data-src=\"/usr/themes/VOID/assets/libs/owo/biaoqing/mihoyo/E897BFE897BF_E68A93E78B82.png\">",
    "data": ":/!(藿藿_抓狂)",
    "text": "藿藿_抓狂"
  }
]

参考《Mikusa Yearly Issue 2》中的步骤,修改表情包相关文件,就可以拥有崭新的表情包啦!!

如果能外挂表情包就好了,可惜我不知道如何就这个功能怎么向 GPT 提问。

Copyright 插件

同样是在《Mikusa Yearly Issue 2》一文中,我提到用上了 Copyright 插件的事。那时还在碎碎念不能直接使用 markdown 格式的链接,这次依托 New Bing,也成功为其加上了 markdown 解析的功能。虽然看不懂原理而且像是有 Bug,但至少真的能直接用 markdown 了。

具体是将这一部分代码:

$t_cover = '<p class="content-copyright"><strong>封面出处:</strong>' . $cr['cover'] . '</p>';

修改成这样:

$parsedCover = Typecho_Widget::widget('Widget_Abstract_Contents')->markdown($cr['cover']);
$parsedCover = strip_tags($parsedCover, '<a><em><strong>');  // 保留链接和强调标签
$t_cover = '<p class="content-copyright"><strong>封面出处:</strong>' . $parsedCover . '</p>';

就可以像这样直接在封面出处中使用 markdown 语法的链接了。

[XilmO@夕末 / sad #Pixiv](https://www.pixiv.net/artworks/109181977)

顺便把作者那块也改成能解析 markdown 的:

$t_author = '<p class="content-copyright"><strong>本文作者:</strong>' . $cr['author'] . '</p>';

修改成这样:

$parsedAuthor = Typecho_Widget::widget('Widget_Abstract_Contents')->markdown($cr['author']);
$parsedAuthor = strip_tags($parsedAuthor, '<a><em><strong>');  // 保留链接和强调标签
$t_author = '<p class="content-copyright"><strong>本文作者:</strong>' . $parsedAuthor . '</p>';

就可以像这样直接使用 markdown 语法了:

[mikusa](https://www.himiku.com)

不过测试的时候发现 Copyright 插件好像不太兼容 Typecho 1.2,禁用后再启用功能会全部失效。所以不要贸然禁用插件,否则只能手动修改数据库才能启用相关功能。

但也可以手动修改插件,让它默认启用「显示原(本)文链接」和「在文章显示」两个功能。即在插件 Plugin.php 中搜索 NULL, NULL, NULL,把第一个 NULL 改成 1 就行。

        $form->addInput($notice);
        $showURL = new Typecho_Widget_Helper_Form_Element_Checkbox('showURL', array(1 => _t('显示原(本)文链接')), 1, NULL, NULL);
        $form->addInput($showURL);
        $showOnPost = new Typecho_Widget_Helper_Form_Element_Checkbox('showOnPost', array(1 => _t('在文章显示')), 1, NULL, NULL);
        $form->addInput($showOnPost);
        $showOnPage = new Typecho_Widget_Helper_Form_Element_Checkbox('showOnPage', array(1 => _t('在独立页面显示')), NULL, NULL, NULL);
        $form->addInput($showOnPage);

镜像站

本来我不想写这一部分的,但怕不明真相的小伙伴们觉得我钱多没地方花买那么多域名。虽然我确实买了好些个域名。

也许我这 PHP 程序不咋会「时刻暴露在危险之中1,即使真的有危险我也不知道如何从日志发现蛛丝马迹,但我会自搜啊!所以就真让我逮到了这么俩家伙。这俩镜像站不像 Z酱那种直接请求源站数据,而是自己手把手,或者说用批量程序创建出来的全新站点。只是使用了我的名字、我的文章和我的图片而已,甚至没有直接使用来自小站的链接。所以我对它们完全没有办法。

第一个 盗版初之音 是今年五月份左右在博客后台的引用中发现的,不知道 Typecho 的哪个版本更新了的这一功能。总之多亏了它自投罗网,我才能第一时间发现这一复制站点。

我确实没想到过注册「初之音」的拼音域名,只是未曾预料这种防止商标被盗用的事竟然发生在了我身上。目前为止,它总共复制粘贴了我 11 篇文章、2张照片以及4个友链。

一开始我还想着及时发表声明,但冷静下来才发现,我对它根本无计可施。声明只会占用我宝贵的博客空间,大家好奇了还会过去点两下吸引走我所剩无几的流量。再说,它复制完那些文章之后就没有下一步动作了,所以我也就没去搭理它。

就是时间一长,谷歌已经把它收录了,还排在我下面。

另一个 盗版初之音 ……怎么说呢,它用了我的落地页不说,还扒走了我的随机图,自已开了个……

我大概就是在它建站的时间点,另一个图站经历了一次不小的图片请求,直接把我机子干爆了……

对于它我也是我无可奈何,只是希望它不要用我 ID 惹事生非。Z酱说被镜像的时候我还眼红自己咋没被镜像,是不是水的不够多。等到真被复制粘贴的时候却又头痛了……

最后

虽然这次看着像是干货满满,可很多代码我都看不懂。今年也是在 AI 大模型的加持下,我才得以对小站进行了这些修改。只是没有基础的代码知识,debug 起来还是困难重重的。我猜,肯定有值得优化的地方……不知道后续出问题的话,我还能不能依靠 AI 的力量修复小站的 bug。

总之,以上就是这一年的 Issue 啦!前阵子催 Z酱更新的时候,才注意到今年还有日志没水。那么,加上这篇日志,本年度博客的 KPI 就完成啦!感谢 Z酱 ヾ(≧∇≦*)ゝ或许我应该转变思路,想办法从Z 酱手里骗到电波站才对。

但是骗到手了我也不会修bug啊,好像陷入了死循环……

嘛,矫情的话就留到以后再说吧,让我们下个 Issue 再见ヾ( ̄▽ ̄)Bye~Bye~