Reading view

There are new articles available, click to refresh the page.

2025年折腾总结——软件试毒版

本着“生命不息,折腾不止”来抵抗无聊的理念,今年继续折磨硬盘试验软件。因为Mac好用常驻床头所以我对软件的要求具有同步的硬性要求,说起来其实也挺折磨人的……很多东西到后头逼得没办法只能自己搭服务器,折腾到最后方才醒悟,自己其实对很多东西要求没那么高,主要就是存储好找而已……但走过的弯路终究还是有效的,所以也没毛病~

一、NAS

(一)铁威马(不推荐)

其实早在2022年就买了铁威马,来回也折腾了一个月搞反向代理什么的,但不得不说它软硬件都很难用,双盘重启掉盘概率接近100%,而且docker已经沦落到得手动安装。所以直到10月把旧笔记本刷飞牛前对它的折腾最终停留在webdav备份和emby服务上。然而emby也慢,刮了无数没用的东西,最后成为了音乐播放器。选择这么个方式的原因也是多端同步需求,毕竟同时满足Mac、iPad、安卓和win多端同步也太难了……

(二)飞牛OS(非常推荐)【免费】

官网:https://www.fnnas.com/

飞牛我从去年听到了今年,最终因为实在受不了铁威马的不作为试了下,发现非常好用。毕竟我和程序员之间差了一万个代码,作为小白用起来就图别太折腾。为了实现实时转码特意把几乎没法离电使用的天选2拿了出来,结果系统自带的驱动并不支持1650Ti,多少也有点无语。

因为搞这个主要为了多端同步,所以在搭建以后一直试着搞事,同时也把之前放在铁威马上的webdav同步改到了飞牛上,倒是也很好用。因为它配备了自带不少应用1panel更是省了我不少事,所以目前主要用它做笔记同步、视频播放和电子书库。

另外值得一提的是飞牛OS虽然自带很多应用,但似乎都不支持改端口号(反正我没找到修改的地方),所以我用的是1panel修改端口号以后使用。

(1)影视库

刮削确实很快,而且能刮削演唱会。唯一要解决的问题可能是没法手动设置其它类型,一些短视频、MV只能以文件夹名突兀的出现在这里头,看上去非常不美观。其它倒是没问题,除了转码最终还得靠CPU的核显……只能庆幸机器要求不高吧。

(2)音乐库

用的还是铁威马 4.8 版本的emby,因为飞牛4.9版本缺了一些我喜欢的要素且没研究出来怎么导入之前建好的歌单,也就不想折腾了。不过值得一提的是网页版无法播放dsf格式,flac都是转成低码率的,同时网页免费版不支持歌词,真想要听还得配第三方软件。

至于歌曲的封面、歌词都是用软件写进了歌曲标签,大概有点像视频的mkv吧……

(3)电子书库

虽然飞牛自带komga但我选择的还是1panel的。在用它之前想用传统的calibre或者koodo,甚至还用过halo(支持点击直接下载),但试了以后不能达到我搜索的要求,所以一度想要放弃。后来选它本是因为一堆漫画想整理下,结果发现不支持mobi格式,倒是意外很适合建库就用上了。

它同时提供线上阅读和下载功能,对于漫画可以直接线上阅读多端同步(反正网页可以看),但对于比较复杂需要花时间的书我更多选择用app阅读。

二、多端同步软件

(一)笔记软件——思源笔记【webdav买断,云端功能按年,本地免费】

官网:https://b3log.org/siyuan/

印象笔记、有道云、语雀、为知、OneNote、飞书这些都用过,思源笔记更是老实续费两年后因为webdav老连不上放弃,结果到最后还是用回了思源笔记,因为它webdav终于好用了~甚至我这篇就是在思源里写的。

我对笔记软件的要求并没有太多,最重要得满足多端同步且离线可用。OneNote同步难,飞书不知道为什么总是数据特别大且除非设置离线否则文档都打不开,有道云没必要的东西太多而且我实在不喜欢必须云端,其它软件各有各的问题,相比之下思源居然最适合——虽然它表格也是烂的出奇而且自带的思维导图不好用,但没关系~我可以做excel或者freeplane文件拖进去同步,在其它端打开也可以修改。

而且思源笔记支持反向链接,对于看书就很舒服了。摘录的笔记可以链到读后感去(反之亦然),如果做一些专题类的东西也可以一个笔记搞定,因为支持超链接对我是很舒服的。

自带目录功能,有多种插件和主题可以选择。

支持网页剪藏,对于网页收集非常友好(顺带一提我以前用的简悦,结果不知道为啥越来越难用了……)

支持多种格式导出,尤其word,真是阿门!

(二)跨平台nas音乐播放器——音流(只推荐移动端)【可免费】

我其实没能搞懂它逻辑,而且经常出现在电脑(包括PC和MAC)播放,进度条在走但没声音的情况。但目前确实没找到更适合地支持emby的播放器,但移动端(安卓、iOS)都没出现过问题,只能说矮子里拔高子。

官网:https://music.aqzscn.cn/

(三)截图和简单OCR——pixpin【可免费】

官网:https://pixpin.cn/

如它官网宣传的。对于部分区域OCR以及大量截图用户简直神器,尤其赶PPT的时候……(社畜悲鸣)

(四)浏览器插件【免费】

Safari确实最适配Mac但它支持的插件也真的少得可怜,edge虽然越来越臃肿难用广告多烧内存但没办法它支持的多同步好啊。

(1)Tampermonkey

官网:https://greasyfork.org/zh-CN

篡改候?反正原来叫油猴,这相当于一个规则或者容器,可以按照需求装很多东西。

(2)其它

内容如名字下方说明,个人觉得这是最重要的几个了。

(五)思维导图——Freeplane【免费】

其实我是在用幕布途中发现它的。写文最大的问题就是没法说完全按照写论文那样钉钉铆铆,所以幕布在构思上给我的最大帮助仅仅只是安卓可用,临时想法可以插进去,但真的胡思乱想的头脑风暴还得靠其它的。而幕布导出mm格式正好它可以用还免费。

自带多种模板,支持自由主题,支持链接,对我挺好的。

三、Windows

(一)Potplayer【免费】

虽然深陷抄袭风波但不得不说它支持的格式多、广,甚至还包括音频的dsf,同时支持ftp和webdav格式,在外面可以连接nas是很爽的事。

官网:https://potplayer.daum.net/

(二)原音HQ播放器【免费】

比Foobar2000累死人的设置简单好用,输出选择好384000完事了。微软商店免费下载。只要自带封面歌词就能用,支持dsf格式,不过只能播放本地音乐。

(三)umi ocr【免费】

比起之前,现在还支持PDF格式的批量OCR,正确率不错,最大的问题可能在于……我没找出批量OCR除了全选复制以外的导出方式

(四)Mem Reduct【免费】

win11开机就8g不是我这个小电脑撑得住的,这个点击以后可以释放大量内存,因为我换回了win10所以一时半会没法截图了,但win10也可以用。

(五)renamer

批量重命名工具,支持全部重命名、加入序号、删除多余文件名部分等等功能(对于一堆资料的人真是谁用谁知道)

(六)BookXNote【同步功能买断收费,本地免费】

官网:http://www.bookxnote.com/

Windows端的阅读软件,支持PDF和epub,笔记可以导出斯维导图格式。它对扫描版的PDF支持度尤其好,以往做笔记还得费劲巴拉的OCR,到这里可以直接图片方式摘录导出。

笔记导出支持多种格式,我一般都丢进笔记软件了。

四、Mac

(一)视频播放——IINA【免费】

苹果自带的商店里有。用它就是图能切换音频,毕竟演唱会和4K碟很多都封装了多轨,在家肯定得图爽了~而且可以看音频码率来确定要不要midi切换下。

(二)Colibri【收费】

对于有大量sacd拥有者来说是极品软件,会自动切换到对应的码率不用自己一个个手动去换,音源一装耳机一戴就完事了,不费劲。不过不支持中文,设置得自己慢慢折腾。

(三)marginnote4【收费】

图它和iPad可以用云同步,效果和bookxnote一致,对扫描PDF效果尤其好,适合看比较专业费脑子的书。

没带Mac就懒得截图了。

结尾

2025没怎么折腾ai修复主要是真的换了4K显示器以后会觉得修复的那都是啥,我现在最后悔的就是把TLW的噪点去了,那可是胶片不可缺损的质感,好在尚能补救(重下一版)。不知道明年还有什么新的东西可以让我折腾,不过最近这硬件涨得……算了我还是啥也别干了(大概……)

沈醉回忆录:军统内幕

摘要:著名军统特务沈醉在被抓捕后的交代材料,此为其中一本,讲述军统内部行动的方法和培训

军统培训特务的内幕——从军统临澧特训班看特务的滋生

行动术”这门课程,是从来不印讲义,也不准学生记笔记,只由教官讲述。学生如有听不懂或弄不清的地方,课后可以提出来,请求补讲


抗战前军统特务在上海的罪恶活动

组员间向无往来,因此时常弄出自己人做自己人工作的笑话来。这些组员平日在人前总是伪装出一副不满现状和反对蒋介石黑暗统治的“进步”姿态,彼此在一个地方活动时,表现出的假积极也很突出,最易引人注目。在彼此不了解对方身份,而且一个人有几个不同化名,因此自己做了自己人的工作,区部也没有发觉,往往花了不少时间和金钱,一直到准备破案逮捕对方时,把偷来和偷拍的照片送到区部做最后决定时才发觉原来是自己人。


…可以,搞绩效呢

如果生活费给多了,有些人便会贪图享受不努力工作,所以只给一点可以维持生活的钱,这样一来,便非好好努力设法得奖金来弥补不足,工作越好,奖金越多,最容易表现出成绩;不好便不给,可免去固定的庞大开支。


进行尸体肢解,再装在箱子里,这种箱子,有时用汽车装出抛到荒僻无人的地方,有时则提到马路上雇上一辆人力车拉到火车站或什么旅馆。由人力车拉走这种箱子时,送的人先跟在后面走上一段便溜走。等到拉到指定地点,拉车的见没有物主跟来,有的便悄悄拉回家去,以为发了洋财,等到打开一看,原来是一具尸体,结果往往弄得吃官司脱不了手


由一个特务从对象背后猛击头部一下,便立刻飞逃。当这个对象被击昏倒地后,另一特务便装作这人的亲友,一面扶住一面大叫抓凶手,一面假充好人叫人力车(或在附近租汽车),伪装送往医院急救,


抗战时期军统特务在重庆的罪行

戴笠放现在肯定是最大营销公司和狗仔队头目

你有病想去找找医生,一定有人说石灰市的南京名医张简斋是中医


军统为了搜集搭乘飞机的旅客照片,特地在市区珊瑚坝飞机场马路边开设一家飞虹照相馆


往往以不赚钱来和其他照相馆抢生意。这种廉价照片拍了以后,在特务机关的档案中便会增多一些资料


邮检所拆信的办法很简单,一般是用牙骨小刀等轻轻挑开,或在蒸汽上蒸一下,便可揭开。据说用鸡蛋白封的信口不容易拆


旧社会里的鸡鸣狗盗和侦缉人员

龙场悟道…

“一回生,二回熟,三回精”,这是过去盗贼们自己常说的口头语。那是说一个初出茅庐的人,第一次是生手,被捉关在看守所或监狱以后,便可在这些地方找到有本领的师傅,传授他一套“本领”。等到刑满释出后,他就成为一个熟手。万一再次被捕入牢,他又可以得到进一步学习、研究“技术”的机会,再出去时便成为精通此道的老手。有些人还可以收上几个徒弟,所以他们把坐牢看成是加强本领与结识伙伴的机会。


我所知道的郑介民

祝姑娘今天掉坑了没

梧州


20251016 沈醉回忆录:军统内幕

拥有快乐的秘密

摘要:在非洲生长的塔希偷窥到姐姐杜拉割礼死亡从此留下了深深阴影,在和美国非洲裔牧师儿子亚当口交后,因为不断被被囚禁的反抗首领发言洗脑,最终自己选择当初给姐姐做割礼的萨利太太做了割礼,从此陷入不幸。在当地解放后她回到当地找到萨利太太,发现她其实对割礼也有不满却被当地架到了民族英雄的位置上,最终闷死她并接受了死刑。

(1.02%)艾丽斯·沃克的第一部长篇小说《格兰奇·科普兰的第三次生命》(The Third Life of Grange Copeland)发表。由于小说中塑造了自甘堕落的黑人男性形象,被多数评论家认为有悖于黑人作家要塑造正面的黑人男性群体形象的传统原则,一时间,她成为美国非裔文学批评界的众矢之的。

笔记:和踩组没区别

(9.93%)有这么一种鸟儿,当朋友之间永远别离、不复相见时,它们总是会哀鸣

(9.93%)现在,我们要给一个又一个的男人投票了

笔记:的确,法国60年代才女性独立
(48.63%)母亲生活在父亲的阴影之下。她鄙视这个男人,盼着他去死。但同时,她又渴望抱孙子,希望这个误入歧途的女儿能生下孩子。这位母亲说,在这个世界上,只有小孩子值得最甜蜜的吻。

笔记:多年媳妇熬成婆,非洲版

(62.32%)每个人在诞生之初都被赋予了性别迥异的两个灵魂,或者说,都被赋予了与两个独特人格相对应的两套法则。在男人身上,女性的灵魂被封存在了阴茎的包皮中。在女人身上,男性的灵魂被封存在了阴蒂中。”

(62.32%)人类的生命没有能力支撑两个灵魂的存在,每个人都必须融入看上去与自己最为契合的性别之中。”

(62.32%)因此,男人割去包皮,以除去他的女性特质。女人割去阴蒂,以除去她的男性特质。

笔记:啥玩意啊

当我看“热恋琪”时我在看些什么

认真地拥抱

刘恋和薛凯琪之间特别吸引人的地方大概就在于认真地拥抱。

影视剧里常用的,让人相信两人彼此真心相爱的,往往也是认真地拥抱。像《廉政英雌》里三位女主角遇险获救后的拥抱,或者其它警匪、枪战片的拥抱。

爱情里面,比较有印象的应该是《妙手仁心》结尾Henry和Annie的拥抱,带着一种失而复得从而永不肯放手的决定,又带有羞赧和调戏的撒娇,那个真的很美很让人欣慰。

刘恋和薛凯琪的拥抱有意思在,首先她们不是影视剧人物而是真实存在的。在安全的环境里,这么认真地去拥抱,去真诚表达出自己的心情,首先就得对方让自己感到安全。

第一次的拥抱在浪3刘恋被淘汰后酒店的相遇,两人甚至互相陪着对方等团队、等赵梦,等都到了以后分开,才去拥抱。薛凯琪的脸看不到,但刘恋明显是忧伤但又有些担心的。那句以后“别哭那么厉害了”,更多是抱着以后可能不会再有太多交集的嘱托。

而第二次,刘恋空降杭州的节目,可能因为薛凯琪想带她认识自己的朋友们并圆她演戏梦想,也可能还因为彼时父亲住院急救母亲又白内障自己承担起所有开支所以选择不返港,她想要有一个人可以说说话,可以放松下心情。

第三次,我当前最喜欢的拥抱。让人转圈后再抱进怀里,Fiona那一刻肯定是想安慰和开解刘恋的,但这个看上去听上去都没有预兆的拥抱,让刘恋都愣了一秒,才让人觉得,这拥抱多么珍贵、多么安心、多么让人可以放下担忧。

也许这就是刘恋抖音1分钟,却45秒薛凯琪,仿佛号是薛凯琪的一样。

关于努力的call back

20250126卫兰的第八期vlog出了,刘恋薛凯琪恰到好处地出现了合照那几秒的后台,神态亲密自然,让我突然想起像合照、聊天这样的环节大多数都会出现在薛凯琪的vlog里,除了这期。

“此处待插入动图”

这期的vlog唯二场景是两人在薛凯琪家跟着声乐老师练歌,以及刘恋晚上很晚到长沙后第一时间去薛凯琪房间练歌,全程镜头里就两个人,气氛也很快乐。从场景上来说整个vlog第一反应就是两人关系是非常亲密的。

“此处插入vlog”

但再看合照部分的不合理处,再想起浪3被提起过很多次而声生不息有提起却被切掉的,薛凯琪一直想表达的努力而言,这段vlog大概率是想表达她车采时用“嫌弃”语气,舞台采访时认真语气的“她是我见过的最努力的艺人”,这句话在“她很努力之外”其实还有句小的没说出来的“我也一直很努力所以会很欣赏喜欢努力的人”。

后面这句话,体现在了“TVB处处问”里提及的方大同,因为知道方大同很努力所以她才会说大家愿意一直等到康复那天;也体现在加更的环节里“因为不擅长跳舞所以10小时10小时地跳,努力之后就会有自信”。

这种对努力的欣赏又可以反过头去解决浪三的一些疑问:被剪到看不出交互且是竞争对手的《自己》和《雾里》两个组是怎么做到在返场时薛凯琪张开双臂去迎接刘恋的?这动作展现出来的亲密感和认同度并不是薛凯琪很喜欢抱人可以解释的,更像是私底下已经抱了好几回了,再结合二公分组选歌的时候两人一直提心吊胆到最后开心地“我终于可以合唱了耶”那里的亲密,都不像是偶尔探班的关系。

这答案,也许藏在赵梦的微博和朱洁静直播里。

朱洁静三公直播有提到雾里时一度练到凌晨四点,赵梦也提过那时候和刘恋同寝两个人一个手机一个电脑卷到凌晨三四点。赵梦一公和薛凯琪同组,大概率私下是讲过的;而两个组的训练室隔的又近,那些疯狂练习在《自己》组下班后看到也不足为奇。本着交朋友目的参加浪3的薛凯琪在几次串门子后看到一直在学习和加练的刘恋,也可能会想到准备演唱会时练舞练到昏天黑地的自己,从而产生微妙的认同感,继而在某些擦肩而过的场合抱抱她,散发光和正能量。

尽管一直被贴学霸标签,但刘恋做事显然还不具有事半功倍的效果,访谈之类的也说明她有着这样的苦恼。一公队友是首席和副团以及正经学过舞蹈的齐溪,她努力是性格使然是追求舞台效果,同时还有些额外的不能拖后腿的压力。这种压力队友们肯定有帮助但未必能开解,局中人痛苦如此,此刻局外的隔壁的薛凯琪来散发光和正能量,就和后面粤语老师一样,是可以抚慰人心的。

而“我终于可以合唱了耶”了这句,又是薛凯琪对刘恋唱歌最深的肯定。声生不息里她也对卫兰说认识十多年没机会一起表演,现在终于有了,她真的很喜欢也很想和每一个喜欢的朋友合作。

喜欢捏人

刘恋好像真的很喜欢捏薛凯琪的胳膊。喜马拉雅说是想要进一步亲密,但考虑看那么多年电影都没见有谁有这习惯,再查到说很喜欢捏小孩子的胳膊哄她入睡,不知道为啥突然有了种“摸摸你冷不冷”的感觉,带着关系和对小孩子的无限宠溺。

22年她还一度喜欢抚摸或者放在颈椎那里,这动作其实蛮有趣的。一般来说要么搂肩要不搂腰,薛凯琪又没有颈椎病,这动作说亲密又感觉差点意思,说不亲密都搁人颈部了。那个位置那么脆弱,所以是想保护吗?又或者只是半个拥抱?因为手肘正好可以垂在背上又不用面对面贴一起。

全心的信任

1、刘恋&薛凯琪

(1)2022年的折手指游戏(又名我有你没有)

2年以后在看那句“你有为我嚎啕大哭过”都得感慨这东西放小说里都觉得精妙,而此刻的两人不过认识3个月。刘恋脑子确实是快。

快在她非常直接但又巧妙地让了薛凯琪一手,于继宵夜后又多了个可以服务对方的内容;也快在她当着镜头认真地向对方确定自己的位置。

这个话题,在那个时候的当下不好提。结识多年后或许会在某个午后提起笑说:“哎呀那个时候你为了我大哭过呢!”,但那个只认识三个月,也只合作过一次的当下,是很难问出口的。因为问了,就显得认真,很容易让人有所误解;但不问,又不放心,不知道那一刻眼泪到底为谁而流。所以只有这时候,用着半玩笑的态度去确认,去送分,才能真正体会到那一天的幕后,哭到到处转,哭到7月扫楼还在说“其实你不知道那个时候……”的种种幕后,都是为了自己。

于是2025年,薛凯琪昭告天下24年圣诞两个人一起去朋友那里参加生日&圣诞聚会时堵在路上一个人练歌一个人休息才半点也不突兀。

因为两年半前,我就知道你会在我生活里成为一个重要人物。

不一定得是爱情,但一定非常重要!

(2)不知道为什么我(刘恋)在你(薛凯琪)面前显得很呆

刘恋在薛凯琪面前呆(扫楼),车采无法反驳和反抗(对比流俗地里阿银对拉组和聪明人)

雏鸟效应,主心骨

全然信任的放松,乐夏后台vlog,求助

快乐、疯狂(杜凯音乐节的冷静,Fiona享受舞台,全情投入)

风格只是幻觉

风格是品牌的故事,是创作的来时路,但不是我们追求的样子。它只是一系列机缘巧合下的幻觉。

永别了~陨落的女战士!

这些年我跟筱烨一起送走了不少小动物,只有招财,你走的姿势跟他们都不一样。

你是我这辈子同居的第一只小动物。

小时候我在楼下捡过一只小猫,但拿回家以后,阿嬷不同意我养,就把它洗干净吹吹干,又放回原地了。那时候发小家里养了只猫,我和同学们都很喜欢去她家,多半也是因为想去摸摸那只小猫。你和别的猫不一样,不是那种温顺的、粘腻的、乖巧的,恰恰相反,你的温柔和关心只给筱烨一个人,你总是像一个大姐一样照着她,帮她一起对抗这个世界。

我直到今天都还记得,2009 年 11 月,我刚来到深圳的那一晚。我去到筱烨和李雪她们的出租屋里,看见地上放着一盆白白净净的沙子时,心里还在想这两个女生可真有意思,还在家里玩沙子。于是把手伸进去,像搓超市里的米一样,在里面抓了几把,感受那种摩擦和包围的手感。

直到筱烨告诉我,那是你的猫砂。

后来李雪就离开了深圳,我和筱烨开始同居,你和我们一起迎来了宝子、小咪,和我们一起从南园村搬到南光村,从南山搬到龙华,再搬回南山,又搬来龙岗,前前后后 16 年。你见证了我们俩所有的喜怒哀乐,关于生活,关于工作,你是除了筱烨以外,这辈子陪伴我最长时间的灵魂。

小柒从小就挺怕你的,我以前也挺怕的。但其实你并不是很凶,你只是气势很足,从不妥协。你从小咪刚来家里的时候就像个大姐姐一样地照顾她,教她上厕所,给她舔毛,把她宠得到现在也还像个小孩子。所以作为家里真正意义上的老大,你总是在柜子的顶上照看着这一片地盘。直到疫情那一年,你因为口炎被折腾得差点死掉。

我们那时候多担心你扛不过去,所有能尝试的药和针都试过了,我们甚至用上了从香港进口的美国特效针,最后你扒光了牙,都没有办法。但就是那么艰难,你也一直顽强地挺了下来。幸好筱烨后来在网上找到了一种别人自制的偏方,没想到居然有效,你又多活了五年。

近几个月看着你越来越瘦,吃得越来越少,尤其最近这两个星期,曾经如此强壮的你,也瘦得几乎没有了重量。这几只新来的猫咪们似乎并不服你这个老大姐,总想挑战你的江湖地位,但直到三天前,你还像往常那样跳上去书柜的顶上,不稳当,但也丝毫不后退。你知道自己没法跟这几个小家伙硬碰硬,但气势和声音上从来没有输过,他们也仿佛知道,只要吓唬吓唬你,让你发出威胁的声音,我们就会半夜从床上起来给他们吃的。前两天晚上,我为了把他们从你身边赶走,还从床上摔下来,可太好笑了。

你又多活了五年,真的挺了不起的!

这几天,我们都有预感,你快要走了。毕竟,曾经送走过那么多小动物。但你知道吗?他们在临走前都非常平静,呼吸慢慢缓下来,身体慢慢地冷下去,唯独你,也只有你在弥留之际,在意识模糊、眼神涣散的时候,仍然在顽强地斗争。你微弱的呼吸,时不时会加强,你安静的身体,隔一阵就会抽动、紧张,你会伸伸脖子仰仰头,你的全身都在战斗,战斗到最后的那一刻。

如果你是一个人,必然是这人世间的豪杰、枭雄。

这个冢,是我能为你做的最后一件事情。我会替你保护好筱烨的,安息吧!不求再相见,愿你免轮回。

大师之钥补完计划 :||

在 2.0 版本的基础上增加了剑尖的胶塞,形态上组成了一把完整的大师之剑。

当它没有作为鼓钥匙来使用的情况下,可以作为项链或者包包的挂饰来使用。

为 Docusaurus 添加 Shiki 代码高亮支持

本文记录 https://docs.halo.run 集成 Shiki 代码高亮的过程。

引言

从 Halo 1.x 开始,我们就一直在使用 Docusaurus 来构建 Halo 的文档。直到 Halo 2.21,我们已经累积了大量的文档,期间发现代码块高亮的问题难以解决。Docusaurus 默认使用 Prism.js 来渲染代码块,且几乎没有其他选择。而我们的文档中使用的一些语言或框架(如 Vue),Prism 并没有提供高亮支持,因此长期以来这些代码块都没有显示语法高亮。即使是 Prism 所支持的语言,渲染出来的语法高亮效果也不尽如人意。

直到后来了解到了 https://shiki.style/,一个较新的代码高亮库,基于与 VSCode 同源的 TextMate 代码高亮引擎,渲染出来的代码效果与 VSCode 一致,并且支持现在主流语言和框架代码的高亮。我认为这几乎是当前各方面最优的代码高亮库。后面我们也为 Halo 开发了 Shiki 代码高亮插件,表现十分良好。因此最近也考虑将 Halo 文档中的代码高亮渲染改为使用 Shiki。好在搜索一番后,已经有了一个广泛讨论的 issue:https://github.com/facebook/docusaurus/issues/9122,并且里面也有人提供了可行的方案。于是我按照这个方案并添加了一些额外功能,本文将记录实现的完整过程。如果你也在用 Docusaurus,并且对 Shiki 有需求,可以参考本文。

背景

  • 目前使用的 Docusaurus 版本是 3.8.0

安装依赖

Docusaurus 的 Markdown 引擎核心为 https://mdxjs.com/,而 MDX 内部基于 remarkrehype,因此 Docusaurus 实际上预留了可以添加 rehype 插件的配置,所以可以直接使用 Shiki 官方的 rehype 插件

pnpm add @shikijs/rehype shiki -D

添加配置

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
// [!code ++:11]
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
                // or
                // langs: ['js', 'ts']
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

其中,langs 可以只填写所需的语言列表,我这里为了省事直接添加 Shiki 所有语言,主要是因为文档太多,已经懒得去统计用到了哪些语言。

此外,theme 也可以指定多主题,如果需要让文档的暗色和亮色模式下代码块的主题不同,可以按照下面的方式更改:

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
// [!code --]
                theme: "catppuccin-mocha",
// [!code ++:4]
                themes: {
                  light: "github-light",
                  dark: "github-dark"
                },
                langs: Object.keys(bundledLanguages),
                // or
                // langs: ['js', 'ts']
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

module.exports = config;

然后在 custom.css 中添加:

[data-theme="dark"] pre {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
}
[data-theme="dark"] pre span {
  color: var(--shiki-dark) !important;
}

由于我期望亮色和暗色模式下都使用暗色的代码块主题,所以没有添加多主题配置。

组件覆盖

由于需要完全让 @shikijs/rehype 接管 Markdown 文档中的代码块渲染,我们需要覆盖 Docusaurus 内部 Pre/Code 的组件,避免被默认的 Prism 处理。Docusaurus 默认提供了 CLI 用于导出 Docusaurus 主题中的组件。

npx docusaurus swizzle @docusaurus/theme-classic MDXComponents/Code --typescript --eject

然后打开 src/theme/MDXComponents/Code.tsx 并修改为:

import type { ComponentProps, ReactNode } from "react";
import React from "react";
import CodeInline from "@theme/CodeInline";
import type { Props } from "@theme/MDXComponents/Code";

function shouldBeInline(props: Props) {
  return (
    // empty code blocks have no props.children,
    // see https://github.com/facebook/docusaurus/pull/9704
    typeof props.children !== "undefined" &&
    React.Children.toArray(props.children).every(
      (el) => typeof el === "string" && !el.includes("\n")
    )
  );
}

// [!code ++:3]
function CodeBlock(props: ComponentProps<"code">): JSX.Element {
  return <code {...props} />;
}

export default function MDXCode(props: Props): ReactNode {
  return shouldBeInline(props) ? (
    <CodeInline {...props} />
  ) : (
    <CodeBlock {...(props as ComponentProps<typeof CodeBlock>)} />
  );
}
npx docusaurus swizzle @docusaurus/theme-classic MDXComponents/Pre --typescript --eject

然后打开 src/theme/MDXComponents/Pre.tsx 并修改为:

import React, { type ReactNode } from "react";
import type { Props } from "@theme/MDXComponents/Pre";
export default function MDXPre(props: Props): ReactNode | undefined {
  return <pre {...props} />;
}

小插曲:当时到了这一步的时候,突然意识到似乎可以复用之前为 Halo 开发 Shiki 插件时发布的 NPM 包(@halo-dev/shiki-code-element)。因为这个包封装了一个 Web Component,所以肯定可以用在这里,只需要在 pre 标签外包裹一个 shiki-code 即可。尝试了一下确实可行,但这样就必须在客户端渲染了。虽然可行,但始终不如在构建阶段就渲染好。虽然可以尝试使用 Lit SSR,但考虑到文档中有一些代码块使用了 Title Meta,而目前 @halo-dev/shiki-code-element 还不支持,所以放弃了这个方案。

完成这一步之后,就可以尝试启动开发服务器了。不出意外的话,代码块就可以正常使用 Shiki 来渲染了。

添加标题支持

原来 Docusaurus 的默认方案是支持为代码块添加顶部标题的,切换到 Shiki 之后,这一部分需要自行实现,以下是具体步骤:

首先为 Shiki 添加一个自定义的 Transformer,用于解析标题的书写语法和添加代码块参数:

创建 src/shiki/meta-transformer.js

function parseTitleFromMeta(meta) {
  if (!meta) {
    return "";
  }
  const kvList = meta.split(" ").filter(Boolean);
  for (const item of kvList) {
    const [k, v = ""] = item.split("=").filter(Boolean);
    if (k === "title" && v.length > 0) {
      return v.replace(/["'`]/g, "");
    }
  }
  return "";
}

export function transformerAddMeta() {
  return {
    name: "shiki-transformer:add-meta",
    pre(pre) {
      const title = parseTitleFromMeta(this.options.meta?.__raw);
      if (title.length > 0) {
        pre.properties = {
          ...pre.properties,
          "data-title": title,
        };
      }
      return pre;
    },
  };
}

然后修改配置:

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");
const { transformerAddMeta } = require("./src/shiki/meta-transformer");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
// [!code ++:3]
                transformers: [
                  transformerAddMeta(),
                ]
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

修改 Pre.tsx 显示标题:

import React, { type ReactNode } from "react";
import type { Props } from "@theme/MDXComponents/Pre";

type PreWithDataTitle = Props & { "data-title"?: string };

export default function MDXPre(props: Props): ReactNode | undefined {
  const title = props["data-title"];
  return (
    <div
      style={{
        ...props.style,
        borderRadius: "var(--ifm-pre-border-radius)",
      }}
      className="shiki-code-wrapper"
    >
      {title && (
        <div className="shiki-code-header">
          <span>{title}</span>
        </div>
      )}
      <div className="shiki-code-content">
        <pre {...props} ref={preRef} />
      </div>
    </div>
  );
}

这里还修改了 MDXPre 标签的组件结构,为后续的功能做准备,其中最外层的 div 添加的 style 属性来自于 Shiki 渲染结果的 pre 标签的样式,包含背景色和字体默认颜色。

最后我们需要为自定义的 MDXPre 组件结构添加样式,这里为了让结构看起来更清晰,我引入了 SASS 插件:

# 安装所需依赖
pnpm add docusaurus-plugin-sass sass -D

修改配置文件:

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        // ...
        theme: {
// [!code --]
          customCss: require.resolve("./src/css/custom.css"),
// [!code ++:4]
          customCss: [
            require.resolve("./src/css/custom.css"),
            require.resolve("./src/css/shiki.scss"),
          ],
        },
      }),
    ],
  ],
// [!code ++]
  plugins: [require.resolve("docusaurus-plugin-sass")],
};

然后创建 src/css/shiki.scss

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }
}

这样就支持使用原有的语法为代码块添加标题了。

显示行号

Shiki 原生并不支持在渲染的 HTML 结果中包含行号信息,但社区中有人提供了一种使用纯 CSS 实现的方案,详见:https://github.com/shikijs/shiki/issues/3#issuecomment-830564854

修改 shiki.scss

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }
// [!code ++:36]
  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end
      }
    }
  }
}

这样就可以默认为所有代码块添加行号显示了。

复制按钮

Docusaurus 默认的代码块有复制按钮,改为 Shiki 之后这部分也需要自行实现,以下是具体步骤:

修改 src/theme/MDXComponents/Pre.tsx

import React, { type ReactNode, useRef, useState } from "react";
import type { Props } from "@theme/MDXComponents/Pre";

type PreWithDataTitle = Props & { "data-title"?: string };

export default function MDXPre(props: PreWithDataTitle): ReactNode | undefined {
  const title = props["data-title"];
// [!code ++:11]
  const preRef = useRef<HTMLPreElement>(null);
  const [copied, setCopied] = useState(false);

  const handleCopy = () => {
    const code = preRef.current?.innerText || preRef.current?.textContent || "";

    copyText(code, () => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };

  return (
    <div
      style={{
        ...props.style,
        borderRadius: "var(--ifm-pre-border-radius)",
      }}
      className="shiki-code-wrapper"
    >
      {title && (
        <div className="shiki-code-header">
          <span>{title}</span>
        </div>
      )}
      <div className="shiki-code-content">
// [!code ++:8]
        <button
          className="shiki-code-copy-button"
          onClick={handleCopy}
          title={copied ? "已复制!" : "复制代码"}
          style={{ ...props.style }}
        >
          <i className={copied ? "tabler--check" : "tabler--copy"}></i>
        </button>
        <pre {...props} ref={preRef} />
      </div>
    </div>
  );
}

// [!code ++:24]
export function copyText(text: string, cb: () => void) {
  if (navigator.clipboard) {
    navigator.clipboard.writeText(text).then(() => {
      cb();
    });
  } else {
    const textArea = document.createElement("textarea");
    textArea.value = text;
    textArea.style.position = "fixed";
    textArea.style.opacity = "0";
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.select();
    try {
      const successful = document.execCommand("copy");
      if (successful) {
        cb();
      }
    } catch (err) {
      console.error("Fallback: Oops, unable to copy", err);
    }
    document.body.removeChild(textArea);
  }
}

添加样式:

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }

  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end
      }
    }
// [!code ++:15]
    .shiki-code-copy-button {
      position: absolute;
      top: 0.5rem;
      right: 0.5rem;
      opacity: 0;
      z-index: 2;
      width: 2rem;
      height: 2rem;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0.4rem;
      border: none;
      cursor: pointer;
    }
  }
// [!code ++:33]
  &:hover {
    .shiki-code-copy-button {
      opacity: 1;
    }
  }

  .tabler--copy {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z'/%3E%3Cpath d='M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1'/%3E%3C/g%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }

  .tabler--check {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m5 12l5 5L20 7'/%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }
}

这样就可以在鼠标悬停到代码块时显示复制按钮了。

集成必要的 Transformer

Shiki 官方提供了一些非常有用的 Transformers,比如行高亮、代码对比等,这里根据自身需要添加即可。

pnpm add -D @shikijs/transformers
const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");
const { transformerAddMeta } = require("./src/shiki/meta-transformer");
// [!code ++:4]
const { transformerMetaHighlight } = require("@shikijs/transformers");
const { transformerNotationDiff } = require("@shikijs/transformers");
const { transformerNotationFocus } = require("@shikijs/transformers");
const { transformerNotationErrorLevel } = require("@shikijs/transformers");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
                transformers: [
// [!code ++:8]
                  // 行高亮,使用 Meta 信息的方式,比如 ```java {1}
                  transformerMetaHighlight(),
                  // 代码对比,使用注释的方式
                  transformerNotationDiff(),
                  // 行聚焦,使用注释的方式
                  transformerNotationFocus(),
                  // 行高亮的错误和警告变体,使用注释的方式
                  transformerNotationErrorLevel(),
                  transformerAddMeta(),
                ]
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

这些 Transformers 只是为对应的行添加了 class,我们需要自行实现样式。以下是完整的 shiki.scss

点击查看 shiki.scss
.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }

  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {
        z-index: 1;
        display: block;
        width: max-content;
        position: relative;
        min-width: 100%;

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end

        // highlighted lines start
        .highlighted {
          width: 100%;
          display: inline-block;
          position: relative;
        }

        .highlighted::after {
          content: "";
          position: absolute;
          top: 0;
          bottom: 0;
          left: -0.75rem;
          right: -0.75rem;
          background: rgba(101, 117, 133, 0.16);
          border-left: 1px solid rgba(34, 197, 94, 0.8);
          z-index: 0;
        }

        .highlighted.error::after {
          background: rgba(244, 63, 94, 0.16) !important;
        }

        .highlighted.warning::after {
          background: rgba(234, 179, 8, 0.16) !important;
        }
        // highlighted lines end
      }

      // focus line start
      &.has-focused .line:not(.focused) {
        opacity: 0.7;
        filter: blur(0.095rem);
        transition: filter 0.35s, opacity 0.35s;
      }

      &.has-focused:hover .line:not(.focused) {
        opacity: 1;
        filter: blur(0);
      }
      // focus line end

      // diff start
      &.has-diff .diff {
        width: 100%;
        display: inline-block;
        position: relative;
      }

      &.has-diff .diff.remove::before {
        content: "-";
      }

      &.has-diff .diff.add::before {
        content: "+";
      }

      &.has-diff .diff.remove::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: -0.75rem;
        right: -0.75rem;
        background: rgb(239 68 68 / 0.15);
        border-left: 1px solid rgb(239 68 68 / 0.8);
        z-index: -1;
      }

      &.has-diff .diff.add::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: -0.75rem;
        right: -0.75rem;
        background: rgb(34 197 94 / 0.15);
        border-left: 1px solid rgb(34 197 94 / 0.8);
        z-index: -1;
      }
      // diff end
    }

    .shiki-code-copy-button {
      position: absolute;
      top: 0.5rem;
      right: 0.5rem;
      opacity: 0;
      z-index: 2;
      width: 2rem;
      height: 2rem;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0.4rem;
      border: none;
      cursor: pointer;
    }
  }

  &:hover {
    .shiki-code-copy-button {
      opacity: 1;
    }
  }

  .tabler--copy {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z'/%3E%3Cpath d='M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1'/%3E%3C/g%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }

  .tabler--check {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m5 12l5 5L20 7'/%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }
}

至此,一个比较好看、功能丰富的代码高亮改造方案就完成了,具体修改代码也可以查阅 Halo 文档的 PR:https://github.com/halo-dev/docs/pull/521,也可以访问 Halo 文档查看修改之后的效果。

参考资料

大师之钥 2.0 全新版本!

很早就想改良了,但是一直懒得做结构。自从上次给小红书网友定制了小樱和皮卡丘的鼓钥匙,终于对改良大师之钥有了具体的思路。这两天趁着设计项目进度慢下来,就赶紧测试了三四个版本,终于确定下来了!

相比二月份做的版本,2.0 有更好的外观 + 更强的结构:

1、从一体打印改为分件设计;
2、提升关键结构的强度和耐久;
3、更接近原作的配色与质感;
4、可量产的打印配置;
5、可拓展/定制的设计架构。

我真是无所不能啊 :p hiahiahia~

相比初代,可以说是质的飞跃了!

当然,2.0 也可以有丝光版本。

在同一种材质下对比,就更能看出不同的工艺和拆件设计,对成品质量的影响有多大了。虽然会增加两条拆件线,但所有外观面的品质都提升了相当多。

通体丝光,咋一看是非常讨好眼球的,但就像美颜过度的照片,既会显得成品油,又会导致视觉失重没有焦点。剑柄没有抢宝石和剑身的戏,这个节奏更恰当一些。不过,具体的颜色和材料,未来还可以再继续试。

二月做初版的时候,还没尝试过什么其他材料,这种图新鲜和拿锤子找钉子的心情,和二十年前刚认识设计的时候很像:

堆料就是好!More is gooooood!

这个过程或许真的很难完全省略或者跳过,但可以随着自我成长,缩短每一次进入的时间和路程。

不敢想,但忍不住想…

最近的感觉太好了,好得我有点害怕…

明明前段时间还像一只发霉的蘑菇埋在墙角,最近这一个多星期天天两点睡七点醒还精神得不得了,天天哼着歌去工作室。想法一个接一个,东西一个接一个地做,好久不打开的音乐和播客 APP 天天连着音箱在工作室里放,嘴里还总是甜甜的……

感觉好棒,但又害怕 😦

鼓钥匙:小樱魔杖

之前给朋友做过一款赛尔达大师之剑的鼓钥匙,发出来以后陆续收到不少私信问能不能定制其他款式。

说实话,鼓钥匙这个形态它还是限制比较多的。因为要跟架子鼓本身的结构配合,所以很多造型没有办法做。

这一把小樱的钥匙,磨磨蹭蹭也做了将近三个月。当然,并不是说做这把钥匙需要花那么长时间,只是因为我是用身体状态正常的间隙时间,抽空一点一点弄的。当中也测试验证了很多轮不同的结构、拆件和打印方式,最终才找到了一个比较合适的方案。回头我再整理一下过程,发出来给大家看看。

我有 AMS 也不是说不能一体打印,但一体打印的话,由于打印本身的工艺限制,在 Z 轴的方向是比较脆弱的,所以在拧的时候稍不注意就会拧断。因此在设计结构的时候也尝试了蛮多种思路,最后这一版算是把结构强度跟外观质感平衡得还算满意的了。

这次尝试用 nano banana 做了两张效果图,就是粉红沙滩那两张。其实很简单,就是先拍摄实物的定妆照,再放进去修改背景和光线。效果确实相当好,省了不少事!

松松有条

不知道多少年没听人用「松弛」来形容我了,今天过来朋友公司聊点事,久违地又听到了一次。

回来的路上看到这个,忍不住偷拍,好可爱!

我也想这么穿,两只不一样的鞋!

哈哈哈哈哈哈哈哈哈哈哈哈哈

计划得有,但不能只有。

使用 Rspack 构建 Halo 插件的前端部分

更新(25-06-19)

现在已经为插件的 UI 部分提供了新的配置方式,https://www.npmjs.com/package/@halo-dev/ui-plugin-bundler-kit 包提供了 rsbuildConfig 方法,可以更加方便的使用 https://rsbuild.dev/ 来构建 UI 部分。

Rsbuild 基于 Rspack 构建,提供了更完善的 loader 配置,所以在封装的时候就直接选择了 Rsbuild。

安装依赖:

pnpm install @halo-dev/ui-plugin-bundler-kit@2.21.1 @rsbuild/core -D

rsbuild.config.mjs:

import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";

export default rsbuildConfig()

package.json 添加 scripts:

{
  "type": "module",
  "scripts": {
    "dev": "rsbuild build --env-mode development --watch",
    "build": "rsbuild build"
  }
}

需要注意的是,为了适应新版的 https://github.com/halo-dev/plugin-starter,默认生产构建输出目录改为了 ui/build/dist ,如果你要从已有的插件项目迁移到 Rsbuild,建议参考 https://github.com/halo-dev/plugin-starter/pull/52 对 Gradle 脚本进行改动,或者自定义 Rsbuild 的配置以保持原有的输出目录配置:

import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";

const OUT_DIR_PROD = "../src/main/resources/console";
const OUT_DIR_DEV = "../build/resources/main/console";

export default rsbuildConfig({
  rsbuild: ({ envMode }) => {
    const isProduction = envMode === "production";
    const outDir = isProduction ? OUT_DIR_PROD : OUT_DIR_DEV;

    return {
      resolve: {
        alias: {
          "@": "./src",
        },
      },
      output: {
        distPath: {
          root: outDir,
        },
      },
    };
  },
});

示例:https://github.com/halo-sigs/plugin-migrate

了解更多:https://docs.halo.run/developer-guide/plugin/basics/ui/build


前情提要

Halo 插件的 UI 部分(Console / UC)的实现方式其实很简单,本质上就是构建一个结构固定的大对象,交给 Halo 去解析,其中包括全局注册的组件、路由定义、扩展点等。 基于这个前提,在实现插件机制时,主要面临的问题就是如何将这个大对象传递给 Halo。当初做了非常多的尝试,最终选择构建为 IIFE(Immediately Invoked Function Expression,立即执行函数),然后 Halo 通过读取 window[PLUGIN_NAME](PLUGIN_NAME 即插件名)来获取这个对象。 构建方案采用 Vite,并提供了统一的构建配置。回过头来看,这个方案存在不少问题:

  1. 会污染 window 对象,虽然目前并没有出现因为这个导致的问题,但是从长远来看,这个方案并不是最优的。(当然,使用 Rspack 来构建并不是为了解决这个问题)

  2. Vite 不支持 IIFE / UMD 格式的代码分割(主要是 Rollup 还不支持),无法像 ESM(ECMAScript Module)那样实现异步加载模块的机制。

  3. 基于第 2 点,如果插件中实现了较多的功能,可能会导致最终产物体积巨大,尤其是当用户安装了过多的插件时,会导致页面加载缓慢。

    1. http://www.halo.run/ 为例,gzip 之前接近 10M 的 bundle.js,gzip 之后也有 2M - 3M。

    2. 以此博客为例,gzip 之后也有 1.8M 的 bundle.js。

  4. 基于第 2 点,如果不支持代码分块(Chunk),也无法充分利用资源缓存,访问页面时,也会一次性加载所有插件的代码(即便当前页面不需要)。

基于以上问题,我开始寻找其他替代方案,最终通过翻阅 Rspack(Webpack 的 Rust 实现)的文档发现,Webpack 能够通过配置实现 IIFE 格式的代码分割,最终选择 Rspack 作为尝试。

基本的 Rspack 配置

安装依赖:

pnpm install @rspack/cli @rspack/core vue-loader -D

package.json 添加 scripts:

{
  "type": "module",
  "scripts": {
    "dev": "NODE_ENV=development rspack build --watch",
    "build": "NODE_ENV=production rspack build"
  }
}

rspack.config.mjs:

import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';

// plugin.yaml 中的 metadata.name
const PLUGIN_NAME = '<YOUR_PLUGIN_NAME>';

const isProduction = process.env.NODE_ENV === 'production';
const dirname = path.dirname(fileURLToPath(import.meta.url));

// 开发环境启动直接输出到插件项目的 build 目录,无需重启整个插件
// 生产环境输出到插件项目的 src/main/resources/console 目录下
const outDir = isProduction ? '../src/main/resources/console' : '../build/resources/main/console';

export default defineConfig({
  mode: process.env.NODE_ENV,
  entry: {
    // 入口文件,可以参考:https://docs.halo.run/developer-guide/plugin/basics/ui/entry
    main: './src/index.ts',
  },
  plugins: [new VueLoaderPlugin()],
  resolve: {
    alias: {
      '@': path.resolve(dirname, 'src'),
    },
    extensions: ['.ts', '.js'],
  },
  output: {
    // 资源根路径,加载代码分块(Chunk)的时候,会根据这个路径去加载资源
    publicPath: `/plugins/${PLUGIN_NAME}/assets/console/`,
    chunkFilename: '[id]-[hash:8].js',
    cssFilename: 'style.css',
    path: path.resolve(outDir),
    library: {
      // 将对象挂载到 window 上
      type: 'window',
      export: 'default',
      name: PLUGIN_NAME,
    },
    clean: true,
    iife: true,
  },
  optimization: {
    providedExports: false,
    realContentHash: true,
  },
  experiments: {
    css: true,
  },
  devtool: false,
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: [/node_modules/],
        loader: 'builtin:swc-loader',
        options: {
          jsc: {
            parser: {
              syntax: 'typescript',
            },
          },
        },
        type: 'javascript/auto',
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          experimentalInlineMatchResource: true,
        },
      },
    ],
  },
  // 这部分依赖已经由 Halo 提供,所以需要标记为外部依赖
  externals: {
    vue: 'Vue',
    'vue-router': 'VueRouter',
    '@vueuse/core': 'VueUse',
    '@vueuse/components': 'VueUse',
    '@vueuse/router': 'VueUse',
    '@halo-dev/console-shared': 'HaloConsoleShared',
    '@halo-dev/components': 'HaloComponents',
    '@halo-dev/api-client': 'HaloApiClient',
    '@halo-dev/richtext-editor': 'RichTextEditor',
    axios: 'axios',
  },
});

配置需要懒加载的路由或者组件:

在 index.ts 中配置路由:

import { definePlugin } from '@halo-dev/console-shared';
import { defineAsyncComponent } from 'vue';
import { VLoading } from '@halo-dev/components';
import 'uno.css';
// [!code --]
import DemoPage from './views/DemoPage.vue';

export default definePlugin({
  routes: [
    {
      parentName: 'Root',
      route: {
        path: 'demo',
        name: 'DemoPage',
// [!code --]
        component: DemoPage,
// [!code ++:4]
        component: defineAsyncComponent({
          loader: () => import('./views/DemoPage.vue'),
          loadingComponent: VLoading,
        }),
      ...
      },
    },
  ],
  extensionPoints: {},
});

注:推荐使用 defineAsyncComponent 包裹,而不是直接使用 () => import() 的方式,后者会在进入路由之前就开始加载页面的代码分块(Chunk),导致页面在加载期间没有任何响应。

构建产物示例:

❯ ll src/main/resources/console
.rw-r--r-- 191k ryanwang staff 16 Jun 10:47  359-3bebb968.js
.rw-r--r--  83k ryanwang staff 16 Jun 10:47  962-3bebb968.js
.rw-r--r-- 4.1k ryanwang staff 16 Jun 10:47  main.js

其他配置

集成 Scss / Sass

安装依赖:

pnpm install sass-embedded sass-loader -D

rspack.config.mjs 添加配置:

import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';
// [!code ++]
import * as sassEmbedded from "sass-embedded";

...

export default defineConfig({
  ...
  module: {
    rules: [
      ...
// [!code ++:13]
      {
        test: /\.(sass|scss)$/,
        use: [
          {
            loader: "sass-loader",
            options: {
              api: "modern-compiler",
              implementation: sassEmbedded,
            },
         },
        ],
        type: "css/auto",
      },
    ],
  },
...
});

集成 UnoCSS

如果你习惯使用 TailwindCSS 或者 UnoCSS 来编写样式,可以参考以下配置:

本文推荐使用 https://unocss.dev/,因为可以利用 UnoCSS 的 https://unocss.dev/transformers/compile-class 来编译样式,预防与 Halo 或者其他插件产生样式冲突。

安装依赖:

pnpm install unocss @unocss/webpack @unocss/eslint-config style-loader css-loader -D

入口文件(src/index.ts)添加导入:

import 'uno.css';

rspack.config.mjs 添加配置:

import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';
// [!code ++]
import { UnoCSSRspackPlugin } from '@unocss/webpack/rspack';

...

export default defineConfig({
  ...
  plugins: [
    new VueLoaderPlugin(),
// [!code ++]
    UnoCSSRspackPlugin()
  ],
  ...
  module: {
    rules: [
      ...
// [!code ++:5]
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
        type: 'javascript/auto',
      },
    ],
  },
...
});

uno.config.ts:

import { defineConfig, presetWind3, transformerCompileClass } from 'unocss';

export default defineConfig({
  presets: [presetWind3()],
  transformers: [transformerCompileClass()],
});

.eslintrc.cjs:

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-recommended',
    'eslint:recommended',
    '@vue/eslint-config-typescript/recommended',
    '@vue/eslint-config-prettier',
// [!code ++]
    '@unocss',
  ],
  env: {
    'vue/setup-compiler-macros': true,
  },
// [!code ++:3]
  rules: {
    "@unocss/enforce-class-compile": 1,
  },
};

总结

以上就是针对 Halo 插件前端部分的 Rspack 配置。我已经对 Halo 官方维护的部分插件进行了迁移,几乎没有遇到什么问题,并且带来的收益非常明显:www.halo.run 和本博客的 bundle.js 在 gzip 之后仅有不到 200k,各个页面也只会在访问时加载所需的资源。

需要注意的是,我对这些构建工具并不算非常熟悉,所以配置仍然有优化空间。我们会持续优化,后续也会考虑提供一个通用的 CLI 或 Rspack 配置,期望实现如下效果:

rspack.config.mjs:

import { rspackConfig } from '@halo-dev/ui-bundler-kit';

export default rspackConfig({
  ...
});

或者基于 Rspack 包装一个 CLI:

plugin.config.mjs:

import { defineConfig } from '@halo-dev/ui-bundler-kit';

export default defineConfig({
  ...
});

package.json:

{
  "scripts": {
    "dev": "halo-ui dev",
    "build": "halo-ui build"
  }
}

参考文档

感谢阅读,欢迎交流与指正!

拆掉旧镜腿,换上新镜腿。

原先那副眼镜腿前两天被我掰坏了,硬是晃晃荡荡地用了两三天。这两天一边工作一边做新眼镜腿,总算弄好了!

意外收获是,测试验证设计过程中发现,打印眼镜腿这种曲线长造型所形成的树形支撑特别好看:

今天得了个奖 :)

所谓沉默的父爱,不过是迂腐的东亚父权造就出来的文化怪胎而已。爱就是爱,只要是真心的爱,一定会被感受到。

2024.11.28 23:19

开咖啡馆的第二大好处——可以随意安排作息时间,这几天暂时失效了。因为楼上楼下的邻居都在装修,8点钟机器开始轰鸣,老小区红砖楼板的结构随之颤抖。解决办法就只能是我自己早睡一会了。我没去打听,仅凭瞎猜,大概反正也不买新房了,索性春节前把老房子装修一新算是提高生活品质吧。这个不太好评价,虽然我对未来的经济环境预期不佳,不过一般人总不至于倾家荡产去搞装修;把钱投在日常生活里,倒比这个时候搞投资要好得多。说到投资,有朋友跟我说看到好多新店开张,怎么还能讲经济环境不好呢?此言可就差矣,君不见这许多人,要么是原来的生计眼瞅着不行了才换条道儿试试,要么是想安顿但从毕了业就从来没机会安顿下来,误以为开家小店或许是条出路……真心祝福,但我实在乐观不起来。向来后知后觉的老街坊们都像是有了勒紧腰带的觉悟,那些在经济活动中活跃敏感的市场消费主力,还能热情高涨地持续支持新开的店铺吗?

fin.

摘掉眼镜的山林,在教我理解空间

最近开始喜欢在家附近的公园里走,一个星期去那么两三次,每次在里面溜达一个半小时到两个小时。我很喜欢走在树林间的感觉,这种感受之前在东北走辽塔的时候我就觉察到了。从前我就知道自己喜欢山野,但工作后的这十几年好像慢慢地遗忘或者注意不到它了。

不知为何,某天我心中有个声音,叫我去那片林子里走走,脑海里就有那个公园的画面出现。刚好那天下午我的状态还可以,能走出门,愿意走出门,就去了。在山林里,整个人都舒服得多。

今天,我有了新的体验。

过去几次,我要么是慢跑,要么是散步,但共同点是我都和日常一样戴着眼睛,以及习惯性地向右转。今天下午,我忽然觉得可以试着摘掉眼镜试试,因为反正也是听着播客散步,并不需要眼睛看得多清晰,看到周围的人反而会让我不自在。

于是,我摘掉眼镜,向左转。

「看不见的,看见了。」

我想起《月亮忘记了》里的这句话。

我走了好久,发现看不清周围并没有让我感到过去的那种恐慌,反而我能清晰地感受到周围的空间、光线、气流、气味。随着脚步的移动,树木的颜色、前后关系、周围空间的轮廓,都比我戴着眼睛看要更清晰。这不是关于分辨率的清晰,是关于感受的清晰。我清晰地感受到这座山、这片池水、这些树林,我好像不存在,就像一个物理模型里的「理想镜头」,我没有实体,只是一个观察的视角。

我试着戴上眼镜再看,都不对,一切都不对,没有空间,只有平面,摘掉眼镜,空间就显现。

如果停下来,空间也会消失,哪怕摘下眼镜。走起来,不断移动着,空间就会显现,生动地包围着我。

看不清时,我却感受到了一种真实。

我再次想起了前段时间自己把玩的一个文字游戏:

一わ全,全わ一=ichiwazen, zenwaichi=一わZEN,ZENわ一

全=ZEN=禅

我要多来这片山林,这里喜欢我。

先答应

最近有几件事,给我了一些触动。

因为确诊抑郁症后,我向公司请了长假来休息,所以忽然多了很多大片的空闲时间。可是吃药的感觉很不好,昏沉、嗜睡、动力低下且多屁。我觉得这不是适合我的方式。于是有了健身,有了我的理智告诉我:

先答应,强迫自己出去。

第一件事是汉洋跟我说他们计划九月初去一趟东北,给辽塔扫描建模,问我要不要一起。其实前几年他问过我好多次,每一次我都因为忙于工作,婉拒了,有时呢,是因为懒得动,也婉拒了。这次我心中有个声音:你先答应,然后就不得不去了。我就这么把自己推了出去,跟汉洋、Tim、夫聪去辽西走了一趟。

一上车,汉洋就问我,为什么这次有空来了?我说,重度抑郁症休病假了。他和 Tim 很自然地说,哦,这个咱们身边搞创作的朋友很多,然后就开始直奔沈阳。汉洋还给我拿着一台他刚从日本淘回来的 Mamiya 645 1000s,这是我第一次正经使用一台胶片相机,并且是一台 120 画幅的腰平取景器的机械单反。

这台相机在这一趟,教会我一些事。回头我再把整理好的照片陆陆续续发出来,有些照片我还是很喜欢的。这一趟时间虽然不长,但它不仅让我这个广东仔第一次对东北有了清晰的体会,也触动了我心中的一些东西。

出发前,脑放电波的 Nixon 问我要不要在苹果发布会之前合作一期节目。我下意识地想婉拒,但另一个声音说:

先答应,又不用你操心,你说就好,答应了再说。

这样,我又一次把自己推向了「不得不做」的位置上。

那一期节目似乎很不错,反响挺好。甚至一些路人都留言表示很喜欢这一期,说很有收获和启发。这对我是一种鼓舞。

在东北的路上,我们在车里聊起理想 mega、小米 SU7 的设计,汉洋说我们回去之后录一期节目吧。我其实不太想,毕竟工业设计这个母题太大了,轮不到我这个在设计领域里并无建树的人来说。但是,可以先答应吧,万一能聊出什么来呢?后来回到深圳,汉洋跟轶轩一起,我们仨在汉洋的酒店房间里聊了两个小时,在轶轩那些简单、外行、尖锐的问题的触动下,我觉得那一期节目剪出来之后应该不会太差。虽然可能只是很基础的科普,但大体上应该值得一听。

结束后我问轶轩,这样聊下来,你现在知道工业设计是做什么的了么?他说,虽然不能简单地描述出来,但确实理解了。

这又是一次把自己推出去,但不差的体验。尽管那天我们被突如其来的大雨浇透了,但也因此在轶轩家里打了几把《黑神话·悟空》,能算是好事吧。而且,就在临出门吃晚饭前,辽塔之行的大部分胶片都扫出来了,全部看下来,有几张还是不错的。他俩纷纷表示,作为第一次用胶片,算是很成功了。

也许有鼓励的成份,但有几张我很喜欢,回头要找 Tim 用飞思精扫再制作出来。

和创作有关的事情,我都不觉得累。

最后一天我在 Tim 的工作室里问他:经常接触不一样的项目,你会觉得疲惫吗?他的回答是,如果经常做一样的事,我就会觉得非常疲惫。

我也一样。

那天还偶遇了梁源,他们在楼下录了一下午节目,聊黑悟空里的佛教文化和文物。我旁听了几小段,挺有意思的。节目这两天也陆续上线了,虽然我说很感兴趣,但也确实提不起劲儿去点开它们,只能先 Mark 在列表里。

去找 Tim 的前一天,跟汉洋去了他们现在的工作室。养伤的 JT 在做日常的康复力量训练,看上去也很迷茫。晚上跟重轻一起吃饭,他看着也挺疲惫,疲于应付白天无聊活动的倦怠。我似乎向来都很喜欢这般真性情的人,嬉笑怒骂都可以自然流露。依稀记得也曾有人这样评价我,但又似乎是很遥远的故事碎片。

今天早上突然想看看苹果新品,手欠翻了翻图纸,看着看着就似乎琢磨出一些线索……截图往群里一放,两颗皮蛋就来问我要不要一起做一期节目。

好吧,虽然我原本可能想搞一期《设以观复》的,但我可能做不动了,如果有他们一起搞的话,是不是我自己的节目真的无所谓,但起码算是对一直关注我的人们有一个交待吧。他俩八月份就问过我和 Toby 要不要在发布会后一起录一期播客,没曾想居然还凭空出来期视频。

且不管能出来什么,先答应吧。

答应了就得不得不面对,不能偷懒。

我是病了,但不是傻了,如果说这段时间我发现了什么之前没注意到的事情的话,那就是「先答应」吧。

我过去很紧张,要有安排,要有预期,要有 planB 和后手,但渐渐发现有这些也不怎么管用,突发状况永远层出不穷,它们总能在预想之外的地方出现。先答应,硬着头皮上,反而似乎并没有我以为的那么多阻力。

例如这两天跟着筱烨去了音乐教室,学了十分钟,阿吉就让我弹贝斯,和小柒筱烨合了一首曲子,最简单几个位置就能出来很棒的旋律。今晚的中秋活动,虽然我们都不太想参加,不想去人多的地方,但为了给阿吉捧场,还是一家人都去了。躲在人群里的感觉并不放松也不自在,但音乐本身能令我感到舒服。

如果把抑郁症看作是太上老君的炼丹炉,似乎可行。

升级版的「用户路径」

单车 x3

今天上午我跟往常一样,把车停在 Coffee Vendor 的门口,喝了一杯就去公司了。在外面跑了一天,看了三家供应商,晚上将近 22:00 才回到公司。然而当我骑上车就感觉不太对劲,后面的轮子有一种不平整的段落感。我停下车,捏了捏后轮,发现它居然完全没有一丁点气了。明明上午还是好端端的,经过一天的暴晒,居然爆胎了?

于是,我推着车走回宿舍。

一路上我边走边想,印象中,路上应该有一家修车的车行。当我穿过拥挤的人群和车流,眼睛的余光扫到了一处不起眼的角落,一个非常有年代感的小小的修车行。

不到 5 分钟,师傅就把胎补好了。胜惠五蚊。

看着他修车的过程,我有那么一瞬间,仿佛回到了小时候。在西区,那两三个自行车修车铺在什么地方,他们大概是什么样的布置。我已经有很多很多年,没有见过这样的画面。十年吗?不止十年,可能快二十年了。

我这辆车有很多小毛病,自己也修过好多回,包括在暴雨中,把它推到路边,浑身湿漉漉的,用纸巾捏着链条把它修好。每一次修它,我心里都特别的烦躁、焦急和无助。这并不是因为修理它很难,而是因为这些状况总是那么突如其来、毫无防备,把我打得措手不及。

因此,上周某一个晚上,当我在路边看到那个人遇到麻烦时,会果断停下来。

那天晚上,大概八九点的样子,我从公司骑车回去,走在每天都会经过的那条路上。前面有一个推着板车卖水果的阿婆,整条路变得非常狭窄和拥挤。阿婆推着车慢地往前走,在她的右手边有一辆蓝色的共享单车。共享单车旁站着一个刚从车座上下来的人,她弯着腰,检查车轮,仿佛有什么东西卡在了车轮里。

我慢慢地骑过她身边,转过头看了一眼,然后停下车问了一句:你怎么了?需要我帮忙吗?

她抬起头,苦笑着,看向我说道:我的衣服被卷进车轮里了!

我把车往前挪了挪,停在路边。我们先是让车轮分别往前和往后转了几圈,找到衣服缠进去的方向;然后慢慢地让衣服从车轮中转出来。但卷进车轮中的衣服,扭曲搅动在一起,衣领帽子袖子又分别缠在一起,所以又花了一些时间,慢慢地把每一个东西都捋顺。前后大概也就五六分钟的样子吧,她对我连连说了几声谢谢,我摆摆手说没事儿,转身就坐上自行车离开了。

因为做了这么一件力所能及的小事,我开心了好一阵。

那天晚上,我脑海中闪过一个念头:如果我每天都日行一善的话,能不能让我爸爸的病有所好转?

上个周末,我们回去了。克服了很多困难和麻烦,回去看见他还在,能自己走,能吃,能自己洗澡,就很好。

茫茫人生,好像荒野。

麦当劳令人失望的玉米杯

麦当劳你要减少一次性用品,有指标压力,这我非常理解。但以前,你是先设计好免吸管杯盖,且普及了一段时间后才取消吸管的。现在你的玉米杯没有任何免勺子的可用方案,就直接取消勺子,还直接张嘴找客户收费,这吃相也太难看了。

别说玉米杯设计了类似饮料开口的盖子,这杯盖显然不是为此设计的,完全不可用。你要是老老实实设计个新盖子,那取消勺子我举双手赞成。

以麦当劳的体量,这个设计修改所涉及的模具、运输、仓储成本都可以做到比市价低很多的水平。

你们为什么不能像以前取消吸管一样有条理地处理问题?你们引以为傲的 SOP 失效了吗?总不能说没钱请设计师吧?

你可是麦当劳啊……非常失望。

政策是政策,设计是设计,不然著名的翻转车头大灯是怎么出现和流行的?不要因为政策就认为不行,也不要觉得商家转嫁成本就是不得已跟合理,无管杯盖就是最现成的好案例,能做到而不作为,这才是重点。

输出是一种排泄

在不同的平台上时不常的都能看到一些内容创作者他们会有疑问,说我的东西明明很有深度,准备得也很充分,制作也很用心,但是为什么没有获得很好的流量,或者其他的回报?这种时候要么就是真的有疑问,要么就是想通过这种疑问的方式,来表达对于这种流量的不满或者鄙视。

每次看到他们说这种话的时候,我就会代入到自己。我也有很多内容是花了很多心思很认真做的,但就是没有什么人看,没有什么人听。前几年确实会有疑惑,但现在我很坦诚地接受自己就是不擅长做那种大众流量欢迎的内容。

这里并没有鄙视大众流量的意思,我是真的发自内心的不懂,哈哈哈哈哈~

因为我做内容 99% 的动机,都只是为了把脑袋里的东西腾出来,它只是我的一个思考过程的外化。有人获得共鸣和启发,那就最好,没有那也无所谓。因此我确实没有真的花过心思在研究怎么样制作大家都喜欢的那种类型的内容,因为我也确实没有发自真心地想把自媒体作为自己的一条所谓职业赛道来看待。

因此,没有获得那样的流量,是很正常,也应该的。

每次洗头都带着信念:)

因为这头长发打算捐给小朋友,所以每次洗头吹头的时候,都生怕搞坏了,小心翼翼的。

有时觉得累,也想找理发店去洗,但又怕他们用的东西有问题,残留了,会影响小朋友的身体,每次再麻烦都自己洗。

快四十了,从来没有这么仔细对待自己头发的时候。

我带着三把枪去了警局

晚上做了个梦,趁醒来这一会还记得,记录下来:

梦里的我,是一个去某个政府机关办点什么事的一般市民。正好撞见了一个眼熟的警察,我凑过去一看,哦,是黄sir(无间道里黄秋生饰演的角色)。我本来打算上去打招呼的,但是他突然跑起来去追一个人,我就也跟着追了过去。跟着他上了一座人行天桥,他和他追的人都停了下来,隔着大概两个人臂展长度的距离,互相用枪指着对方,气氛紧张,就算是梦里,也能感觉到环境里呼吸的热气和汗水的湿润气息。

整个空间的色调是暖调和暗调的,是一种电影里拍摄城区里路灯下会用的影调。光线是顶光,俩人的脸上都是眼睛鼻子嘴的阴影。我这时才看清楚,和黄sir对峙的人,是陈桂林(周处除三害中阮经天饰演的角色)。他嘴里说着一些什么,我反正也听不清楚,也不敢靠近去听。只看到他一步步走近黄sir,两人竟然靠得非常非常近,到了两支枪口互相低着对方的额头的程度。忽然间,陈桂林举枪的手送了下来,就一瞬间,但是我看到了,因为是梦,我看到的甚至是特写镜头,他的手指没有扣住板机,手腕也轻微地放松了一点点。这时候,黄sir果断伸手抢下了陈桂林的枪。

他转身就把他和陈桂林的两把枪都给了站在旁边的我,说你拿回警局去。

我莫名其妙接受了这个临时任务,光明正大又小心翼翼地拿着两把枪在大街上走。我不知道该怎么拿枪,即不让它们吓到路人,又不容易因为姿势而走火打中自己。我小心地把手指从板机后面穿过去,抵住它,然后枪口反拿,以一种类似于西部片里枪插在枪套里的姿势,拿在手里。我快速冲到了一个像政府机构的建筑里,因为梦里我也不知道警局在哪里,反正调整好枪的姿势之后,一抬头就是这个地方了。

这里居然有一大片草地,就在楼前。

然而当我准备去交枪的时候,我看到,我爸爸正坐在草地上。应该说,他坐在一块铺在草地上的野餐布上。我这辈子都没见过这样的他。

视线穿过他,我看见远处有个人忽然举起了一把枪,筱烨和小柒就坐在他附近。周围的人似乎并不是警务部门的人,大家都僵住了。我莫名其妙地就冲上去一把抢走了他手上的枪,就是那种我自己也没看清怎么做到的就抢下来的那种。明明我手上还有两把枪,怎么会有余力和多余的手去抢呢?不解。总之,我带着三把枪,走到警务处,很得意地告诉他们,我把这三把枪都带回来了。心里还想,他们是不是会给我颁一个类似于「好市民奖」之类的东西,TVB 和港片里干这种事的人都会有这样的台词或心理活动的,我觉得应该有机会。

正想着,就走出来回到了楼前的草地。

我以为我爸会跟我说点什么,但他没有,他只是看了我一下,然后继续看不知道哪个方向的天空。我也并没有期待什么,只是觉得「哦,好久不见」。

梦就醒了。

横看成岛 纵观为舟

刚在地上捡到小柒的一位舞蹈老师的证明材料,上面显示,这位老师是 2001 年生人,也就是说 00 后已经在给 80 后的我的小孩上课了。同时,他年收入 12 万,换算成月收入就是 1 万,我在他这个年纪时,月收入是 3000 元。

多么具象的「时间」啊!

不仅仅是年龄代际上的时间感,这个早就有体会了,更主要的感受是收入数字的增长和货币的膨胀。我毕业那会住 ¥350 的房间,一个月的吃和交通下来还能剩 1000 左右。那时候,我存了三个月钱,买了人生中第一台相机,是松下的 LX3,当时的售价就是 3000 元。

前些年,深圳设计公司的应届生行情大概是 6~8K 这个水平,中单率高的设计师一个月最多能拿到 2 万。但今天的 2 万的购买力跟十几年前比,显然远不如那会儿般松弛。在现在的深圳,1 万的收入至少要拿出 5~6 成交给房东,自己能支配的部份吃吃喝喝谈个恋爱就不剩什么了。

单看数字,挺大,但也是因为显得大,才觉得时间可怕。

晚上我一边溜着狗,一边把这张照片发到朋友圈,看着这个画面,我脑海里浮现出了这八个字:

横看成岛,纵观为舟。

因为满眼的绿色虽然被分成了三等分,但它平均的质感以及层次,让我觉得很像一片海面,而这个路人就像海面上的一座小岛。小岛的这个意向,其实来自于我朋友圈那个封面。有一次中午我在公司附近散步,看到了海上的一艘货船,它正好行驶到这个位置上,我拍了这张照片。

它是一艘船,但它跟海上的这些电塔共同构成了一个新的画面,看起来像一座小岛。

我常常觉得,如果我们采用一种与别人横向比较的方式来看待问题,那么我们会给自己找到一个所谓的位置。这个位置它码住了我们,规定了我们必须要做的一些事情。

但其实如果把这个视角转过来,转换到在这个行走的路人身上,他面前其实除了这条路以外,这片所谓的海洋其实是身边的一面墙,他可以走这条路,他也可以不走。在他那个维度里头,路有很多条,他是一艘自由的航船,而这个视角从旁人看来,是看不见的。

經濟再差也不能公開談論

經濟狀況究竟有多糟糕呢?從各大品牌在售後策略、降價思路和運營的混亂程度等方面的表現,均可窺見一斑。尤其是當你置身於自媒體、電商與品牌運營三者的交匯點上,這種巨大的荒謬性將更加明顯。

上週末出差重慶,兩周沒在家,難得一個週末,結果倆人坐下後就被各自工作群里的事情纏著,不是回消息就是打電話,咖啡都沒喝上一口。

在國內的社交媒體似乎不讓提「經濟不好」這樣的事,與之相關的話也會被限制,索性我就轉成日語來發了:

経済状況は本当にどれほど悪いのでしょうか?各大ブランドのアフターサービス戦略、値引きの考え方、そして運営の混乱度などから、その一端を窺い知ることができます。特に、個人のネットワークソーシャルメディア、ECサイト、およびブランド運営の交差点に立つと、この巨大な不条理さがさらに明白になります。

先週末、出張で重慶に行ってきました。二週間も家に帰っていなかったので、久しぶりの週末を楽しみにしていたのですが、結果として、座った途端、それぞれの仕事グループからの連絡が絶えず届き、メッセージを返したり、電話をしたりすることに追われてしまいました。コーヒーすら一口も飲めないままです。

為什麼是轉日文不是英語呢?因為即便是英語,在內地的網絡環境里也顯得有些直白了。日語反而更有「似乎知道在說什麼,但根本看不懂」的戲劇化的「陌生化」的效果。

好不容易,終於把翻了一年的《夜航西飛》讀完了。

這是我今年讀完的第三本書。

昨天去宜家看洗手檯和鏡櫃,直到在餐廳排隊前一秒,都沒想起宜家給我發的領生日蛋糕的短信。可就是那麼巧,下周生日,昨天正猶豫要不要去店裡看看,我就慫恿筱燁說想幹就幹,這一來才想起有一個蛋糕等著領。這就是天注定的意思。

[1116]房间布局大调整

娃的房间一开始做了个带书架的电脑桌,后来我又买了一个1米×0.7米的桌子,前者被老婆嫌弃了很久,后者又让空间利用成为了问题。

这个电脑桌桌面摆放了很多的东西:路由器、NAS、显示器、打印机、插线板。再加上要买的UPS,大概是挤不下了,于是重新布局这个区域的念头上了心头。

放假前,教育相关公众号发了篇放假期间的注意事项,包括用手机的时候建议投屏到大显示器上,想到现在的那个实在是太老了,当即心下一动,就决定买个大的了。

双节假期到来,买了卷尺量量尺寸,琢磨琢磨布局,于是一口气买了四样东西:

  1. APC Back-UPS Pro BR550G-CN。APC网上搜到的,看着支持NAS,也懒得比较别的,价格不到一千,差不多就行了。
  2. AOC 27吋2K显示器。原来那个2007年买的20吋AOC显示器,一直用到现在,虽然曾一两年前一度偶发几次异常,但很快又没了,于是得过且过地用着。这次照旧买了这个品牌的,也算支持国产吧!
  3. 左转角电脑桌。电脑桌要个转角的,我们两年前有考虑过,由于空调管子在墙角从上到下垂着,导致桌子无法两边都贴墙。退而求其次,让桌腿与空调管不碰到就好了,于是买了一个1.6米×1.2米×0.55米、几案形式、整板桌面的。
  4. 摆放电子设备的三层机架。准备放在桌子下面角落。

待10月8日到货,就忙碌了起来,腾空原处,书架上的书都搬下来,清空了柜子抽屉,要挪出房间的时候一看高度,书架比房门还高个十几公分,只好一个人把书架放倒侧立后推出去了。

原计划这个书架要挪到客房去的,一想进门的拐角,预料不太好弄,等到她娘俩回来,两番尝试,翻边侧倒,好不容易推进了房间,不想又遇到了进去后翻不过来的窘境,齐心协力,把书架抬到了床上,由我在背后把它推立起来,然后一起把桌子放到地面,想法完美“落地”!哎,你看,男人还是要给力的,柔柔弱弱可不行!

UPS呢,姑且把NAS插在了Master插座上。机架果如所料,带着轮子70公分高还是放不到75公分高的桌子底下,果断把轮子拆掉就好了。显示器吧,跟2018年的15.6吋的三星笔记本相比,色彩鲜艳些,看电影爽多了!娃上编程课,那字就大多了,一如预期地满意!

❌