普通视图

域名到期续不了费,我的域名在阿里云渡劫

2026年1月16日 18:16

最近遇到一个异常蛋疼的问题,有经验的兄弟伙给个建议,急,在线等!

话说我的域名 maie.name 是早些年在阿里云注册的,马上到期了,但是续不了费。也不是说这个域名有多好,当时 maie.com,maie.net,maie.cn都被人注册了,所以随意选了一个价格合适的 maie.name,就一直用到现在。

由于 .name 等域名未进入工信部英文顶级域名名单,所以在国内不受待见,无法续费、备案,现在阿里云建议我转到阿里云国际站(香港),系统也提供一键操作。当我在域名控制台点击操作时,在国际站自动创建了一个账户,但是域名没转成功,提示有“域名转移锁”。当我在域名管理中尝试关闭“域名转移锁”时,却提示此域名无任何操作权限。提交阿里云工单,有人给发了转出码。当我准备在国际站转入时提示“错误! 该账号仅限管理账号下已有域名,不能新注域名或购买阿里云国际站的其他产品。”,也就是说这个系统自动创建的账户没有手动转入的权限,需要自己手动注册一个新账户才能转入。不知道是不是阿里云把会干活的工程师都开除完了。这些都不重要,也不是不能解决。

还有问题,不一定钱能解决。阿里云国际站不支持他自己的支付宝支付,狠起来连自己都杀,佩服!

还有更多问题,如果转到国际站了,大概率是不支持解析到国内主机的,因为域名没备案;域名没备案的原因是域名备不了案;域名备不了案的原因是不给备案,海外也不用备案;就算海外能备案这个域名也无法备案,因为没在名单里……感觉陷入了死循环。

要不国内重新注册一个域名?换域名也很麻烦啊,要备案,URL 要换。

要不转到海外去?主要是国内使用啊,蛋疼的网速,像卡 bug 一样。况且还有那么图片要重新传,想想就头大。

要不干脆关站算了,懒得折腾!

我可能是个疯子

2026年1月19日 10:56

前几天在整理写博客以来的文字,涵盖了从大学至今的内容,因为电脑丢失过一次,所以高中以前的内容都不复存在。现在整理下来的内容,居然有 300 万字左右。


我一直深受一句话的影响,是一位美国《作家文摘》的编辑写下的一句话:“一位作者的立身之本并不是技巧,而是他写作的意愿和欲望。”以至于别人在问起我为什么要写作时,我只能用一句无奈于无法通过技巧获得成功的、但是又高度浓缩了意愿和欲望的结论回答道:“我喜欢写。”

我以前管理过一个“写作互助督促小组”。一开始是在豆瓣上集结了一群笔耕不辍的创作者,群的要求只有一个:我们只督促更新,不互相评价彼此发出来的文章,如果要互动请去作者的豆瓣。

那个时候我正在进行五百日写作计划,所以我每天都在发更新。一开始大家还饶有兴致地参与其中,渐渐地陪我日更的人越来越少,再后来人来人往,开始觉得我的每日更新是一种“压力”,最后他们都非常统一地在我发布更新之后,用“疯子”刷屏。再后来有新人加入时,我也会以“疯子”自我介绍。直到这个群包括我再也没有人发布过更新,我就解散了小组,解散时只剩下7个人,但也都搁笔好几年了。

当我开始决定要学习写剧本的时候,期间保留联系的朋友还半开玩笑地诅咒般告诫我:“我有个朋友也是写剧本的,把自己写猝死了。”这倒让我串联起一个小时候没看懂的剧情:

在宫崎骏的动画《侧耳倾听》里,当雫得知自己的小男友天泽圣司要去意大利学习手作小提琴时,她顿生的痛苦不是与情人分离,而是自己浑浑噩噩地过着国中的日子,却还没有找到自己值得一生追求的事情。看到小男友这么努力,雫也努力地开始想要创作一部小说。在写作的过程中,她经历了所有创作者都会经历的痛苦:不自信地永远在准备、灵感枯竭的自怜、对小说构思的自恋、让剧情晕染到现实的自我表演……直到她在图书馆翻开一页书,看到了一个在监狱里依旧做着小提琴的工匠,他借着牢房窗口投射的光,在一个暗无天日的牢笼里过完自己的一生,但手中的小提琴是他孑然一生的追求——那可能是天泽圣司的结局,也是自己想要一生追求写作的结局。


我还不至于是匠人,但我可能是一个合格的疯子。

就算如此,我也很难用常识来解释自己的坚持,哪怕是 300 万字的结果,也很难证明意愿和欲望这件事。它就像是小时候在沙坑里堆砌的城堡一样,我如果不推倒它,也总有人会去推倒它,也总有一场雨会让它夷为平地,甚至还会有死对头的小男孩为了不让我玩沙坑,索性在里面拉屎撒尿。

这两天收到一枚“苹果”,是因为前几天我发布的文章而获得灵感的朋友,在他的博客引发的思考——《我为什么写博客》,他最后得出的结论是“给自己”。

我无法证明“给自己”的欲望,人们也无法理解“给自己”的意义,他们互相都无法覆盖对方的“正确答案”,而这个对抗的狭缝,就是写作的乐趣。就像一个女人哭,不是什么大事,甚至每个人都有自己的定论,但这个哭泣的女人脸上沾满了鲜血,手上还拿着一把正在滴血的刀时,那么人人都是莎士比亚!

然后呢,没有然后,因为那个女人在现实里早就被抓了,而在小说里正在经历怎样的故事,至少得想把她写下来,而不是“我想到的比你更精彩”。

也是这个狭缝,就是自己这个疯子的乐趣。


值得自我反驳的点:

  • 300 万字并不能证明「疯」,只能证明我更加适应孤独;坚持并不是一件高尚的时,而是赋予写作的浪漫标签;
  • 写作互助督促小组并不是一个通过“别人无法坚持”,从而证明自己是“正确”的途径,只是人们发现这条路径不值得继续投入;
  • 比起“为什么要写?”不如追问一个问题“如果有一天我不得不停下来,我会失去什么?”

域名到期续不了费,我的域名在阿里云渡劫

2026年1月16日 18:16

最近遇到一个异常蛋疼的问题,有经验的兄弟伙给个建议,急,在线等!

话说我的域名 maie.name 是早些年在阿里云注册的,马上到期了,但是续不了费。也不是说这个域名有多好,当时 maie.com,maie.net,maie.cn都被人注册了,所以随意选了一个价格合适的 maie.name,就一直用到现在。

由于 .name 等域名未进入工信部英文顶级域名名单,所以在国内不受待见,无法续费、备案,现在阿里云建议我转到阿里云国际站(香港),系统也提供一键操作。当我在域名控制台点击操作时,在国际站自动创建了一个账户,但是域名没转成功,提示有“域名转移锁”。当我在域名管理中尝试关闭“域名转移锁”时,却提示此域名无任何操作权限。提交阿里云工单,有人给发了转出码。当我准备在国际站转入时提示“错误! 该账号仅限管理账号下已有域名,不能新注域名或购买阿里云国际站的其他产品。”,也就是说这个系统自动创建的账户没有手动转入的权限,需要自己手动注册一个新账户才能转入。不知道是不是阿里云把会干活的工程师都开除完了。这些都不重要,也不是不能解决。

还有问题,不一定钱能解决。阿里云国际站不支持他自己的支付宝支付,狠起来连自己都杀,佩服!

还有更多问题,如果转到国际站了,大概率是不支持解析到国内主机的,因为域名没备案;域名没备案的原因是域名备不了案;域名备不了案的原因是不给备案,海外也不用备案;就算海外能备案这个域名也无法备案,因为没在名单里……感觉陷入了死循环。

要不国内重新注册一个域名?换域名也很麻烦啊,要备案,URL 要换。

要不转到海外去?主要是国内使用啊,蛋疼的网速,像卡 bug 一样。况且还有那么图片要重新传,想想就头大。

要不干脆关站算了,懒得折腾!

2026年了 你还在写博客嘛? 我的拖延症

前几天看到一则新闻,说 Stack Overflow 现在的流量,甚至还不如它刚成立后第一个月的水平。连 SO 都如此,更不用说其他一些网站了。这些年在 AI 的冲击下,很多网站陆续关停,甚至连纸媒也没能幸免,前不久停刊的《电脑报》或《电脑爱好者》,都让人唏嘘。 可以说,这些都是 AI 的“受害者”。大语言模型把互联网上积累多年的内容“吃”了个遍,变得异常强大,也彻底改变了人们获取信息的习惯。以前是 Google 搜索,点开链接自己慢慢找答案;现在更多时候是直接问 AI,马上就能得到结果。AI 就像一个无所不知、无所不能的个人助手,还会记住你的兴趣和使用习惯,变得越来越顺手。 很多博客因此停止了更新,我倒还在坚持。一方面是把博客当作公开的笔记本,也当成一个数据库;另一方面,虽然很多时候写着写着会觉得像是在对空气呐喊,但我并不在意,有没有读者都无所谓。平时写点东西,也算是一种脑力训练。 日记/笔记、照片这些,其实都是很珍贵的数据,用博客来整理和记录是个不错的方式。以前还能顺便赚点广告费,现在基本没什么了。不过我也屏蔽了 AI 的爬虫——毕竟我不太想让 AI 白白来“薅”我的数据。 其实我心里一直有一些想写的主题,但总是拖延。比如旅行,拍了很多照片,却懒得整理,时间一久就更提不起劲了;有时候又担心自己写得不够好,总想着再想一想、再准备一下,结果就一直拖着没动笔。 我之前看到一句话:不管做得好不好,只要开始做,就已经成功了一半。希望 2026 年的我,能在这件事上有所改善吧——毕竟,我的 2025 年总结到现在都还没写完…… [caption id="attachment_70801" align="alignnone" width="1536"]全家福 ChatGPT (Ghibli风格) 全家福 ChatGPT (Ghibli风格)[/caption]

相关文章:

  1. 第一次私校家长会: 原来家长比孩子还卷 前几天参加了娃的第一次家长会,和几位家长聊下来,真是个个都很厉害。不光孩子们卷,家长也一样卷,一眼望去基本都是 Dr/博士。娃还调侃我一句:“这有什么的,你不也是 Dr 吗?” 我心里默默想:还好没写学校名字,不然我这野鸡大学的头衔真拿不出手 😂。 私校里真是人才济济,乐器过 8 级的太常见了,卷得不得了。我还问过娃,是想当 big fish in a small pond...
  2. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  3. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  4. 微信PC端程序占用了1.39 TB的空间! 快速清理微信占用空间 前两天我的 C 盘剩余空间突然变红了,我随手一查,竟然发现微信 PC 端程序居然占用了 1.39 TB 的空间,简直不可思议。在手机上,微信同样是名列前茅的“吞空间大户”,在 设置 → 通用 → 手机存储空间 里几乎稳居第一。 更离谱的是,这些空间大多并不是因为聊天记录,而是各种缓存文件、视频、图片和被动接收的文件所堆积起来的。平时我们只是点开看一眼,就算没保存下来,微信也会悄悄把它们留在本地,占据大量磁盘。尤其是群聊里转发的视频和文件,日积月累就成了一个“隐形黑洞”。...
  5. 英国房子的EPC节能报告(Energe/Efficiency Performance Certificate) EPC (Energe/Efficiency Performance Certificate) 是英国房子的节能报告, 法律上规定, 每个房子都必须要有一个EPC报告, 报告的有效期为十年. 房东在把房子出租或者想卖房的时候, 这个EPC就必须有效, 在一些情况下 比如出租房子的时候, 这个EPC报告还必须符合一些最低标准, 比如房子必须满足 F档(类似及格线)...
  6. 在英国给孩子换学校的经历: 孩子离开了村里的小学 由于搬了家, 孩子上学得提前半小时出门了, 因为早上堵, 也得开车半小时才能到. 之前在 Fen Drayton 村庄上小学, 早上8:45学校门开, 9点敲钟孩子排队依次进入教室, 我们由于在村里, 只需要提前5分钟出门和孩子一起走路就可以了. 现在一下子早上变得很匆忙, 得叫孩子起床, 做早饭,...
  7. 这周第一次参加微软的Hackathon/黑客马拉松 这周我第一次参加微软的 Hackathon(黑客马拉松)。其实像微软、Amazon、Meta 这些科技大厂,每年都会举办 Hackathon,算是企业文化的一部分。微软的 Hackathon 一般在九月,持续三天,工程师和研究员们可以自由组队,围绕“Build”和“Hack”这两个主题搞一些有意思的项目。三天时间不太可能做出成熟的产品,所以重点是做一个 Prototype,最后再提交视频等材料参与评选。 去年也有一次 Hackathon,不过不是全公司级别的,没有奖项,但我还是折腾了一下,当作学习和玩乐。再往前两年,有个美国同事拉我进了他的 Hackathon 小组,但因为时差原因,我没能真正参与,只是顺手领了一件活动T-shirt衣服。 说到领衣服,今年周一在楼下就能领取,但需要刷工牌确认是参赛人员,每人限一件。本来我还想着能多领一件给我媳妇,可惜不行。 PS:这一周感觉比平时更忙更累。因为每天都去公司。 更新:竟然获得了当地(也就是剑桥/local)的奖,(团队所有成员)得到了一个杯子,不过这个杯子连个公司的LOGO都没有,上面写着 “>...
  8. 面向猫猫编程 Cat Oriented Programming (Chessly/Pyro这一生持续更新) 家里有两只猫 Chessly/Pyro,想着找个地方记录它们的生活,最后决定还是写在这里的博客。猫的一生很短,差不多也就二十年。 Chessly(黑白猫)是我加入微软剑桥研究院MSRC第一个月带回家的,过了两三个月,又把Pyro(橘猫)也接回了家。两只猫的名字是孩子们取的:Chessly因为黑白的像棋盘,加上“ly”听起来像个女孩的名字;而Pyro的意思是一团火(烟火),充满活力。 刚开始的时候,Chessly特别喜欢待在我的工作区域。她有时候趴在键盘上或旁边,有时候藏在显示器后面。偶尔还会绕到我身边“咕咕”地撒娇,等着我去摸她。有时更干脆跑到我腿上,舒舒服服地躺着。 不过,现在它们俩的体型都大了很多,躺在桌上就会挡住屏幕,真是“面向猫猫编程”(Cat Oriented Programming)的极致体验。 记录生活的点滴,也是一种珍惜,毕竟这二十年,我们会一起走过。 2024年 2025年 Ring视频:两猫日常就是打闹,Chessly追上Pyro想舔他,在猫的世界里,地位高的才能舔地位低的。 我家猫现在越来越胖,很喜欢在我工作的时候躺在显示器钱,很影响我的工作,不过这时候我就是会休息一下摸摸她,就当放松一下了。 Pyro在窗边喝水,这是个小的煮饭锅,现在不用了,就给猫当喝水的碗。Pyro很胆小,经常看到我就跑。没法跑就咕咕叫。 Chessly很喜欢陪我工作,然后她很好厅的盯着屏幕上的鼠标光标,真怕她把屏幕抓坏了。 哥哥弹琴,弟弟唱歌,Chessly午睡,真是幸福啊,下辈子做只猫吧。...

VitePress 集成 Tailwind CSS

VitePress 集成 Tailwind CSS ​

随着 AI模型 对 Tailwind CSS 的高频率使用和适配,当你使用 AI 编程的时候,不使用 Tailwind CSS 都感觉有点吃亏。这算是一个很好的的理由吧,😜。 说说优点:

  1. 使用 TailwindCSS 让 AI 更懂你,也可以更好的使用 AI
  2. 可以遵循 TailwindCSS 的样式规范,代码量减少,省心省力

1. 安装依赖 ​

bash
pnpm install tailwindcss @tailwindcss/vite
pnpm install -D autoprefixer

2. 初始化和配置 ​

初始化 Tailwind CSS,然后在主题配置中导入。

友情提示

@theme 里配置好 Tailwind CSS 的变量,然后就可以直接在 class 里使用了,比如: border-border,相当于 border-(--vp-c-border)

css
@import 'tailwindcss';
/* 适配主题 */
@variant dark (.dark &);

/* 定义 css 变量和使用Tailwind 变量 */
:root {
  --border-color: var(--vp-c-border);
  --shadow-color: theme('colors.slate.200');
  --icon-color: theme('colors.neutral.900');
}

/* Tailwind 主题配置 */
@theme {
  --color-border: var(--border-color);
  --color-shadow: var(--shadow-color);
  --color-icon: var(--icon-color);
}
ts
import './style/tailwind.css'

同时,在 VitePress 的 config 中配置 Tailwind CSS。

.vitepress/config/index.ts
ts
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  vite: {
    plugins: [tailwindcss()]
  }
})

以上就是所有的集成步骤了,是不是很简单!

分享我的配置 ​

这里分享下我的配置,因为我后续的组件都或多或少使用到了这部分 CSS 变量

.vitepress/theme/style/tailwind.css
css

VitePress 添加 Vercount 统计

VitePress 添加 Vercount 统计 ​

由于 busuanzi 统计插件今年曾罢工了一段时间,然后就转到了 Vercount 统计。使用 Vercount 统计插件,发觉它功能更丰富,自定义程度高,兼容 busuanzi,还有统计面板,后续就一直使用了。这里说一下集成 Vercount 统计插件的过程,其实是跟 busuanzi 高度类似的。

提示

项目使用 Tailwind 进行了重构,本文组件样式都对此有所依赖,参见 我的 Tailwind 配置。如果不想使用 Tailwind,可以直接把代码扔给 AI,让其转化为一般 css

1. 引入 Vercount 的 JS 文件并调用 ​

由于 Vercount 暂不支持 npm 安装,所以这里我们对引入稍作处理。

新建 useVisitData.ts 文件,然后再主题中心调用

ts
ts
import DefaultTheme from 'vitepress/theme'
import { EnhanceAppContext, inBrowser } from 'vitepress'
import useVisitData from './hooks/useVisitData' // 网站访问统计 vercount

export default {
  extends: DefaultTheme,
  enhanceApp({ app, router }: EnhanceAppContext) {
    if (inBrowser) {
      // 访问量统计,路由加载完成,在加载页面组件后(在更新页面组件之前)调用
      router.onAfterPageLoad = () => {
        useVisitData()
      }
    }
  }
}

2. 显示网站统计 ​

使用方式跟 busuanzi 一致,对应 ID 显示对应统计量。而且 Vercount 兼容 Busuanzi 的 span 标签,数据会在首次访问时自动同步。

也就是说你仍然可以使用原来 busuanzi 的 ID,并且数据可以继承过来。

Vercount ID busaunzi ID 说明
vercount_value_site_pv busuanzi_value_site_pv 全站访问量
vercount_value_site_uv busuanzi_value_site_uv 全站访客量
vercount_value_page_pv busuanzi_value_page_pv 单个网页访问量

3. 定制统计组件 ​

对于我们之前的统计组件,这里也做了代码更新和优化。注意事项:

  1. 样式使用了 tailwind4
  2. 使用 setTimeout 是防止页面加载未完成时,不蒜子脚本已执行成功,从而无法获取统计数据
  3. WStatistics.vue 组件注册到主题配置 .vitepress/theme/index.ts 中去,就可以全局使用了,要么就局部引用局部使用

1. 组件创建 ​

vue
ts

2. 全局注册和使用 ​

ts
import DefaultTheme from 'vitepress/theme'
import WStatistics from './components/WStatistics.vue'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    // 注册自定义全局组件
    app.component('weiz-statistics', WStatistics)
  }
}
vue
<template>
  <div>
    <weiz-statistics />
  </div>
</template>

4. 单个网页统计 &ZeroWidthSpace;

参见 busuanzi - 单个网页统计,为单个文章显示统计信息

1. 组件创建 &ZeroWidthSpace;

vue
ts

2. 全局注册 &ZeroWidthSpace;

.vitepress/theme/index.ts
ts
import DefaultTheme from 'vitepress/theme'
import WDocTitleMeta from './components/WDocTitleMeta.vue' //文章顶部

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    // 注册自定义全局组件
    app.component('weiz-title-meta', WDocTitleMeta)
  }
}

3. 调用方法 &ZeroWidthSpace;

参考 VitePress 的高级配置,我们在 Markdown 渲染器 里进行拦截,监听到有 h1 标签时,将此组件插入在 h1 后面

.vitepress/config/index.ts
ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  //markdown配置
  markdown: {
    // 对markdown中的内容进行替换或者批量处理
    config: (md) => {
      // 创建 markdown-it 插件
      md.use((md) => {
        // 组件插入h1标题下
        md.renderer.rules.heading_close = (tokens, idx, options, env, slf) => {
          let htmlResult = slf.renderToken(tokens, idx, options)
          if (tokens[idx].tag === 'h1') htmlResult += `<weiz-title-meta />`
          return htmlResult
        }
      })
    }
  }
})

VitePress 建站资源汇总

VitePress 建站资源汇总 &ZeroWidthSpace;

整理下使用 VitePress 搭建博客过程中使用过的一些资源和方案

主要参考站点 &ZeroWidthSpace;

VitePress 官方文档

VitePress快速上手中文教程,这个站点扩展很全,包括静态部署选择,样式美化,第三方插件等,都是手把手教程,很细

XaviDocs个人技术文档,借鉴了部分 VitePress 的使用经验

VitePress主题 vitepress-theme-async 源码,借鉴了部分对 VitePress 的配置和操作逻辑,阅读这些代码对你理解 VitePress 会有很大帮助

晚阳Crown,借鉴了使用 Cloudflare R2 搭建图床以及使用 Cloudflare 管理域名的经验

基础配置和样式美化 &ZeroWidthSpace;

暗黑模式切换动画 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/basic/dark-animation

链接前加图标 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/basic/link-icon

优化代码块为mac风格 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/basic/code-block

优化自定义容器 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/basic/custom-block

使用 DocSearch 搜索 &ZeroWidthSpace;

官方教程 https://vitepress.dev/zh/reference/default-theme-search#algolia-search

详细教程 https://vitepress.yiov.top/docsearch.html

点击查看我的配置
ts
import type { DefaultTheme } from 'vitepress'

export const algolia: DefaultTheme.AlgoliaSearchOptions = {
  appId: 'appid',
  apiKey: 'apikey',
  indexName: 'weizwz',
  placeholder: '搜索文档',
  translations: {
    button: {
      buttonText: '搜索文档',
      buttonAriaLabel: '搜索文档'
    },
    modal: {
      searchBox: {
        resetButtonTitle: '清除查询条件',
        resetButtonAriaLabel: '清除查询条件',
        cancelButtonText: '取消',
        cancelButtonAriaLabel: '取消'
      },
      startScreen: {
        recentSearchesTitle: '搜索历史',
        noRecentSearchesText: '没有搜索历史',
        saveRecentSearchButtonTitle: '保存至搜索历史',
        removeRecentSearchButtonTitle: '从搜索历史中移除',
        favoriteSearchesTitle: '收藏',
        removeFavoriteSearchButtonTitle: '从收藏中移除'
      },
      errorScreen: {
        titleText: '无法获取结果',
        helpText: '你可能需要检查你的网络连接'
      },
      footer: {
        selectText: '选择',
        navigateText: '切换',
        closeText: '关闭',
        searchByText: '搜索提供者'
      },
      noResultsScreen: {
        noResultsText: '无法找到相关结果',
        suggestedQueryText: '你可以尝试查询',
        reportMissingResultsText: '你认为该查询应该有结果?',
        reportMissingResultsLinkText: '点击反馈'
      }
    }
  }
}

新建页面和注册组件 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/basic/pages-components

功能扩展 &ZeroWidthSpace;

添加图片查看器 Fancybox &ZeroWidthSpace;

Fancybox 是一款非常流行且功能强大的 JavaScript 图片查看库

集成参考 https://note.weizwz.com/vitepress/extend/vitepress-fancybox

添加不蒜子统计 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/extend/busuanzi

添加 Vercount 统计 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/extend/vercount

文章统计,自定义首页,归档页,标签页 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/extend/post-data

其他参考资料:

vitepress-theme-async 主题文章统计

添加友链界面 &ZeroWidthSpace;

参考 https://note.weizwz.com/vitepress/extend/links

部署相关 &ZeroWidthSpace;

静态部署 &ZeroWidthSpace;

部署到 GitHub Pages ,参考文章

https://vitepress.yiov.top/assets.html#部署

基本流程: 项目根目录建立文件 .github/workflows/main.yml,增加自动部署配置,代码提交, GitHub 监测到工作流后自动执行

点击查看我的配置
yml
# 将静态内容部署到 GitHub Pages 的简易工作流程
name: Deploy static content to Pages

# env:
#   VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
#   VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

on:
  # 仅在推送到默认分支时运行。
  push:
    branches: ['main'],暂停部署到 GitHub pages

  # 这个选项可以使你手动在 Action tab 页面触发工作流
  workflow_dispatch:

# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages。
permissions:
  contents: read
  pages: write
  id-token: write

# 允许一个并发的部署
concurrency:
  group: 'pages'
  cancel-in-progress: true

jobs:
  # 构建
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          persist-credentials: false
          fetch-depth: 0
  # github page deploy
      - name: Set up pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - name: Setup Pages
        uses: actions/configure-pages@v4
      - name: Install dependencies
        run: pnpm install
      - name: Build
        run: pnpm run build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          # Upload dist repository
          path: './dist'

  # 部署
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

      # vercel deploy
      # - name: Restore file modification time 🕒
        # run: find docs/post -name '*.md' | while read file; do touch -d "$(git log -1 --format="@%ct" "$file")" "$file"; done
        # run: "git ls-files -z | while read -d '' path; do touch -d \"$(git log -1 --format=\"@%ct\" \"$path\")\" \"$path\"; done"
      # - name: Install Vercel-cli🔧
      #   run: npm install --global vercel@latest
      # - name: Pull Vercel Environment Information
      #   run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
      # - name: Build Project Artifacts
      #   run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
      # - name: Deploy Project Artifacts to Vercel
      #   run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

这个配置,也是可以部署到 Vercel 的,注释掉 github-pages 部分,放开后面的 vercel deploy 部分即可。需要注意构建命令和输出目录,原始构建命令为 npm run docs:build,输出目录 docs/.vitepress/dist

添加 Github Giscus 评论系统 &ZeroWidthSpace;

Github Giscus 的好处简单易用,限制的地方在于需要仓库公开,代码必须托管在 Github 上。可参考我的这篇文章

https://note.weizwz.com/vitepress/extend/vitepress-giscus

其他参考链接:

Github Giscus官网

VitePress 添加评论功能

使用 Cloudflare 管理域名 &ZeroWidthSpace;

参考这篇文章

https://www.oneyangcrown.top/posts/cloudflare-hosted-domain-cdn-ssl-guide/

如果你的域名在阿里云或者腾讯云,找到域名解析,修改DNS服务器地址为 Cloudflare 上的即可

使用 Cloudflare R2 配置图床 &ZeroWidthSpace;

参考这篇文章,先配置 R2,然后使用自定义域名。

https://www.oneyangcrown.top/posts/cloudflare-r2-free-image-hosting-guide/

注意,配置过程中创建的 API令牌,访问密钥等,需要我们储存起来,方便后续在 PicList 中使用;

第二个需要注意的是,图床我们一般使用 子域名。比如,你在 Cloudflare 上管理的域名为 weizwz.com,R2 的自定义域名你可以输入 p.weizwz.com,然后 Cloudflare 会自动给你添加 子域名解析,实在是在贴心了。

关于图床API的使用,就是上传和删除等操作,这里建议使用工具 PicList,官网文档很详细,包括下载和配置等

https://piclist.cn/app.html

由于我们使用的是 Cloudflare R2,重点查看这里 PicList 内置AWS S3 配置

上传配置里,我们可以设置水印,上传时压缩图片,还有图片转换为 webp 格式,进一步减小图片体积

还可以上传配置到代码仓,方便切换设备后同步配置。在 github/gitee/gitea 中创建一个私有仓库,然后将 PicList 配置上传,参考 设置配置同步

配置同步需要的 GitHub Token,点击 GitHub 上的个人头像,Settings,拉到最下面点击 Developer settings,然后会跳转到新界面,按下图创建

创建好后,复制 Token,粘贴到 PicList 配置里,然后就可以上传你的配置到仓库了。

使用 Cloudflare Pages 托管网站 &ZeroWidthSpace;

参考这篇文章

https://www.oneyangcrown.top/posts/cloudflare-pages-deploy-hexo-blog-guide/

文章中的博客使用的是 Hexo 框架 不是 Vitepress,因此我们需要修改构建命令。VitePress 默认构建命令默认是 npm i && vitepress build docs,输入目录 docs/.vitepress/dist

我对 VitePress 原始构建配置有修改,我的构建命令是 pnpm i && pnpm run build,构建输出 ./dist。如果你也有修改的话,需要修改为你自己的配置。这个配置在 package.json 中的 scripts 中可以看到。

构建监听分支默认为主分支 main,当 main 分支有修改时,Cloudflare 将自动构建部署。

使用 Twikoo 评论系统 &ZeroWidthSpace;

Twikoo 评论,需要部署前后端,包括数据库,操作略有复杂,但是不限制代码托管和服务托管,自定义程度更高

官方文档 https://twikoo.js.org/ ,非常详细,部署步骤如下:

1. 先建立 MongoDB Atlas 数据库

2. 然后选择云厂商部署,我这里选择的 Vercel 部署,因为操作相对简单,使用 Cloudflare workers 部署 限制较多,部署成功后,修改默认域名为你的子域名,需要去你的域名管理那上修改 DNS 解析。

以 Cloudflare 为例,添加 CNAME 类型,子域名 twikoo,内容 cname.vercel-dns.com

3. 后端部署完成后,再集成到前端项目

修改我们的 VitePress 项目,参考这篇 VitePress 集成 Twikoo 评论

前端集成基本步骤:pnpm install twikoo -> 封装 Twikoo.vue 组件 -> 插入 Layout 插槽

4.配置邮件。前后端都处理好后,界面就能正常展示了,但是我们还要处理下邮件功能

首次打开设置按钮后,会有设置密码框,设置一个复杂密码并记住。然后进入配置管理,选择邮件通知

按照提示输入你的邮箱,邮箱授权码等即可。最后有个邮件测试,测试后,你能收到一封邮件,说明功能可用了

VitePress 添加不蒜子统计

VitePress 添加不蒜子统计 &ZeroWidthSpace;

不蒜子 是一个极简的网页计数器,支持统计全站访客和访问量,支持单个网页访问量,免费稳定。先上定制效果图:

1. 安装插件 &ZeroWidthSpace;

sh
pnpm add -D busuanzi.pure.js

2. 调用统计 &ZeroWidthSpace;

要触发不蒜子统计,需要我们在项目路由切换后,手动调用它的方法,从而向不蒜子后台发送请求,增加计数。所以我们修改主题配置 .vitepress/theme/index.ts

ts
import DefaultTheme from 'vitepress/theme'
import { inBrowser } from 'vitepress'
import busuanzi from 'busuanzi.pure.js'

export default {
  extends: DefaultTheme,
  enhanceApp({ app, router }) {
    if (inBrowser) {
      // 访问量统计,路由加载完成,在加载页面组件后(在更新页面组件之前)调用
      router.onAfterRouteChanged = () => {
        busuanzi.fetch()
      }
    }
  }
}

3. 显示统计量 &ZeroWidthSpace;

按不蒜子的使用说明,只要页面中出现它配套的 id,js 就会自动填充数字到对应id的元素中。

ID 说明
busuanzi_value_site_pv 全站访问量
busuanzi_value_site_uv 全站访客量
busuanzi_value_page_pv 单个网页访问量

简单示例,页面中有如下内容即可

html
本站总访问量 <span id="busuanzi_value_site_pv" /> 本站访客数 <span id="busuanzi_value_site_uv" />

4. 定制统计组件 &ZeroWidthSpace;

以上使用简单,但是不好看,没有什么设计可言。这里我们设计一个统计卡片,数字可以跳动,有逐渐增长的进度条;并且我们将统计数据存储在 sessionStorage 中,当切换页面后,数据有增长时,数字还会跳动,进度条也会增长。

直接贴代码吧,注意:

  1. 其中 scss 中的一些变量基本使用 VitePress 自带变量,无需担心
  2. 使用 setTimeout 是防止页面加载未完成时,不蒜子脚本已执行成功,从而无法获取统计数据
  3. WStatistics.vue 组件注册到主题配置 .vitepress/theme/index.ts 中去,就可以全局使用了,要么就局部引用局部使用
vue
<template>
  <div class="statistics">
    <div class="title-wrapper">
      <div class="title">
        <span>访问统计</span>
        <span class="title-hover">访问统计</span>
      </div>
      <i class="weiz-icon weiz-icon-chart-line main"></i>
    </div>
    <div class="statistics-main">
      <div class="statistics-wrapper">
        <span class="statistics-title">总访问量</span>
        <span class="statistics-pv" id="pv">{{ pv }}</span>
      </div>
      <div class="chart pv-wrapper">
        <div class="pv-num" id="pvProgress" style="width: 70%"></div>
      </div>
      <div class="statistics-wrapper">
        <span class="statistics-title">独立访客</span>
        <span class="statistics-uv" id="uv">{{ uv }}</span>
      </div>
      <div class="chart uv-wrapper">
        <div class="uv-num" id="uvProgress" style="width: 45%"></div>
      </div>
    </div>
    <span id="busuanzi_value_site_pv" style="display: none" />
    <span id="busuanzi_value_site_uv" style="display: none" />
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getSessionStorage, setSessionStorage, numberWithCommas } from '../../utils/tools'

let sessionPv = getSessionStorage('pv')
let sessionUv = getSessionStorage('uv')
const pv = ref<string | number>(sessionPv ? numberWithCommas(parseInt(sessionPv)) : 'loading')
const uv = ref<string | number>(sessionUv ? numberWithCommas(parseInt(sessionUv)) : 'loading')

let timeoutPV = 0
const getPV = () => {
  if (timeoutPV) clearTimeout(timeoutPV)
  timeoutPV = window.setTimeout(() => {
    const $PV = document.querySelector('#busuanzi_value_site_pv')
    const text = $PV?.innerHTML
    if ($PV && text) {
      const start = getSessionStorage('pv') || '1000'
      pv.value = numberWithCommas(parseInt(text))
      setSessionStorage('pv', text)
      // 调用封装的函数
      animateNumberAndProgressBar({
        counterSelector: '#pv',
        fillBarSelector: '#pvProgress',
        start: parseFloat(start),
        end: parseInt(text),
        totalDuration: 2000,
        minPercentage: 5,
        targetPercentage: 75
      })
    } else {
      getPV()
    }
  }, 500)
}

let timeoutUV = 0
const getUV = () => {
  if (timeoutUV) clearTimeout(timeoutUV)
  timeoutUV = window.setTimeout(() => {
    const $UV = document.querySelector('#busuanzi_value_site_uv')
    const text = $UV?.innerHTML
    if ($UV && text) {
      const text = $UV.innerHTML
      const start = getSessionStorage('uv') || '1000'
      uv.value = numberWithCommas(parseInt(text))
      setSessionStorage('uv', text)
      // 调用封装的函数
      animateNumberAndProgressBar({
        counterSelector: '#uv',
        fillBarSelector: '#uvProgress',
        start: parseFloat(start),
        end: parseInt(text),
        totalDuration: 2000,
        minPercentage: 5,
        targetPercentage: 50
      })
    } else {
      getUV()
    }
  }, 500)
}

// 统计数字动画
const animateNumberAndProgressBar = ({ counterSelector, fillBarSelector, start = 0, end, totalDuration = 2000, minPercentage = 5, targetPercentage = 75 }) => {
  // 如果开始和结束的数字相同,直接返回
  if (start == end) {
    return
  }
  // 调整进度条起始位置,要基本符合进度条的长度
  const maxNum = (end * 100) / targetPercentage
  let startPercentage = (start / maxNum) * 100

  const counterElement = document.querySelector(counterSelector)
  const fillBarElement = document.querySelector(fillBarSelector)

  let startTime = null
  const totalSteps = end - start

  function animateCounter(timestamp) {
    if (!startTime) startTime = timestamp
    const elapsed = timestamp - (startTime ?? timestamp)

    const progress = Math.min(elapsed / totalDuration, 1)
    const currentNumber = Math.floor(start + progress * totalSteps)
    let stepPercentage = progress * (targetPercentage - startPercentage)
    // 保证肉眼能看到至少5%的变化
    if (targetPercentage - startPercentage < minPercentage) {
      stepPercentage = progress * minPercentage
      startPercentage = targetPercentage - minPercentage
    }

    const currentProgress = startPercentage + stepPercentage

    counterElement.textContent = numberWithCommas(currentNumber)
    fillBarElement.style.width = currentProgress + '%'

    if (fillBarElement.style.display !== 'block') {
      fillBarElement.style.display = 'block'
    }

    if (progress < 1) {
      requestAnimationFrame(animateCounter)
    }
  }

  fillBarElement.style.width = startPercentage + '%'
  fillBarElement.style.display = 'block'

  requestAnimationFrame(animateCounter)
}

onMounted(() => {
  getUV()
  getPV()
})
</script>

<style lang="scss" scoped>
.statistics {
  width: 100%;
  display: inline-block;
  border-radius: 16px;
  background-color: var(--vp-c-bg);
  color: var(--vp-c-text-1);
  font-weight: 500;
  padding: 24px;
  box-shadow: 0 1px 2px 0 rgba(25, 26, 28, 0.05);
  transition: all cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.6s;
  &:hover {
    color: var(--vp-c-text-1);
    transform: scale(1.03);
    box-shadow:
      0 10px 15px -3px rgba(25, 26, 28, 0.1),
      0 4px 6px -4px rgba(25, 26, 28, 0.1);
    .title .title-hover {
      width: 100%;
    }
  }
  .title-wrapper {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 32px;
  }
  .title {
    font-size: 16px;
    line-height: 1.5;
    font-weight: 600;
    white-space: nowrap;
    position: relative;
    overflow: hidden;
    .title-hover {
      position: absolute;
      left: 0;
      top: 0;
      width: 0;
      overflow: hidden;
      background-color: var(--vp-c-bg);
      color: #409eff;
      transition: width 0.4s ease-in-out;
    }
  }
  .statistics-wrapper {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 8px;
  }
  .chart {
    height: 8px;
    border-radius: 8px;
    background-color: var(--vp-c-gray-3);
    > div {
      height: 100%;
      border-radius: 8px;
      background-color: #409eff;
    }
  }
  .pv-wrapper {
    margin-bottom: 16px;
  }
  .statistics-title {
    & + span {
      font-size: 16px;
      line-height: 1.5;
      font-weight: 600;
    }
  }
}
</style>
ts

切换路由的效果展示

5. 单个网页统计 &ZeroWidthSpace;

单个网页只显示访问量太单薄,因此我们还增加了额外信息,如文章创建更新时间,文章字数等

直接贴代码,注意事项同上

vue
<template>
  <div class="weiz-title-meta">
    <div class="tags">
      <div class="created" title="发表于">
        <i class="weiz-icon weiz-icon-created gray" />
        <span>发表于 {{ firstCommit }}</span>
      </div>
      <div class="updated" title="更新于">
        <i class="weiz-icon weiz-icon-updated gray" />
        <span>更新于 {{ lastUpdated }}</span>
      </div>
      <div class="word" title="字数">
        <i class="weiz-icon weiz-icon-word gray" />
        <span>字数 {{ wordCount }}</span>
      </div>
      <div class="reader" title="阅读量">
        <i class="weiz-icon weiz-icon-user gray"></i>
        <span>阅读量 {{ pv }}<span id="busuanzi_value_page_pv" style="display: none" /></span>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useData } from 'vitepress'
import { ref, onMounted } from 'vue'
import { countWord, countTransK, formatDate } from '../../utils/tools'

const { frontmatter, page } = useData()
const wordCount = ref('')
const firstCommit = ref('')
const lastUpdated = ref('')
const pv = ref('')

let timeoutPV = 0
const getPV = () => {
  if (timeoutPV) clearTimeout(timeoutPV)
  timeoutPV = window.setTimeout(() => {
    const $PV = document.querySelector('#busuanzi_value_page_pv')
    const text = $PV?.innerHTML
    if ($PV && text) {
      pv.value = countTransK(parseInt(text))
    } else {
      getPV()
    }
  }, 500)
}

onMounted(() => {
  const dateOption = formatDate()
  firstCommit.value = dateOption.format(new Date(frontmatter.value.firstCommit!)).replace(/\//g, '-')
  lastUpdated.value = dateOption.format(new Date(frontmatter.value.lastUpdated || page.value.lastUpdated!)).replace(/\//g, '-')

  const docDomContainer = window.document.querySelector('#VPContent')
  const words = docDomContainer?.querySelector('.content-container .main')?.textContent || ''
  wordCount.value = countTransK(countWord(words))

  getPV()
})
</script>

<style lang="scss" scoped>
.weiz-title-meta {
  .tags {
    display: flex;
    flex-wrap: wrap;
    margin: 0 0 32px;
    color: var(--vp-c-text-2);
    font-weight: 500;
    line-height: 18px;
    word-break: keep-all;
    > div {
      display: flex;
      align-items: center;
      margin-top: 16px;
      margin-right: 6px;
      &:last-child {
        margin-right: 0;
      }
    }
  }
  .weiz-icon {
    margin-right: 2px;
  }
}

@media (min-width: 768px) {
  .weiz-title-meta .tags > div {
    margin-right: 16px;
  }
}
</style>
ts

组件创建好后,还需要在全局注册为组件,方便我们使用

.vitepress/theme/index.ts
ts
import DefaultTheme from 'vitepress/theme'
import WDocTitleMeta from './components/WDocTitleMeta.vue' //文章顶部

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    // 注册自定义全局组件
    app.component('weiz-title-meta', WDocTitleMeta)
  }
}

WDocTitleMeta.vue 组件需要注册到配置中心后,怎么插入到单个文章页面中去呢。这就需要用到 VitePress 的高级配置,我们在 Markdown 渲染器 里进行拦截,监听到有 h1 标签时,将此组件插入在 h1 后面

.vitepress/config/index.ts
ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  //markdown配置
  markdown: {
    // 对markdown中的内容进行替换或者批量处理
    config: (md) => {
      // 创建 markdown-it 插件
      md.use((md) => {
        // 组件插入h1标题下
        md.renderer.rules.heading_close = (tokens, idx, options, env, slf) => {
          let htmlResult = slf.renderToken(tokens, idx, options)
          if (tokens[idx].tag === 'h1') htmlResult += `<weiz-title-meta />`
          return htmlResult
        }
      })
    }
  }
})

VitePress 切换暗黑模式丝滑动画

VitePress 切换暗黑模式丝滑动画 &ZeroWidthSpace;

移植之前 Hexo 博客中使用的暗黑切换动画,主要用到的原理就是 设置动态css变量原生视图过渡 - startViewTransition 以及 css的clip-path属性。先上效果图:

1. 设置全局css &ZeroWidthSpace;

新建 dark.css,并注入到主题配置 docs/.vitepress/theme/index.ts

.vitepress/theme/style/dark.css
css

切换动画速度,通过 animation 中的时间控制

2. 设置动态css变量 &ZeroWidthSpace;

新建 Dark.ts,并在 Layout 插槽中使用

ts
vue
<script setup lang="ts">
import { useData } from 'vitepress'
import { toggleDark } from './Dark'

const { isDark } = useData()
// 实现切换主题过渡动画
toggleDark(isDark)
</script>

最后将Layout 插槽组件,注入到 主题配置 docs/.vitepress/theme/index.ts

.vitepress/theme/index.ts
ts
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'
// 暗黑样式
import './style/dark.css'

export default {
  extends: DefaultTheme,
  // 使用注入插槽的包装组件覆盖 Layout
  Layout: Layout
}

VitePress 统计文章,新建归档页和标签页

VitePress 统计文章,新建归档页和标签页 &ZeroWidthSpace;

VitePress 虽然没有直接提供统计文章的方法和页面,但是有提供相关 API 帮助我们完成这项工作。详情可以查看 构建时数据加载

明确文章字段 &ZeroWidthSpace;

首先我们要明确我们需要哪些文章字段,这取决于我们的文章卡片要显示哪些内容。我们能想到的必不可少的,有 文章标题,创建时间,标签,url链接。以下是一个简单的示例:

.vitepress/theme/type/WPost.ts
ts

获取文章时间 &ZeroWidthSpace;

基于以上字段,标题 、url等可以在 VitePress 内部提供,摘要、标签等,我们在写博客时可以在 frontmatter 中直接写入,剩下只有一个时间字段,我们无法获取。这就需要我们在服务端根据文章创建时间或者git提交时间来获取。

我们创建一个 fileTime.ts 文件来获取文章时间,这里利用 node 的方法和 git 的命令

.vitepress/utils/fileTime.ts
ts

统计文章数据 &ZeroWidthSpace;

根据 VitePress构建时数据加载 - 基本用法 的要求,我们新建 post.data.ts 文件

ts
ts

这里需要注意的点:

  1. post/**/!(*-demo).md' 中的 !(-demo).md 是排除以 -demo结尾的文件,如果你没有的话,可以直接写 post/**/*.md

  2. Post 类型来自于我们之前的文章类型定义

  3. formatDate 是一个格式化时间的方法,见上文代码 .vitepress/utils/tools.ts

  4. 获取文章数据主要方法还是利用官方提供的 createContentLoader

  5. 最后需要对文章按照发布时间来排序,以便我们显示最新文章

使用文章数据 &ZeroWidthSpace;

直接在 .md 页面和 .vue 组件中使用从post.data.ts 导出的 data 数据

vue
<script setup>
import { data } from './post.data.ts'
</script>

<pre>{{ data }}</pre>

归档页和标签页数据处理 &ZeroWidthSpace;

归档文章我们一般按年份展示,标签则需要根据选中标签展示对于文章。这里就需要对文章数据进行处理。

新建一个 post.ts 文件,专门用来处理文章数据

.vitepress/utils/post.ts
ts

新建归档页 &ZeroWidthSpace;

新建页面可以查看另一篇文章 VitePress 新建页面和注册组件

这里以归档页面简单做一个示例:

新建一个 post.vue 组件,内容如下:

.vitepress/theme/components/WPost/index.vue
vue

其中的 weiz-post-list 文章列表组件,你可以自由发挥

然后将此组件注册到全局主题中

.vitepress/theme/index.ts
ts
import Post from './Post/index.vue'

export default {
  enhanceApp({ app }) {
    // 注册全局组件
    app.component('post', Post)
  }
}

最后我们新建一个 posts.md 页面,应用此组件即可

docs/pages/posts.md
md
---
layout: post
title: 归档页
description: 这是唯知笔记网站的文章归档界面……
---

标签页类似,只是多一个选择标签过滤数据的事件,这里不再赘述

另一种思路:生成 json 文件 &ZeroWidthSpace;

根据官方文档描述,还有一种思路就是在 项目构建时,服务端生成 json 文件,前端请求 json 文件获取数据

警告

这样会把你的文章接口 API 暴露在客户端,如果文章数据量很大,或者文章数据敏感,不建议使用此方式。

新建文件 loadPosts.ts,注意这里的文件目录是在 config 下

.vitepress/config/loadPosts.ts
ts
// 代码和 docs/.vitepress/utils/post.data.ts  类似,细节不太一样
import { createContentLoader } from 'vitepress'
import fs from 'fs'
import path from 'path'
import { sep, normalize } from 'path'
import { formatDate } from '../utils/tools'
import { getGitTimestamp } from '../utils/fileTime'
import { Post } from '../utils/post'

export const loadPosts = async (mode) => {
  // 使用 createContentLoader 加载 Markdown 文件
  const posts = await createContentLoader('post/**/!(*-demo).md', {
    // 包含原始 markdown 源
    includeSrc: false,
    // 包含摘录
    excerpt: false
  }).load()

  //
  const promises: Promise<any>[] = []
  const _post: Post[] = []
  posts.forEach(({ frontmatter, src, url }) => {
    const title = frontmatter.title,
      _tags = frontmatter?.tags,
      category = frontmatter?.category,
      abstract = frontmatter?.description,
      // 获取手动设置的更新时间
      createdDate = frontmatter?.firstCommit ? +new Date(frontmatter.firstCommit) : '',
      updatedDate = frontmatter?.lastUpdated ? +new Date(frontmatter.lastUpdated) : '',
      // 日期格式
      dateOption = formatDate(),
      // 链接去掉项目名
      link = normalize(url)
        .split(sep)
        .filter((item) => item)
        .join(sep)
    // 没有时间的文章根据git时间戳获取
    if (createdDate && updatedDate) {
      _post.push({
        title,
        url: link.replace(/post\//, ''),
        date: [createdDate, updatedDate],
        dateText: [dateOption.format(createdDate), dateOption.format(updatedDate)],
        abstract: abstract,
        category: category,
        tags: _tags
      })
    } else {
      // https://vitepress.dev/zh/guide/getting-started#file-structure
      // 如果你的文档在docs目录下,路径开头需要拼接 docs/ ,末尾需要拼接 .md
      const task = getGitTimestamp(`docs/${link.replace(/.html/, '')}.md`).then((date) => ({
        title,
        url: link.replace(/post\//, ''), // 由于使用了rewrites重定向,这里也对url作处理
        date: [date[0], date[1]],
        dateText: [dateOption.format(date[0]), dateOption.format(date[1])],
        abstract: abstract,
        category: category,
        tags: _tags
      }))
      promises.push(task)
    }
  })
  let formattedPosts = _post.concat(await Promise.all(promises))
  // 发布时间降序排列
  formattedPosts = formattedPosts.sort((a, b) => b.date[0] - a.date[0])

  // 定义输出路径
  const outputPath = path.resolve(mode === 'production' ? './dist/posts.json' : './docs/posts.json')

  // 确保目标目录存在,如果不存在则创建,否则首次构建会报错
  const outputDir = path.dirname(outputPath)
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true }) // 递归创建目录
  }

  // 将数据写入 JSON 文件
  fs.writeFileSync(outputPath, JSON.stringify(formattedPosts, null, 2))

  console.log('Generated posts.json successfully!')
}

然后修改 config 配置,在开发和生产构建阶段,分别调用此方法

docs/.vitepress/config/index.ts
ts
import { loadPosts } from './loadPosts'

export default async ({ mode }) => {
  // https://vitepress.dev/zh/reference/site-config
  return defineConfig({
    // 其他配置省略
    vite: {
      plugins: [
        // 开发环境执行
        {
          name: 'load-posts-plugin',
          async configResolved() {
            await loadPosts(mode)
          }
        }
      ]
    },
    // 构建时执行
    async buildEnd() {
      try {
        // 加载文章
        loadPosts(mode)
      } catch (error) {
        console.error('Error during buildEnd:', error)
      }
    }
  })
}

这样本地项目启动或者项目生产构建时,就会生成一个包含文章数据的json文件。

对应的,在客户端我们发送一个请求去获取json文件,从而拿到数据。定义以下方法

ts
export const postsData = async () => {
  const response = await fetch(window.location.origin + '/posts.json')
  const posts: Post[] = await response.json()
  return posts
}

在归档页或者标签页

vue
<script setup lang="ts">
// 省略其他
onMounted(async () => {
  posts.value = await postsData()
})
</script>

通过以上方式我们也能拿到文章数据,正常展示在页面上。

部署问题 &ZeroWidthSpace;

Git 时间显示异常 &ZeroWidthSpace;

由于我们的时间依赖于 git,所以脱离 github pages 部署的话,新建和更新文章时间可能显示是最近部署的时间。

这里只列举下 cloudflare worker 部署的解决方案,其他的可以自定搜索:

  1. package.json 中新增命令 build:cf
package.json
json
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev": "vitepress dev docs",
  "build": "vitepress build docs",
  "build:cf": "git fetch --unshallow && vitepress build docs",  
  "preview": "vitepress preview docs"
},
  1. 修改 cloudflare 上的构建命令为 pnpm i && pnpm run build:cf

VitePress 新建页面和注册组件

VitePress 新建页面和注册组件 &ZeroWidthSpace;

VitePress 默认只有一个首页,而且内容区域基本都是固定的,如果你想自定义首页,或者新增归档、标签等页面,那么注册组件和新建页面就是必不可少的步骤

注册组件 &ZeroWidthSpace;

如果只使用普通组件,不需要注册,直接导入使用即可。一般注册组件,都是我们要在很多地方使用(比如文章列表,文章卡片),或者直接当一个页面使用(比如首页,归档页),注册后就不用再导入了。

1. 创建组件。这里假设我们创建了一个名为 Home.vue 的组件,并且它位于 .vitepress/theme/ 目录下。

.vitepress/theme/Home.vue
vue
<template>
  <div>这是我的首页</div>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped></style>

2. 在主题配置中注册这个组件

.vitepress/theme/index.ts
ts
import Home from './MyComponent.vue'

export default {
  // 为自定义界面添加class,非必须
  Layout: () => {
    const props: Record<string, any> = {}
    const { frontmatter } = useData()
    props.class = frontmatter.value?.layout || ''

    return h(WLayout, props)
  },
  enhanceApp({ app }) {
    // 注册全局组件
    app.component('home', Home)
  }
}

组件注册后,我们就可以使用了。如果是在 vue 组件中,我们不需导入,直接使用就可 <home />,如果是在 md 页面中,我们可以直接修改 layout 字段值即可。

新建页面 &ZeroWidthSpace;

注册完 Home 组件后,我们就可以新建一个页面使用它了。这里我们修改首页 index.mdlayout 字段,使用我们注册的组件名称 home。(首页比较特殊,默认入口就是 docs/index.md,VitePress 已经提供了这个文件,不需要创建)

docs/index.md
md
---
layout: home
title: 唯知笔记
titleTemplate: 唯知笔记

site:
    title: 唯知笔记
    author: weizwz
    #  其他字段省略
---

同时 index.md 里的字段 site.title 等,我们可以在 Home.vue 组件里通过以下方式获取到

.vitepress/theme/Home.vue
vue
<script setup lang="ts">
import { useData } from 'vitepress'

const { frontmatter: fm } = useData()

console.log(fm.value.site)
</script>

除了首页的其他页面都需要我们新建对应的 md 文件,比如我们想要一个归档页面:

按照之前步骤,先创建一个名为 MyPost.vue 的组件,它位于 .vitepress/theme/ 目录下,然后在 .vitepress/theme/index.ts 中完成注册,注册名称是 my-post

最后我们新建一个 docs/pages/posts.md

docs/pages/posts.md
md
---
layout: my-post
title: 所有文章
description: 这是唯知笔记网站的文章归档界面……
---

这样归档页面就可以建好了,访问路径为 /pages/posts

VitePress 添加图片查看器 Fancybox

VitePress 添加图片查看器 Fancybox &ZeroWidthSpace;

Fancybox简介 &ZeroWidthSpace;

Fancybox 是一款非常流行且功能强大的 JavaScript 图片查看库。之前博客采用Hexo框架的时候,使用的 butterfly主题 就默认内置的 Fancybox。其在保持轻量化的同时,还支持丰富的功能,比如:缩放、旋转、全屏查看、手势操作等,并且可以展示视频、iframe 内容以及动态加载的内容;支持响应式布局,兼容所有主流浏览器。所以使用Vitepress切换博客框架后,第一时间就开始着手配置Fancybox。

封装组件 &ZeroWidthSpace;

官方有给出 Vue示例,但是并不完全适配 Vitepress,这里对照着封装下。

主要原理

根据官方示例,思路就是在 vue 组件被挂载时在 mounted 里完成绑定,在组件更新时在 update 里重新绑定,在组件被卸载时在 unmounted 里销毁。

那么在 VitePress 里,我们将在其全局主题配置 docs/.vitepress/theme/index.ts 中完成此设置,但是在 VitePress 中,切换页面后并不会触发 update,而是触发路由方法 onAfterRouteChange,所以我们用 onAfterRouteChange 代替 update,同时在切换路由之前 onBeforeRouteChange 中,销毁 Fancybox。

1. 封装组件 ImgViewer.ts &ZeroWidthSpace;

先封装一个组件 ImgViewer.ts,以便我们统一处理文章中的图片:

  1. 鉴于 Fancybox 的要求,不同图片的 data-fancybox 属性值将归于不同图库,所以我们对图片统一设置此属性
  2. 由于个人在文章图片中没有单独设置 alt 属性,所以统一设置此属性为离图片最近的标题文本

代码如下:

.vitepress/theme/components/ImgViewer.ts
ts

2. 在全局设置中启用组件 &ZeroWidthSpace;

我们在全局设置中根据不同生命周期和运行时API,启用不同的组件方法。代码如下:

.vitepress/theme/index.ts
ts
import type { Theme } from 'vitepress'
import { EnhanceAppContext, inBrowser } from 'vitepress'
import { h, onMounted, onUnmounted } from 'vue'
import DefaultTheme from 'vitepress/theme'
import { bindFancybox, destroyFancybox } from './components/ImgViewer' // 图片查看器

export default {
  extends: DefaultTheme,
  ...

  enhanceApp({ app, router }: EnhanceAppContext) {
    ...
 	   if (inBrowser) {
      router.onBeforeRouteChange = () => {
        destroyFancybox() // 销毁图片查看器
      }
      router.onAfterRouteChange = () => {
        bindFancybox() // 绑定图片查看器
      }
    }
  },
  setup() {
    onMounted(() => {
      bindFancybox()
    })
    onUnmounted(() => {
      destroyFancybox()
    })
  }
} satisfies Theme

3. 修改默认样式 &ZeroWidthSpace;

由于对Fancybox的纯黑背景不太感冒,修改为半透明高斯模糊遮罩层。
以下代码中使用的部分变量,参见 我的 Tailwind 配置

.vitepress/theme/style/custom.css
css

此 css 也是最终引入 docs/.vitepress/theme/index.ts 即可

最终效果 &ZeroWidthSpace;

封装并配置好后,我们查看界面效果,如图:

请回答 2025

2025年12月31日 17:14
2026

按照惯例,每年这个时候我都会写一篇跨年博客来大谈特谈,今年是第十四年,是不再跨年的第一年。

其实从去年开始我就已经不再列目标了,随着年纪的增长越发感觉无意义,于是把过去没有完成的目标都拿出来,时过境迁,如今再去审视当年的目标,有些可能极其可笑了,不同的年龄不同的心境,追求的东西也有所不同,小时候可能是一件新衣服,中年可能是每天早上晨勃能准时到来,老了可能是平静的过一天就很好了。

  • 体重65kg 2024
  • 不添置新的数码产品 2024
  • 家庭出游两次以上 2024
  • 精简生活 2021
  • 做菜 2020
  • 跑步十公里 2019
  • 学习语言 2018 2020
  • 去一次马尔代夫 – 出去走走 2015
  • 尝试录播客
  • 纯远程工作
  • 开源项目再挣扎下

目标完成的七七八八,奈何大部分都是被动的,并没有以前那种刷目标的成就感了。

接下来继承新的传统,每月选出一张代表。

一月,柬埔寨。
二月,再逛曼谷。
三月,开罗。
四月,尔滨。
五月,比利时。
六月,纽约。
七月,京都。
八月,没地方去了就去曼谷。
九月,首尔。
十月,神户。
冬月,巴黎。
腊月,哥本哈根。

2025 我几乎把所有的钱都花在了机票上,飞行次数之多、距离之长都是以前不曾想过的,但其实并不是一件让我开心的事情。我以前经常说自己运气还不错,然而人确实有运气用光的那一刻,最后一个月情绪持续低落。虽然很早就决定今年的跨年博客不再展望,但没想到的是此刻真的没什么可展望的了,真的是完美诠释了一切都是被动完成的。

作为一个P人,其实对人生一直没有什么规划,但我确实把今年当作了一个人生的分水岭,今天一过就是我的下半生,当然前后可能并没有什么不同,但希望自己能有一个新的心境。

最后祝所有关注以及不关注我的朋友们新年新气象。

以及

2025 年个人博客回顾

2025年12月24日 21:48

发文

今年共发表文章 63 篇,与去年基本持平,略增 1 篇。内容上继续围绕“闲余”“雅致”的核心主旨,以游山玩水、山野探索为主,专题包括“我在苏州逛园子”、“山西河北历史文化之旅”、“博物图鉴”、“日常漫步”,也浅谈了几篇关于极简、效率与断舍离的文章,技术类内容已基本淡出。年度点击量最高的文章是年初第一篇发布的《Calibre 推荐配置与插件》,共获得 18,533 次点击。全年收到评论 2,103 条,较去年增加 300 条,S兄成为本年度“首席评论员”。评论数最多的文章是《以阅读体验为核心的网页宽度设计策略》。

共发布 63 篇文章,浏览最多的 10 篇文章如下:

Calibre 推荐配置与插件 (18,533)
隐居假期 (8,731)
WordPress 极简主题 Dear v1.2.0 (8,644)
以阅读体验为核心的网页宽度设计策略 (7,486)
上海外滩 (7,413)
山西河北9天历史文化之旅保姆级攻略 (7,332)
我在苏州逛园子之留园 (7,183)
我在苏州逛园子之虎丘 (6,944)
途中风景 (6,573)
观“古蜀瑰宝—三星堆与金沙”文物特展 (6,537)

共收到 2103 条评论,评论最多的 10 篇文章如下:

以阅读体验为核心的网页宽度设计策略 (63)
隐居假期 (58)
桦加沙过后的凤凰山 (58)
日常漫步 Vol.19 之稻田与猫 (49)
雅余也有公众号了 (45)
上海外滩 (45)
山西河北9天历史文化之旅保姆级攻略 (45)
途中风景 (43)
macOS Tahoe 26 丑哭了 (42)
WordPress 极简主题 Dear v1.2.0 (42)

 

倒腾

今年对博客主题进行了 5 次调整,并完成 1 次服务器重新部署。回顾“更新日志”,仍以“移除”、“简化”和“禁用”为主线。通过持续简化,在大量更新图片的情况下,全站占用空间反而减少了 50%,其中减少生成冗余尺寸的缩略图收效显著。最新的 Lighthouse 评分中,除因文字颜色较浅导致“无障碍”项得分 95 分外,其余各项均为满分。

生命在于折腾,生命不息,折腾不止。是时候再次回答一遍“独立博客自省问卷15题”了。(今年意外还收到5份答卷

 

主题

今年未发布新主题,也未对现有主题进行迭代。一方面灵感有限,另一方面网络上优秀主题已足够丰富。极简的终点究竟在何处?或许明年再尝试回答,但始终坚信:内容为王

 

感谢

今年评论数增长 300 条,在此特别感谢以下“TOP 10 评论员”S (100)ACEVS (67)网友小宋 (62)夜未央 (53)heilzz (50)粽叶加米 (30)1900 (29)满心 (25)寻鹤 (21)全局变量 (21)。在此不一一列举,感谢所有“友圈”中的朋友们,明年继续互动起来。

2026 年,继续努力,回馈自己,成为更好的自己。

-

附:2024 年个人博客回顾

另外,请记住!75年前的今天,12月24日是长津湖战役胜利的日子。中国人民志愿军在冻饿交加、装备悬殊的极端条件下殊死搏斗拼来了这场胜利。

我的2025

2025年12月25日 00:28

2025年经历了很多事情,所以时间过得很快,夏天的事情仿佛还在昨日,如今已是数九隆冬天了。

二十一世纪二〇年代已经过完六个年头了,二〇年代第一年提到的四川县市游记计划也许久没有更新,倒不是没有外出探索,而是没有总结出来发表,当然更没有为了打卡而打卡,目前没有那么多闲暇的时间。

身体还算好,心情也算行。来年还有更多的事情要做,也有藏在心底的计划需要去实施。

如何在工作和生活之间维持平衡,是一门大学问,目前来说好像只能在其中做选择,而无法兼顾。看了一些书,见了一些人,面子这种东西还是有市场的,没有看透本质的人,还是会执着追求这样东西。

信心,或许是如今最宝贵的东西。所有的斗志、热情、抱负,都基于信心。守护住自己的信心,暮色苍茫看劲松,乱云飞渡仍从容。

前不久入手了两种新植物,一种是叫呼吸香水的月季,一种是能爬藤的使君子。希望来年它们能茁壮成长,绽放自己。

今年博客建立也满15周年了,有几次我去看了下自己的留言板,建站之初留过言的好多博客已经消失不在,或许大家都很忙无暇照顾,亦或本就无心经营。还是那句话,贵在坚持。

月底给本站套上了CDN,一来是想做下尝试,看看CDN的防护能力和VPS的防护能力相比是不是要强大一些,二来是顺手体验下HTTPS的感觉,目前是HTTP和HTTPS并存的,没有做强制跳转。如果好用就继续用,如果不好用就取消掉。就目前来看,CDN可以通过更灵活的规则屏蔽一些爬虫和扫描器,但是访问速度可能会慢那么一点点,我自己测试了一段时间,好像影响不大。如果各位博友看到这里,有什么建议或者意见可以告诉我,如果你们自建的RSS订阅服务被屏蔽了,或者访问速度不理想,再或者发现了其他bug,请直接敲击你的键盘,在评论区留言。

2025年即将过去,我很怀念它。


除非注明,三棵树阁文章均为原创,转载请以链接形式标明本文地址
本文链接:https://www.sksren.com/archives/2185.html

写在2026年新年之前

2025年12月23日 15:53

现在是凌晨的 4 点 23 分,当我写下这个题目时,就意味着又是一年过去了,也是这个博客的第五篇《写在新年之前》,也意味着这个博客竟然已经坚持到第五年了。

我算是一个很长情的人,但我也会在某些特定的时刻,亲手毁掉已经构建好的东西,这便是所谓的「死本能」。因为毁掉是最能体现「权力」的存在,就像是帝王一个命令、甚至只是一个眼神,就可以决定一个臣民的生死一般,毁灭是将权力极致化的体现。

拥有令人艳羡的爱情,绝不是最完美的事情,因为它随时会化为泡影,但如果这个在外人看似完美的爱情,是经由自己而毁灭的,爱情在那一刻得到了升华和符号式的刻骨铭心——你看,是因为我被辜负,所以我拥有过最完美的爱情,也成为了最值得同情的受害者。

今年的「总结」就从「死本能」切入吧。


白日出没的月球

@桐庐

今年养了第二只狗,取名咪盔,其实就是「胸罩」的别称。他出现的时机,是因为第一只取名奶子的狗,社会化做得太好,一直很需要玩伴和社交,所以我们才决定养一只能够陪伴他的弟弟。于是,这就成了机缘巧合的始末,我们会半开玩笑地说,如果第一只狗养的是咪盔,他的性格与奶子完全相反,一个不太需要社交的狗,也会打消我们再养狗的想法。

出场的顺序,就变成了最直观的游戏规则,而这种顺序就是所谓的「滤镜」。

我本不想聊这件事,但这个主题几乎贯穿了我的整个 2025 年——关系的死亡。我今年结束了好几段关系,最主要的、也是最戏剧性的,大概是和助理的决裂。也是因为出场顺序,让她在我这里一直存在着某种信任的「滤镜」,她很好地补全了我在学生时代最渴望的那种玩伴符号,我必须承认,她是极具生命力的代表,情绪化、所谓的侠义、说走就走的配合,而潜在的「死本能」,是一场我们想要挑战的「自由意志」的实验——我们是否真的有能力改变一个被原生家庭驯化的成年人,以及是否真的能通过认知的改变突破宿命论的束缚。

实验结果是失败了,因为她又回到了她的世界,甚至是一个从头到尾都没有真实过的世界,只是我们因为她出现在我们生命中的顺序,而相信了她前后逻辑相矛盾的部分,自动美化了她最情绪化的部分。

关于这段关系决裂的细节,我并不想占据《写在新年之前》太多篇幅,所以在正文开始的前言部分大致聊一聊,看似重要,也仅仅只是因为出场顺序的关系,被排在了一笔带过的部分。

没错,你说的全都没错。
别管哪个谁怎么说,
你就活在自己的井中,
别看那个风怎快活。

——《白日出没的月球》苏打绿

日光

美好是因为克服美好的恐惧,
美好是因为无视美好的逝去。

——《春·日光》苏打绿

5 月份按照惯例,又去了一趟日本。日本不是一个充满变化的城市,从机场到大阪市区的高速路该破破烂烂的,还是以前那个样子。手机里在日本拍摄的照片也越来越少,包括去鸟羽水族馆看海獭,也就拍了 8 张照片。

@鸟羽

曾经我和老婆几乎看过了日本所有的海獭,也为了这些海獭去了不同城市。最后剩下的海獭,也只有鸟羽水族馆的最后两只。这是一个直观的、关于死亡的具体感知。甚至有一年,在我们看过其中一只海獭的第三天,我在社交网络刷到了它去世的消息,即庆幸看到了他的最后一眼、也遗憾看到的竟然是最后一眼。

今年再看到这两只海獭时,我脑子里出现了一个「恐怖」的想法,或许就是因为它们的有限生命,让它们才能在最后的日子发光发热。它们成为鸟羽水族馆里唯一需要排队和规定观看时长的区域,越来越多人看过它们,也越来越多的人会在它们离开的那一刻,和我有同样的庆幸与遗憾。

人的大脑是可以被「驯化」的,它能够很快的适应「熟悉」,在一个长期居住的房子里,你几乎可以闭着眼睛在房间里拿到你想拿到的东西。但就是因为这种熟悉,让人们也失去了好奇心,但也是因为这种熟悉感,人们才会为家里的某一处出现玻璃碎裂的声音,而被调动所有的感官、甚至是刺激与兴奋——这便是「死本能」的底层——大脑在已经熟悉的状态里,会本能地看到那些被破坏、冲突的部分,甚至为了这样的刺激而去主动制造破坏与冲突。

我以前常给人提供一个看似很没有意义的解决方案:如果你每天都是同样的两点一线生活,那就找个机会改变一下两点一下之间的路径,去发现被自己无视的乐趣。但能真的去实践的并不多,因为改变熟悉本就意味着要对抗沉没成本,甚至会因为预判了它改变不了什么,而选择继续留在熟悉之间,把自己活成机器,又抱怨自己被驯化成了机器。

我是一个会主动「破坏」熟悉感的人,是我明确知道我需要释放「死本能」的一部分,破坏是充满罪恶感的,更何况是要毁掉自己已经熟悉的一切,但无视破坏欲,并不意味着「死本能」就会消失。正是因为熟悉感在一点点吞噬一个人的存在感时,才需要「死本能」作为平衡,为他们在熟悉的空间里,「不小心地」摔坏那个完好无损的玻璃杯。

美好或许是因为期待美好的逝去。


狂热

却忘了所有新都来自旧,
只在乎今天有多少回扣。

——《夏·狂热》苏打绿
@重庆

朋友小袁来和我们生活了一个夏天,每天下午来家里做饭,然后晚上玩游戏或是看电影、录播客节目。我们很难从热闹中获取能量,大部分的时间都是通过独处获得感悟。比如此时此刻我在酒店客厅码着字,再过一会儿小袁就会带着行李先行离开,然后我们再踏上返程的路。

我很难形容这种关系,因为很多人会认为它充满了「冰冷」。比如小袁和我们度过了将近两个月的生活后,离开我们的那天,我们仅仅就是在房门内外彼此告别,没有送别的不舍、没有践行的仪式感。但我们又不是真的没有情感,他也会在微信群和我们抱怨,很想念某家一起吃过的苍蝇馆子,我们极少会记录生活,比如认真地为一餐拍照和留念。

前几天,小袁开车带我们在宁波逛吃时,聊起了这种「冰冷」的感情——并不是我们无法共情情感,而是我们一直在追求那种纯度更浓的「情感」。其实我们三个人凑在一起时,也并没有仪式感,三个人各自玩着手机,偶尔聊上几句,可以很严肃地聊哲学命题,也可以回味非常低俗的荤段子,没有掺杂功利、目的的社交,反而对我们而言是纯粹的。

在刚开始学写剧本的时候,总是在寻找「还没有被写过的故事」,所以有一段时间,我也甚至在无意识地拒绝看电影,因为会有强烈的挫败感——为什么他们能想到这样的故事,而我还没有找到那个「最特别」的故事。但真的写成了那些我以为还没人写过的故事时,原本应该支撑它内核的情感模块零散一地,之所以人们不会为杜撰的故事动容,不是因为他们没有经历过,而是他们无法共鸣——如果这些故事真的发生在了自己身上时,是否会和主角做出同样的选择。

极致的情感不是复杂,而是极具浓缩的哲学命题,就像是梁山伯与祝英台、罗密欧与朱丽叶,他们所讲述的是不一样的故事,却有着同样的浓缩内核——我愿意为爱而死,但我却无法让逝去的人因爱而复活。

就比如,我们听过很多关于狗与饲主之间令人动容的故事,但我们能更快识别出里面的「纯度」,人的记忆是会撒谎的,情感也是很容易被重新加工的。而那些真实的情感,往往不需要铺陈、转折,甚至仅仅只是一句叹息足以。小袁的妈妈给我们讲起她接手自己爸爸的老狗,原本她爸爸希望她女儿能带着这只肿瘤缠身的老狗去做安乐死。但这只狗坚强地活了下去,于是小袁的妈妈决定瞒着自己的爸爸,把狗带回家,又养了九个月。最后要走的那天,她在狗耳朵边感慨着,希望它下辈子能做个人,如果还记得自己就到自己的梦里。三个月之后,她真的梦见了一个女婴,在她的怀里嬉笑着。她被吓醒后,才恍然大悟,或许自己的无心之言就这样成真了。小袁的妈妈用她的方式讲述着这个故事,而我们在那一刻心都被揪了一下。

而在这段纯粹的情感里,还有一个难以被忽视的——她的爸爸将自己的狗交给女儿安乐死后,扭头就走,一句道别都没有——不过我们知道,那一刻他在内心做出切断的时候,已经上演了无数场关于奇迹和重逢的桥段,但他必须做出给老狗安乐死的最终决定。

这个世界上随时都在发生全新的故事,而它们都同样有着「旧文明」的内核——人性。


故事

人生一场大梦,
夜落不觉晓。

——《秋·故事》苏打绿
@重庆

夏天的最后几天,我们送走了家里最老的猫,也是因为我们做出了安乐死的决定。虽然这件事我们讨论过很多次,也认为我们可以足够理智地应对宠物的离开,但真的看到安乐的针剂被推进养了 14 年的毛孩子身体里时,他被挤出了堆满身体的痛苦,被挤出的痛苦占满整个房间,压缩着我们挤出了原本以为不会流下的眼泪。

人的大脑会因为痛苦启动不自觉的保护模式,就像现在有人问起关于屁屁的事,我和老婆竟然会在第一时间想不起屁屁离开时是多少岁,我也是因为看了博客记录的那一天,才想起他原来是 14 岁的老头子了。

陈丹青的那句「死亡是极其无聊的」,并不是一句空穴来风的废话。因为死亡就是无聊的,而为了对抗这种物理性的、直观的死亡,人们才需要用感性的部分填满所有生命逝去的空洞。死亡被人们用极尽可能的方式记录至今,从壁画上那些关于怪兽、神明的描述,到文学作品里关于死亡的类比与符号,它之所以还是文明里不可或缺的部分,是因为生命本身都是朝着它而奔进,当死亡消失时,生命也变得毫无意义。

我将对于屁屁的感情、做出安乐的决定以及直面他死亡的这些部分,都记录在了想要写出的故事里,这就是我在追寻的「还没有被写过的故事」,不是因为形式上的重复,而是因为它的情感浓度够纯,才能提炼出让每个人都能与之共鸣的情感。这不仅仅是感动,而是将人类对于死亡的情感——因为它从古至今仍然还未能被翻译成一段确切的标准。

我在写下这段文字时,是在 Notion 的编辑器里,右下角的 Notion AI 快捷按钮有一次被我改成了头顶着小猫的形象,它只是很无聊的细节,但人类的情感就可以将它翻译成——像是屁屁正趴在某个角落,在我抬眼的瞬间,它没有撤回目光,而是静静地看着我。

于是,我又用感性的部分,翻译了所谓的「我很想你」。

死亡是无形的,你可以在记忆里和情感里,将它捏成任何你想要的形状,但却再也触碰不着。


未了

虽然反复,却渐渐懂得,
每一步都是自己的。
不爱永恒,但求现在,
真实活着的人生。

——《冬·未了》苏打绿
@东京

今年的主题排序,是苏打绿春夏秋冬的专辑。我并不是这个乐团合格意义上的粉丝,因为我很想看看吴青峰的脑袋里到底还装着怎样的东西。

去年陪老婆看了好几场苏打绿的巡回演出,今年在日本东京看了海外巡演的最后一场。因为我老婆很喜欢苏打绿,我去年开始有一段时间有些抵触听苏打绿的歌,我一直误以为这是一种奇妙的「雄竞」,老婆会为了他们拖着我去各个城市看演唱会,她很长一段时间听的歌都是他们的,所以我会觉得这种「对别人的分心」可能是一种我不爽的结果。

但是,这里面还裹挟了一个更奇妙的东西。

吴青峰很喜欢童话、希腊神话、中式哲学,所以他把这些想法都融入了自己的作品。就比如《未了》里描述的西西弗斯,也曾经是我很爱在写作中出现的角色,但为什么他的作品可以被传唱?

对,是嫉妒。

这个被裹挟在看似合理的情绪里的恶魔,竟然是这么最简单不过的存在,我差点骗过自己——我当然嫉妒吴青峰的才华,他既是推着石头上山的西西弗斯,也是站在奥林匹斯看着这一切的宙斯,还是将他们的故事谱成曲目的赫尔墨斯。越是这样,我就越觉得自己或许从一开始就只能推着石头一次次回到原点的渺小。这种嫉妒带来的是越捆越紧的窒息感,我必须承认他的每一句歌词对我而言都是充满画面感的艺术品,但他同样又是那个将自己的作品撕碎重构的人,他颠覆自己、否定自己、重新编译自己年轻时对不同命题在中年时的看法,他将「死本能」在自己的作品里发挥到极致,以至于外人等不到他被毁灭的那一刻,他已经自我毁灭重生了。

操,这是何等的造诣啊!

在这篇文章之前,我从来没有找到坚持写作的意义。我只是觉得我很爱写,也爱积累的过程,我已经完成了所谓的 10000 小时理论,那我到底在坚持什么?西西弗斯之所以接受惩罚,不仅仅是因为他的狡猾,也是因为他的命运使然。如果有一天他真的成功地将巨石推到山顶,他要做的一定是再亲手将它推下,因为这就是他存在的意义。

我只有不停写,不停地积累,巨石才不会滚落,但滚落又是必然的命运,否则它将不再构成西西弗斯,也不再构成我。我怕死,所以我通过不停写来证明自己还活着;我又不怕死,因为我知道我已经留下了很多足够证明自己曾活过的东西。

当命运的巨石必然滚回原点,赋予意义不再是活着的意义,而是活着的证明。每个人都不一样,只是我恰好选择了写作。而探索所谓意义的过程不是将巨石推向山顶,而是推石、跌落、重新开始的往复,直到力竭、直到咽气、直到在死前的最后一眼看到巨石留下的坑洼,才理解了这一生并未白活的含义。

毕竟,意义无法拯救任何人。


@宁波

最后老规矩,新年快乐!

蹭了一波美国大选的热度

2024年11月6日 17:54

沉寂了许久的老达博客的访问量,今天终于又爆发了一回。受美国大选影响,老达博客访问量今日有望突破1000IP!重回人生巅峰,哈哈

美国大选给老达博客带来一波流量

看看老达博客今天的百度统计,访客来源几乎全部都是美国大选有关的搜索关键词,可惜现在百度已经把老达博客抛弃了,来源大部分都是360搜索,如果有百度加持的话,这一波流量上3000ip也很正常。

美国大选相关关键词带来一波流量
美国大选相关关键词带来一波流量

给老达博客带来流量的是老达在2022年6月写的一篇文章:《2024年美国总统大选时间-美国总统选举日》,当时的更新风格就是广大网友们关心什么,咱就更新什么问题,一切为网友服务!知道美国大选是个周期性的热门,所以更新了几篇有关美国大选的文章,包括美国大选时间,美国大选程序等等,提前种下的瓜结果了,哈哈。

看来,以后要继续更新2028年美国大选时间、2028年美国大选结果发布之类的文章了。

就在老达更新这边文章的时候,美国那边的总统选举结果基本已经出来了,特朗普同志已经获得超过270个选票,顺利当选下一届美国总统!这里顺便恭喜一下川建国同志!之后的四年,可能又是充满戏剧性的四年,看着美国大选的闹剧,越来越感觉到,马斯克说的特别对,整个世界就是一个草台班子。

所以,我们自己都不要妄自菲薄,遇事不用紧张,大家都一样,都是草台班子,干就完了!

❌