所有文章26 篇
用 Astro 做工业官网➔
我用 Astro 做了 LIANXIN 的工业机械官网,里面有产品目录、工厂展示,也接了 Directus 新闻和 AI 产品顾问。做这种网站,很多选择都落在同一个问题上:哪些内容应该直接送到用户面前,哪些功能等用到的时候再启动。 机器图片、参数表、介绍文字适合提前生成;新闻要能随时更新;轮播、咨询和视频又各有自己的交互。把这些边界拆清楚,后面的实现会顺很多。 官网现用的工厂主题海报。来源:LIANXIN 官网。 页面先到,交互按需启动 Astro 比较合我胃口的一点,是它允许我从 HTML 开始组织页面。产品介绍和图片这些内容先渲染出来,需要状态和事件的地方再接 React。装了 React 集成,也不用把整页都做成客户端应用。 本站导航用 client:load,页面加载时就激活;产品轮播用 client:visible,进入视口再加载和水合。两种方式都可以先输出 HTML,区别在于何时让组件在浏览器里运行。具体触发时机可以看 Astro 的客户端指令文档。 FAQ 更进一步,用了项目自己注册的 client:interaction。鼠标移入、触摸按下或键盘聚焦时开始加载。这里有个小坑:用户可能在加载结束前就点了按钮,所以指令会拦住这次点击,等组件就绪再补发。普通链接照常跳转。省 JavaScript 可以,别把第一次点击也省了。 新闻更新走另一条路。列表和详情按请求读取 Directus,首页的最新新闻则交给 server:defer。首页先返回主体和占位内容,新闻区块随后单独请求 HTML。这样 CMS 响应慢一些,也不会拖住整个首页。这部分需要服务端,我用 Node adapter 的 standalone 模式运行站点。 Astro 官方原理图,说明静态页面与动态区块如何分别取得内容。来源:Astro 5.0,© Astro;此图并非本站部署架构图。 产品页和 AI 共用一份资料 产品有不同系列,每个型号又有图片、参数和应用说明。如果这些东西散在页面里,改一项参数就得挨个找。我把产品整理成 TypeScript 数据,再让页面读取它。型号详情用同一份模板,通过 getStaticPaths() 生成路由。 这是详情页里的路径生成部分,省略了导入和页面模板。 export function getStaticPaths() { return products.map((product) => ({ params: { category: product.category, slug: product.slug, }, props: { product, category: getCategory(product.category), }, })); } 参数表读这份数据,页面里的 Product 结构化数据也读它。AI 产品知识同样从这里生成,避免官网改了规格,顾问还在背旧版本。数据模型里保留型号、分类、图片和规格这些明确字段,页面只负责把它们展示出来。 AI 后端是独立的 Cloudflare Worker。它根据问题里的型号、别名和当前页面检索资料,最多选六份交给 DeepSeek,再检查回答附带的来源 ID 是否属于这批资料。浏览器拿到的是回答和对应的站内链接,模型密钥留在 Worker。 这种检索方式规模小、规则直接,适合目前这批产品。它能约束资料来源,不能保证模型每句话都对,具体选型和报价仍要人工确认。另外,共用源文件不等于自动同步上线。产品资料改动后,主站和顾问 Worker 都需要重新构建部署,这个步骤不能漏。 图片讲构图,视频讲时机 工业设备本来就长,外面再套几层大边框,能看清的机器只会越来越小。产品图这里用 object-fit: contain,保留完整外形;标题、卖点和按钮放到旁边,让图片有足够的展示面积。轮播控制留在左右两侧,翻图时不用往下找。 IBM 80E 的展示图,机身轮廓完整保留。来源:LIANXIN 对应型号页。 首页海报则准备了横、竖两版,用 <picture> 按 max-aspect-ratio: 1/1 切换。选图依据是视口形状,竖屏平板也能得到竖版构图。首张海报优先加载,后面的图延迟加载。文案目前也在海报里,换来的排版控制更直接,代价是改文字和做多语言都要重新出图。 视频的处理更干脆。工厂介绍先展示封面,视频地址放进 data-src,点击播放时才赋给 src。访问者只是看看页面,就不用顺手下载一段几分钟的视频。 播放函数中的关键动作如下,省略了加载提示、超时和错误处理。 if (!video.hasAttribute("src")) { video.src = video.dataset.src!; } void video.play(); 这一块用原生 <video> 和一个自定义元素管理播放状态,就够用了。网络慢时显示加载提示,失败后给出重试按钮和原视频链接;离开组件时中止事件监听并暂停视频。用户至少能知道点下去之后发生了什么,也有办法继续操作。

用 Android 做疲劳检测➔
我做了一个 Android 疲劳提醒应用,用前置摄像头观察闭眼和张嘴,在手机上完成检测。模型接进来以后,真正需要想清楚的是后面的判断。眨一下眼就报警,提醒很快会变成噪声;摄像头看不清了还显示正常,又会让人误解系统的能力。 先把眼睛和嘴巴量出来 项目用的是 ML Kit Face Detection,模型随 APK 打包,安装后不用再等模型下载。它提供人脸轮廓、嘴部关键点和睁眼概率,疲劳规则由应用自己实现。这样检测和判定就能分开,调整持续时间不用动相机代码。 模型负责提取人脸信息,什么时候提醒,由后面的规则决定。插画来自 Google ML Kit 官方文档。 眼睛用 EAR,也就是眼睛的纵横比。从眼轮廓里选六个点,把两段上下眼睑距离相加,再除以两倍眼宽。计算式是 EAR =(上下一组眼睑距离 + 另一组眼睑距离)÷(2 × 眼宽)。闭眼时高度变小,EAR 通常也会下降。用比例可以减轻人脸远近带来的尺度变化,但侧脸、遮挡和关键点偏移仍然会影响结果。 Google ML Kit 的官方轮廓示例,点图可查看原图。眼睑和嘴唇的点要分清,本文的 MAR 量的是唇外沿。这是官方示例,不是本应用实测截图。图片来源。 嘴巴用 MAR。这个项目取上唇外上沿中心到下唇外下沿中心的距离,除以左右嘴角距离。公式看着简单,取点位置却不能含糊。量唇外沿和量嘴巴内部开口,算出来的数不会一样,网上找到一个阈值直接抄过来,很可能对不上。 眼部还保留了 ML Kit 的睁眼概率,与 EAR 做 OR 判断,任一路满足条件就进入对应计时。概率这一路要求左右眼的值都有效。这样能让两路信号各自参与,但也意味着单路误差就可能触发候选;它们来自同一个检测器,不能当成两套独立证据。嘴部单独判断,眼睛暂时不可用时,有效的 MAR 仍然可以参与检测。 一次越线,还不能算一个动作 单帧只知道这一刻眼睛有多窄、嘴张得有多大。要把眨眼、短暂张嘴和持续动作分开,还得加上时间。当前普通闭眼条件是 EAR ≤ 0.22,或两眼概率都有效时任一眼低于 0.35,持续 800 ms 后进入 WARNING。更强的闭眼条件是 0 < EAR < 0.17,或两眼概率都低于 0.35,持续 400 ms 就进入 DANGER,且优先级更高。 哈欠候选用的是双阈值。MAR 超过 0.40 才开始计时,开始后只要不低于 0.35 就继续,累计达到 800 ms 才提醒。假设数值在 0.41、0.39、0.42 之间晃,单一阈值会让计时反复归零。嘴还张着,计时器已经替你闭上了。 代码里的关键分支只有几行。下面节选自判定逻辑,省略了无效值检查和后续计时。 val yawning = mar != null && if (highMarStartMs == null) { mar > config.marYawnEnterThreshold // 0.40 } else { mar >= config.marYawnExitThreshold // 0.35 } 时间取相机帧的时间戳,不取模型回调完成的时刻。否则推理这次慢一点、下次快一点,也会混进动作时长。相邻有效观测间隔超过 1.5 秒时,未完成的计时清零,从当前帧重新开始。中间没有画面,系统就没有依据把两次张嘴拼成一次连续动作。这个间隔也是工程取舍,不能证明采样之间发生了什么。 看不清,就别继续报正常 判定器除了 NORMAL、WARNING 和 DANGER,还留了 DEGRADED。没有人脸、眼嘴信号全部无效,或检测到黑暗画面、相机异常时,就清除未完成计时,显示当前无法可靠检测的原因。NORMAL 也只表示当前有效信号没有达到提醒条件,不能解释成驾驶员一定清醒。 相机侧通过 acquireLatestImage() 取最新分析帧,当前会话已有推理任务时,新帧直接释放,避免越算越落后。停止检测或切到后台时,会话编号递增;旧任务即使晚点返回,也不能覆盖新的状态。重新开始检测后,同样要从新画面重新计时。 声音和振动只由 DANGER 触发,WARNING 只更新界面。危险告警做了约两秒冷却,并用不重放历史事件的 SharedFlow 发送;界面只在前台恢复状态下接收,暂停时停止提示。否则回到应用突然补响一声,提醒的就成了过去的自己。 目前这套实现仍是固定阈值的研究原型,没有个人校准。持续张嘴也可能来自说话,睁眼概率还有正脸角度限制。当前手机端只在模拟器上验证了安装、相机链路和前后台切换,真人识别、覆盖层精度与声音振动还需要目标手机实测。接下来更值得做的是用正常说话、短张嘴、自然哈欠和闭眼录像,在静止环境里检验误报与漏报;这套原型还不能当作经过道路验证的驾驶安全系统。

MediaPipe 手势识别的几个坑➔
这次用鲁班猫 3 做了个离线手势识别,系统是 Android 14。摄像头看到手就画出骨架,比个 OK,自动保存一张带骨架的截图。 模型直接用 Google 的 MediaPipe。它已经能找出手部的 21 个关键点,还带七种静态手势分类。自己要处理的是相机、画面坐标,以及识别结果怎么变成实际操作。 模型用现成的 Android 用 Kotlin 写,相机接 CameraX,识别接 MediaPipe Tasks。依赖用这几项。 def cameraxVersion = '1.4.2' implementation "androidx.camera:camera-core:$cameraxVersion" implementation "androidx.camera:camera-camera2:$cameraxVersion" implementation "androidx.camera:camera-lifecycle:$cameraxVersion" implementation 'com.google.mediapipe:tasks-vision:1.0.0' 把官方模型放进 app/src/main/assets,创建识别器时指定这个文件,运行模式设为 LIVE_STREAM,通过回调拿结果。官方 Android 示例已经有完整接法,照着看比从空白工程硬猜快。 模型跟着 APK 一起打包,推理在板子上完成,运行时只需要相机权限。 底图来自 MediaPipe 官方测试图片,这是本项目的板端测试输出。红点是关节点,蓝线标出拇指,其余连线用绿色。 相机别开两路 CameraX 常见的接法是 Preview 显示画面,ImageAnalysis 给模型送帧。但在我这块 RK3576 板子的固件上,两路一起开会卡住。 最后只保留一条 ImageAnalysis,预览和识别共用它。拿到 1920×1080 的图像后,处理行步幅,把最长边缩到 640 像素,再统一旋转和镜像,送进模型。 帧策略用 KEEP_ONLY_LATEST。处理不过来就丢旧帧,别让识别排队。当前代码还限制了至少 100 ms 才提交一帧,所以提交速率最高约每秒 10 帧,实际显示速度还得看推理回调。 ImageProxy 用完一定要 close,跳过帧和异常分支也一样。 缓冲区占着不放,相机后面就可能没帧了。 另外,板子上的 CAM0、Android 的 Camera ID 0 和 /dev/video0 要分别核对。这个项目同时检查物理 m00_ 模块、逻辑 ID 0 和当前固件的 FRONT 属性,条件不满足就停。这套映射只针对已经验证过的板子和固件。 骨架为什么会飘 这个问题得从坐标和时间两头查。 坐标上,模型给的是归一化关键点,预览用的却是 centerCrop。图片会先放大,再裁掉超出显示区域的部分。直接拿关键点乘屏幕宽高,当然对不上。 把关键点投到屏幕上,要和预览用同一个缩放比例和裁切偏移。下面的 x、y 是模型返回的归一化坐标,srcW、srcH 是模型输入图像的宽高。 scale = max(viewW / srcW, viewH / srcH) offsetX = (viewW - srcW * scale) / 2 offsetY = (viewH - srcH * scale) / 2 screenX = x * srcW * scale + offsetX screenY = y * srcH * scale + offsetY 这些除法要用浮点数。旋转和镜像也统一在推理前处理,别让预览和模型各算各的方向。 时间上,手已经移走了,上一帧的结果才回来。把旧骨架叠到新画面,多少有点灵魂出窍。 我的处理是从结果回调里取出对应的输入图片,和这次关键点一起更新到界面。画面和骨架必须属于同一帧。 代价是显示会等识别结果,预览流畅度也跟着受限。 还有一个容易漏的判断。分类结果是 None,只代表没认出支持的手势。只要关键点还在,骨架就继续画;没检测到手、结果过期或者切到后台时,再清掉。 OK 截图要防连拍 官方这七种分类没有 OK,我用现有关键点补了一层几何判断。拇指和食指指尖靠近,食指弯曲,另外三根手指伸直,才算匹配。 距离按掌部尺寸归一化,再结合关节角度判断。用固定像素距离的话,手往前伸一点,阈值就不合适了。这里用的是按输入宽高还原后的 2D 坐标,和画面里的骨架保持一致。 然后才是触发控制。视频每帧都可能认出 OK,你一直举着手,就一直存图,相册受不了这个。 判断 当前条件 确认手势 最近最多 5 帧中至少 4 帧强匹配,当前帧也满足,窗口持续至少 300 ms 触发后锁定 保持 OK 只拍一张 允许再拍 离开 OK 至少 600 ms,两次触发至少间隔 1.5 秒 触发条件严一点,保持条件宽一点,减少边界抖动。采样断开超过 400 ms,就清掉旧的观察窗口。这些是当前配置,实际触发时间还受帧率影响。 截图直接合成当前画面和同帧骨架,再用 MediaStore 存成 PNG。只画这两层,状态栏和提示框就不会混进去。编码和写盘放到独立线程,别堵着相机识别。 保存的是当前显示画面的合成图,输入已经缩小过,不能当成传感器原图。 这版配置的还是 Delegate.CPU。RK3576 有 NPU,但应用得接入 RKNN 才用得上。RKNN 的官方流程包括模型转换和运行时接入,前后处理也要重新对齐,光改一个配置没用。

自主进化的本地AI Agent-Hermes➔
Hermes Agent 是一款基于大语言模型的开源自主智能体工具。 只要配置好模型接口或本地环境,就能让 AI 自动编写与运行代码、调用工具、检索信息并完成多步骤复杂任务。 它是开源且免费的。 支持接入各类开源大模型以及主流商业模型 API。 具备强大的函数调用与自主任务规划能力。 支持本地部署与运行,数据和指令安全可控。 能够直接与系统终端环境及多种外部工具无缝协作。 使用方法 在本地环境中完成软件的安装与环境初始化。 配置对应的大模型 API 密钥或本地模型连接信息。 在命令行或交互界面中输入你想要执行的任务目标。 智能体会自主拆解步骤并按需调用系统工具执行。 查看执行过程与输出,等待任务自动完成。 官方链接: https://hermes-agent.nousresearch.com 进入官网即可查看详细文档与快速上手指南 满分10分 个人评分6.5分 因为功能依赖python 需要下载库 但是无法自定义国内镜像 不挂VPN得等个三四十分钟 如果没有VPN 更新和安装的过程会十分痛苦 没事 后面我会推一个好用的clash

鸟街上的孤岛➔
下午不知触动了哪根神经,突然想起小时候看过的一本小说,《鸟街上的孤岛》。 读那本书的时候,我不是在读别人的故事。我爬上了那栋废墟的绳梯,躲进三楼的储物柜;我穿过秘密通道,在犹太区和波兰区之间来回;我屏住呼吸躲德国兵,在空房子里翻找食物。那场冒险从头到尾是我自己的。这么多年过去,我还能记起那种心跳——又害怕,又兴奋,又觉得自己无所不能。一个孩子对冒险的全部渴望,那本书都给过了。 而比冒险更让我留恋的,是那里面爱情和友情的样子。 那段爱情几乎什么都没有发生,可恰恰因为什么都没有发生,反而什么都定下来了。它让我在很小的时候就认定:真正的感情是干净的、克制的、在乱世里也不改样的;是废墟里分一块面包,是朝不保夕还惦记着另一个人。友情的定性也一样——一只叫白雪的天竺鼠,几个靠得住的人。我后来对爱情和友情的全部标准,大概就是那时候照那本书定的模子,一直没换过。 后来我长大了,看了很多新闻,读了很多历史,对犹太民族渐渐有了一些不太好的看法。这话说出来不好听,可我得诚实。人到了某个年纪,心里会装下很多互相打架的东西。 奇怪的是,这并没有让我讨厌那本书。 那场冒险、那段爱情和友情,还好好地待在记忆里,还是那么亮。书里的真善美——勇敢、善良、爱、希望——至今还是我三观里最下面那层地基。我甚至偶尔会想:写下这些的人,本身就是个犹太幸存者;书里我遇见的那些人,也是犹太孩子。这让我有点别扭,可事实就是事实。大概人的心里,本来就该装得下这种别扭。 有时候我觉得,童年读过的书就像种子。它长出来的东西,不归后来那些乱七八糟的情绪管。鸟街七十八号那栋废墟,现在还立在我精神世界的某个角落,风吹不倒。

不接触SEO容易忽视的框架—Next.js与Nuxt.js➔
引言 在常规的React或Vue项目开发中,前端工程师主要致力于构建单页应用。这类应用在状态管理和局部刷新上表现优秀,开发体验好。但在涉及业务线增长,需要通过搜索引擎自然流量获客时,SPA会遇到技术瓶颈。为了解决这个问题,通常需要引入Next.js或Nuxt.js。 1、Next.js 和 Nuxt.js 是什么 Next.js和Nuxt.js分别是基于React和Vue构建的同构前端框架。 原生的React和Vue默认采用客户端渲染,即服务器仅返回一个包含基础结构的HTML文档,所有DOM节点的生成以及数据请求均交由浏览器下载并执行JavaScript来完成。 Next.js与Nuxt.js的核心特性在于提供了SSR服务端渲染和SSG静态站点生成能力。它们将页面的渲染逻辑前置到服务器端或打包构建阶段,直接向浏览器输出包含完整HTML结构的页面。 2、有什么用 这两个框架在工程化实践中的核心价值体现在两个方面: 第一是搜索引擎优化即SEO。主流搜索引擎的爬虫在抓取网页时,处理复杂且需要异步加载数据的JavaScript能力非常有限。传统的SPA在爬虫抓取时通常呈现为空白页面,导致内容无法被建立索引。引入服务端渲染后,爬虫可以直接读取到渲染好的结构化文本和链接,从而保障网页的收录效率和搜索排名。 第二是提升首屏加载速度。由于服务器直接返回完整的HTML内容,浏览器无需等待下载庞大的JS文件后再去执行渲染逻辑。对于网络环境受限或对首屏绘制时间等核心性能指标要求较高的C端产品,这种方案能大幅缩短用户的白屏等待时间。 3、为什么推荐 除了解决SEO与首屏性能问题,从工程效率的角度来看,这两个框架也具备较高的采用价值。 首先是路由设计的简化。两者均采用基于文件的路由系统。开发者只需在特定目录下按照规范创建文件,系统即可自动生成对应的路由,降低了维护中心化路由配置表的成本。 其次是具备轻量级后端能力。框架内部集成了API路由功能,开发者可以在同一个项目中编写简单的后端接口逻辑,直接进行数据库交互,非常适合全栈式微型项目的快速迭代。 最后是内置的自动化性能优化。框架底层集成了对图片、字体、外部脚本的构建与加载优化策略,开发者无需过多介入底层的Webpack或Vite配置,即可使项目达到较高的前端性能基线。 4、相比起Astro的优势和劣势 在内容型网站和SEO领域,近期Astro框架的关注度较高。与Astro相比,Next.js和Nuxt.js的优劣势较为明确,主要取决于业务场景。 Next.js与Nuxt.js的优势在于状态管理和复杂交互处理。如果业务包含高频的前端逻辑交互,例如SaaS系统后台、复杂的表单系统或在线协作工具,Next与Nuxt可以无缝复用React或Vue的完整生态,开发者在客户端与服务端之间的心智转换成本极低。 其劣势在于构建产物的体积依然偏重。即使采用SSR,Next和Nuxt最终仍需向客户端下发框架的运行时代码和状态数据,以完成页面的水合过程,这意味着整体的JS包体积很难做到极致精简。 Astro的架构设计不同,它默认采用群岛架构,核心理念是按需加载JavaScript。大部分静态页面会以纯HTML形式输出,仅在需要交互的局部组件上挂载JS。因此在构建博客、文档中心或营销落地页等重内容、轻交互的场景时,Astro的加载性能和打包体积往往优于Next和Nuxt。此外,Astro支持在同一个页面中混合使用React、Vue等不同框架的组件,提供了更高的选型自由度。 高交互且需要部分SEO的复杂Web应用更适合选择Next.js或Nuxt.js;而以静态阅读为主的内容型站点,现阶段使用Astro是更好的工程实践。

打破前端框架壁垒-前端新贵 Astro ➔
前端技术栈更迭极快,2021年才首次发布的Astro算是一个极其年轻的新起框架。作为一个专为内容展示设计的构建工具,它目前的行业热度和实际表现非常亮眼。最近我将个人站点的底层彻底重构为了Astro架构。这里直接从技术逻辑出发,梳理一下这个新框架的核心机制和开发体验。 1、Astro与其它主流框架有特别突出明显的区别? 常规的前端框架像Vue或React是把整个应用逻辑打包发送给浏览器解析。 Astro的核心区别在于默认剔除所有客户端JavaScript代码。它在构建阶段直接将组件编译成纯静态HTML。只有开发者明确标记需要交互的局部组件才会单独下发执行脚本。这种底层设计直接消除了框架运行时的性能损耗。 2、为什么推荐Astro? 2.1 逻辑简单容易上手 传统的单页应用配置路由通常需要引入专门的路由库并且维护复杂的映射表。Astro采用基于文件系统的直观路由逻辑。在特定目录下新建一个代码文件就自动生成对应的网址路径。 它的全局布局封装极其直观,没有沉重的状态管理负担,整体开发门槛比基础的Vue还要低。 假设你项目里有一个叫做 src/pages 的核心文件夹.你在里面新建几个代码文件.网页的网址就会自动按层级生成. 你新建 src/pages/index.astro 文件.用户访问 www.web-chang.com 就会直接打开这个页面. 你新建 src/pages/about.astro 文件.用户访问 www.web-chang.com/about 就能直接看到你的关于我页面. 你在 pages 目录下新建一个 blog 文件夹并新建 test.astro 文件.也就是 src/pages/blog/test.astro 这个物理路径.用户访问 www.web-chang.com/blog/test 就能直接打开这篇文章. 你完全不需要去维护类似 router.js 这种专门管理网址跳转的配置文件.你的物理代码文件夹长什么样.线上的网址路径结构就是什么样.这种所见即所得的设计直接干掉了繁琐的路由配置工作. 2.2 对服务器性能要求低能省钱 动态渲染框架需要始终消耗服务器的算力和内存来实时处理用户访问。Astro的纯静态打包产物对服务器物理性能几乎零要求。构建出来的最终文件可以直接托管到各类免费CDN或极低配置的轻量节点上。这对预算有限的独立开发者来说是极其硬核的降本方案。 2.3 兼容性好 做前端技术选型通常意味着被特定生态彻底绑定。Astro完全打破了这个壁垒。在一个Astro页面内部可以同时无缝混用Vue和React编写的UI组件。 举个极其直白的例子. 假设你手里有一段用Vue写好的顶部导航栏代码.同时你又在网上找到了一段用React写好的留言板代码.在传统的开发环境里,这两种代码底层规则完全不同,是绝对没法放在同一个页面里运行的. 但是在Astro的网页文件里,你可以直接把它们拼在一起.网页的页头位置直接引用Vue的代码,网页的页脚位置直接引用React的代码.Astro底层会自动同时解析这两种不同的语法,把它们变成正常的网页内容.它们不仅能同时完美显示在屏幕上,各自的点击反馈功能也完全互不干扰. 这种底层兼容意味着,以后你在网上看到任何好用的开源组件代码.根本不需要管它是基于Vue还是React写的,直接复制搬进你的Astro项目里就能跑通. 底层编译器会自动处理这些跨技术栈代码的依赖隔离与加载。开发者可以毫无障碍地复用自己以前积累的任何代码资产。 2.4 对SEO的优化 依靠客户端脚本动态渲染内容的网页对搜索引擎爬虫极不友好。Astro输出的纯静态HTML让爬虫能瞬间抓取到页面的全部完整文本。(WordPress相比之下垃圾来的说是)它原生提供完善的元数据配置支持。这种物理层面的预渲染机制能显著拉升站点的搜索引擎收录速度和排名权重。 3、极简的数据获取与安全闭环 独立开发者往往需要快速跑通全栈业务。Astro天然契合前后端分离的开发思维。它可以在编译阶段直接发起请求去拉取远端数据库的内容并物理硬编码写入最终的网页文件。结合无头后端管理系统,开发者可以彻底分离数据维护和前端视觉呈现。这套架构不仅从物理层面隔绝了后端接口泄露的风险,也把前端页面的访问响应速度推到了极限。 也就是直接把所有的页面都爬取下来转化成HTML文件直接供人访问

Directus实战-使用篇➔
本篇文章的实战方向就以本站点功能为导向 文章大部分比较简单,最难理解的地方是决策和角色部分,这部分逻辑和安全令牌等功能结合在一起比较绕,需要好好想一下逻辑。 1、建表 强烈建议勾选的 创建于 (date_created): 作用: 自动记录这篇文章是哪年哪月哪日几点几分创建的。 为什么建议选: 博客文章肯定需要显示“发布时间”对吧?勾选这个,你每次新建文章,系统自动打上时间戳,前端直接调取显示即可。 更新于 (date_updated): 作用: 只要你修改了这篇文章,系统会自动把这个字段更新为当前最新时间。 为什么建议选: 前端可以用来显示“最后修改于 2026-06-05”。 2. 看你需求勾选的 已存档 (archived): 作用: 相当于“草稿箱”或者“软删除”。 为什么选: 如果你写文章写了一半不想发布,或者想下架一篇文章但不想彻底删除它,勾选这个。你在前端调用 API 时,只需过滤掉被存档的文章即可。 排序 (sort): 作用: 允许你在后台用鼠标拖拽来改变文章的先后顺序。 为什么选: 对于“博客”来说,通常按时间倒序排列就够了,这个可以不勾。但如果你以后建“产品 (Product)”表,强烈建议勾选这个,因为产品经常需要手动调换展示顺序。 3. 可以不勾选的(多用户协作项) 创建自 (user_created) & 更新,由 (user_updated): 作用: 记录是哪个后台账号写了/改了这篇文章。 为什么不选: 因为这是你个人的博客和官网,后台只有你一个人用,不需要记录是谁写的。如果是企业很多人一起管后台,那这个必选。 2、创建字段 在设置里能看到你的模型(表) 点开表就能进行字段设置 够简易吧 都不需要你自己设置字段类型了 直接选择就行 不过具体的还需要你自己稍微了解一下 比如我的文章分类字段 category 就是用的下拉列表 翻译有点让人看不懂 我这边还是建议去使用英文界面 比如富文本编辑器就是”所见即所得“选项,其实就是”WYSIWYG“了啦,不知道怎么翻译的。 下面基本上都会给你一个Content Translations 翻译选项 这个也好理解 正文是 你好 你在这下面写上翻译 Hello 两种语言两种标识 zh 和 en 当你界面有翻译按键时候 中文就请求zh标识内容 英文就请求en标识内容 创建好之后保存就可以到内容界面进行添加字段 2、开放 API 读取权限(关键) 逻辑可以简单理解成 1、你是什么角色(身份)? 2、你的身份你能干什么 ?(应用策略) 比如游客访问api(用户身份),游客只有查看的权限(应用策略),但是管理员用户通过api有更新和删除的权限。 用户角色里面有个默认的”公开“角色 备注也说明了 不需要token验证 点进去能看到这个”公开“的角色有一个”图片公开“的策略 这是我自己设置的 因为图片也需要token验证才能获取的话非常容易泄露你的验证令牌 非常不建议这样做 3、API链接 简单来说就是 你创建了 blog 表 你的域名/items/表名 就能获取到你那张表的全部JSON数据 当然还有别的玩法 比如 https://域名/items/表名?filter[archived][_eq]=false&fields=id,title,article,categore 这种筛选数据的用法 详看Directus的官网文档 逻辑很简单的 4、如何添加安全验证的TOKEN令牌? 这个也好搞 第一步:在 Directus 制造“钥匙”并上锁 这个挺方便的,相比起以前的做法 只需要两步 1. 创建一个账号并生成 Token 登录 Directus 后台 点击左侧菜单的用户目录 点击右上角 + 创建用户 名字随便填,比如叫 Frontend。不需要填邮箱和密码 最关键的一步: 在用户资料页面往下滚,找到 Token 这一项 你会发现说保存用户会自动生成TOKEN,保存之后再回来就能看到 2. 创建一个对应权限策略 设置好对应表的权限 在底部分配刚刚的账户 然后就可以在postman里面进行测试即可 完结

Directus-轻量化CMS后端面板-部署篇➔
此篇主讲如何部署,使用教程在另外一篇。 1、Directus-它是一个实时 API 引擎和基于数据库的直观 Admin 面板 为什么极其适合你: 不绑架数据库: 它不会乱改你的 PostgreSQL 表结构,你的数据依然干净透明。 自动生成 API: 你在后台建好 Blog、Product、Message 三个集合后,立刻就能通过 /items/blog 或 /items/product 供你的 React 和 Vue 获取数据。 界面极佳: 后台 UI 是用 Vue 写的,非常现代化,且支持中文,可以直接拿来当企业内部系统。 权限管理: 内置完善的权限,你可以设置“前端只能读取博客,可以新增留言,但不能修改产品”。 部署: 使用 Docker 一键部署,非常轻量快捷方便部署。 Directus 是一个标准的 Headless CMS 官方文档直达链接 官方主页: https://directus.io/ 官方文档入口: https://docs.directus.io/ API 参考文档: https://docs.directus.io/reference/introduction.html 前端 SDK 文档: https://docs.directus.io/guides/sdk/getting-started.html 2、如何部署Directus 这里以宝塔面板为例 第一步:进入“容器编排” 在你的截图顶部菜单栏,找到并点击 “容器编排” 第二步:添加项目 进入“容器编排”页面后,点击 “添加项目”。 在弹出的窗口中: 项目名称:填入 directus (只能用小写字母)。 来源:选择 内容。 第三步:填入配置文件 将下面的代码复制并粘贴到配置内容的输入框中。 version: '3' services: directus: image: directus/directus:latest container_name: directus-server restart: always ports: - 8055:8055 volumes: # 在宝塔会自动在项目目录下创建 uploads 等文件夹存放图片 - ./uploads:/directus/uploads - ./extensions:/directus/extensions environment: # 1. 基础密钥 (自己设置 或者问AI帮你生成) KEY: "*****************************" SECRET: "*****************************" # 2. 你的在线 PostgreSQL 数据库连接信息 (必须修改这里!) DB_CLIENT: "pg" DB_HOST: "你的数据库IP或域名" DB_PORT: "5432" DB_DATABASE: "你的数据库名" DB_USER: "你的数据库用户名" DB_PASSWORD: "你的数据库密码" # DB_SSL: "true" # 如果你的云数据库要求 SSL 连接,就把前面 # 删掉 # 3. 超级管理员账号 (你登录 Directus 后台用的账号) ADMIN_EMAIL: "admin@yourdomain.com" ADMIN_PASSWORD: "your_password_123" # 4. 跨域设置 (方便你的 React 和 Vue 跨域请求) CORS_ENABLED: "true" CORS_ORIGIN: "*" 第四步:确认并启动 填写完毕后,点击底部的 “确认” 。宝塔会自动开始拉取 Directus 的官方镜像并启动容器。这个过程视服务器网络情况,大概需要 1~3 分钟。 第五步:放行端口(这点很重要 小白容易忘掉) Directus 默认使用的是 8055 端口,你需要确保这个端口对外开放,否则网页打不开。 在宝塔面板放行:点击宝塔面板左侧菜单的 “安全”,在端口放行那里,填入 8055,备注写“Directus”,然后点击放行。 在腾讯云控制台放行(关键):因为你是腾讯云的服务器,必须登录到腾讯云的网页控制台 -> 找到你的这台轻量服务器/云服务器 -> 找到 “防火墙” (或安全组) -> 添加规则 -> 放行 TCP 8055 端口。 为什么会有两个端口开发? 1、腾讯云安全组放行是对公网开放 2、宝塔放行也是控制外部访问,它是“第二道门”,腾讯云是小区大门,宝塔是你家防盗门,这么理解就行了。 第六步:访问后台 一切就绪后,在浏览器地址栏输入: http://你的服务器IP地址:8055 如果看到界面,使用刚才设置的 ADMIN_EMAIL 和密码登录即可开始建表。 3、Directus登录 进来你应该会看到这个 这是 Directus 的许可证确认页面。 直接点击右边的 "I'm using Core plan"(免费,无需密钥)。 然后保存就行 然后会进入到你的登录界面直接登录即可。

鹿的梦➔
她推了推鼻梁上的眼镜,抬头看我。 那年南京刚开始冷。不是冬天那种冷,是秋风里夹了一点凉意。明孝陵的石象路在这个时候最好看。两旁的枫树像是赶在霜降前把事情办完,一棵树的颜色层层叠叠的,阳光透过来,整条路都是暖的。她走在前面,踩着一地落叶。 养鹿的地方不大,进去一人发一小把干草。那些鹿见惯了人,步子不急不缓,凑过来的时候鼻尖先到,湿湿凉凉的,她本能地把手往后一缩,又慢慢伸出去。后来干脆蹲下来,一只鹿就站在跟前,安安静静吃她掌心里的草。她腾出另一只手,摸它的额头。鹿没动,眼皮慢慢垂下来,像是睡着了。 鹿安安静静地,什么都不懂。不懂什么季节,不懂什么离别,只知道掌心是暖的,草是甜的。 她也是。不只是鹿的事,好多事她都不懂。她不知道我快要毕业了,不知道有些事情是有截止日期的,不知道秋天再长也长不过一场霜。她知道的东西很少。鹿很可爱。 而南京的秋天太短了。那天她蹲在草地上,细细碎碎地喂着鹿,我站在一步之外看着她的背影。她那么轻,轻得像秋天里的一片叶子 我怕自己接不住,又怕接住了之后,风会把它带走。 那年的枫叶落得很快。还没等人看够,树枝就空了。 后来我又过了一个秋天。枫叶照样红,鹿苑照样有人排队买票。 明孝陵我没有再去过。 风到底还是吹了过来。我终究没能挡住那场霜降,只能站在原地,眼看着秋天从枝头上彻底脱落。或许有些人和季节一样,注定只能陪你走完一段短短的石象路。 当风把叶子带走的时候,连同那个来不及拢紧外套的初秋,也就一起安安静静地,停在了一只鹿的梦里。

与附近设备共享文件的宝藏软件-LocalSend➔
LocalSend 是一款在同一个无线网络下传输文件和文字的工具。 只要手机(支持苹果和安卓)和电脑(支持MAC/WIN/LINUX)连接同一个无线网,不用注册账号,就能互相发送照片、视频和各种文件。 它是开源且免费的。 里面没有任何广告。 传输大文件不限制速度。 不需要连接互联网也能传输。 文件只在局域网内部流动,安全保护隐私。 使用方法 确保两台设备连接在同一个无线网络。 在两台设备上分别打开这个软件。 在发送文件的设备上选择好需要传输的照片或文件。 在屏幕下方出现的设备列表中点击接收方的名字。 接收方的设备点击接受即可完成传输。 官方链接: https://github.com/localsend/localsend 下拉到此处选择对应的安装包即可 个人评价满分 非常好用 软件用Flutter写的 跨平台性不用多说吧

无锡惠山➔
才发觉自己拍的都是视频,没拍什么图,那就放几个视频。 这次去天气不热,这个船好像是40一位。 我觉得太值了。 因为在无锡多年居住,只见梅雨纷纷,难见春和景明。 跟一位友人共同前往,友人为人害羞,本人和他就都不出镜了。 比较有意思的是,和他游玩的旅程中大多数时间在交流分享工作中的所见所得,不像是出来玩的(乐)。 主要设备主要是小米17

低配服务器-宝塔面板性能优化步骤➔
此篇文章主要是面向WP站点 宝塔面板性能优化步骤 1. 开启虚拟内存 “Linux工具箱”,第一步直接用它。 · 操作: 打开 Linux工具箱 -> 找到 Swap/虚拟内存。 · 设置: 填入 2048 (即 2GB) 或 4096 (4GB),点击确定。 · 作用: 这是保命底线。当你在后台高频保存复杂页面时,如果 4G 物理内存瞬间吃满,Swap 可以防止数据库和 PHP 崩溃。 2. 解除 MySQL 5.7 的内存限制 MySQL 是重型展示站最容易卡脖子的地方,我们要给它足够的内存去缓存数据。 · 操作: 点击 MySQL 5.7 -> 设置 -> 性能调整。 · 设置: ○ 找到 优化方案,下拉菜单直接选择 2-4GB 这个档位。 ○ 重点检查 innodb_buffer_pool_size 这个参数,它应该被自动调高到了 512M 或 1024M(如果没有,手动改成 512M)。 · 作用: 让绝大多数的产品查询直接在内存中完成,不再慢吞吞地去读写硬盘。 3. PHP优化 PHP 负责把前端的请求动态编译出来 · 第一步(装扩展): 点击 PHP 8.2 -> 设置 -> 安装扩展。务必安装 opcache 和 redis 这两个扩展(不要漏掉 redis,否则装的 Redis 软件就不起作用)。 · 第二步(调并发): 点击 性能调整。 ○ 运行模式:动态模式 (Dynamic) ○ max_children:40(这是给整台服务器设定的最高安全水位,保证即使 5 个站同时活跃,也不会抽干 4G 内存) ○ start_servers:5 ○ min_spare_servers:5 ○ max_spare_servers:10 · 第三步(扩内存): 点击 配置修改。把 memory_limit 从默认的 128M 修改为 256M 。 4. 优化 Nginx 配置(选做,但强烈建议) · 操作: 点击 Nginx 1.26.3 -> 设置 -> 性能调整。 · 设置: 检查 worker_processes,通常可以设置为 2(对应你的 2 核 CPU),或者保持 auto。同时确保 Gzip 压缩 是开启状态(压缩级别 4 左右即可,不用太高以免吃 CPU)。 5. 网站内部设置 服务器底层架构调优完毕后,在每一个部署好的 WordPress 站点内部,必须完成这两个动作,彻底形成闭环: 1. 接通 Redis 内存数据库: 装好Redis后,在每个站点的后台安装 Redis Object Cache 插件并一键开启。让海量的重复查询直接走内存,大幅降低 MySQL 的运算压力。 2. 建立全页静态缓存: 给每个站点安装 WP Rocket(或其他强大的缓存插件),把前台海外访客看产品的流量 100% 挡在 PHP 和数据库之外。 6. Redis 的多站点必要标识 服务器上要放 3-5 个站点,并且每个站点都要开启 Redis Object Cache,必须要修改 wp-config.php,给每个站点分配不同的 Redis 数据库 ID 或前缀。 · 如果不改的后果: 默认情况下,所有站点都会把缓存写进 Redis 的 0 号数据库。结果就是——A 网站的前台可能会莫名其妙显示出 B 网站的产品,整个数据库全乱套。 · 解决办法: 在每一个站点的 wp-config.php 文件中,加入以下两行代码(每个站点的编号必须唯一): 站点 A: · PHP Plain Text define( 'WP_REDIS_DATABASE', 0 ); define( 'WP_CACHE_KEY_SALT', 'siteA_' ); · 站点 B: · PHP Plain Text define( 'WP_REDIS_DATABASE', 1 ); define( 'WP_CACHE_KEY_SALT', 'siteB_' ); · 以此类推,这样 Redis 就会把每个网站的数据严格物理隔离,查询速度极快且互不干扰。 原来的WPconfig.php里可能会有自动生成的 注释掉放上自己的就行 7. WPRoccket严格控制 Heartbeat 1. Behavior in backend (WordPress 后台) · 关掉会怎样? 你的 WordPress 仪表盘将不再“实时更新”。比如 WooCommerce 的实时销售额统计、插件的实时通知提示将停止自动刷新,必须等你手动按 F5 刷新网页才会更新。 · 对服务器的影响: 如果你开着后台挂机,关闭它可以省下大量无意义的 CPU 消耗。 · 建议:选择【Disable completely (完全禁用)】。平时谁也不会一直盯着仪表盘看实时数据,直接禁用,把宝贵的并发进程省下来。 2. Behavior in post editor (文章/页面编辑器) · 关掉会怎样? 这会带来致命后果。Heartbeat 在这里的核心作用是“自动保存 (Autosave)”和“文章锁定(防止两个管理员同时编辑覆盖)”。如果完全关闭,当你在排版一个复杂的页面时,浏览器突然崩溃或断网,你刚才几个小时的努力就会全部白费,因为系统没有自动保存。 · 对服务器的影响: 编辑页面时高频的自动保存确实很吃 CPU。 · 建议:保持【Reduce activity (减少活动)】。也就是你截图里现在的状态。WP Rocket 会把自动保存的频率从默认的每 15 秒强制拉长到每 2 分钟一次。既保住了你的工作心血,又大幅降低了对 PHP 的压力。 3. Behavior in frontend (网站前台 - 访客看到的页面) · 关掉会怎样? 如果你的网站前台安装了实时聊天插件、或者某些极其依赖 AJAX 实时刷新购物车的 WooCommerce 迷你购物车(Mini Cart),完全关闭可能会导致这些功能失效。 · 对服务器的影响: 如果前台有几百个访客,每个人每分钟都在向服务器发一次心跳请求,你的 2核4G 瞬间就会被击穿(出现 502 错误)。 · 建议:选择【Disable completely (完全禁用)】。既然你的前台页面已经有了 WP Rocket 的静态缓存,访客根本不需要和服务器保持这种毫无意义的“心跳连接”。(注:如果禁用后发现 WooCommerce 加入购物车有卡顿,再把它退回到截图里的【Reduce activity】即可)。 8. 邮件超时引发的 PHP-FPM 雪崩 云厂商为了防止服务器被黑客用来发垃圾邮件,默认都是死死封锁 25 端口的。 站点触发了发邮件的动作,WordPress 底层的 PHPMailer 就会傻傻地去请求 25 端口。 因为端口被墙,请求发不出去,它就会一直在那里“等超时”。 40 个 PHP 并发进程,如果同时有 40 个人触发了邮件动作,这 40 个进程就会瞬间全部处于“挂起等待”状态,直接导致 CPU 飙升到 100%,整个网站彻底卡死报 502 Bad Gateway。 1. PHP 底层禁用 直接从 PHP 的配置文件里把 mail 函数阉割掉,让 PHP 彻底丧失发邮件的能力。 1. 打开宝塔面板,找到左侧的 软件商店 -> 点击 PHP-8.2 的 设置。 2. 在左侧菜单找到 【禁用函数】。 3. 在输入框里输入 mail,点击 添加。 4. 点击左侧的 【服务】,点击 重启 PHP。 · 效果: 只要有代码试图调用 PHP 底层的发件系统,会被瞬间拦截并报错,绝不会出现“等待超时”卡死 CPU 的情况。 2. WordPress 业务层拦截 仅仅禁用 PHP 函数,WordPress 有时候还是会走一遍耗时的代码逻辑然后再报错。作为开发者,最干净的做法是在 WordPress 刚要张嘴发邮件的时候,直接把堵回去。 你可以把下面这行代码,加到你当前主题的 functions.php 文件里(或者通过 Code Snippets 插件添加): Plain Text // 彻底拦截 WordPress 发送任何邮件,耗时 0 毫秒 add_filter( 'pre_wp_mail', '__return_false' ); · 效果: 这是 WordPress 官方提供的一个前置钩子(Hook)。只要有任何插件(包括 WooCommerce)试图发邮件,走到这里直接返回 false 结束流程。 注意:如果邮件功能被彻底关掉,登录点击“忘记密码”是收不到重置链接的,账号就彻底成了死档,必须有个发邮件的解决方案。(详细详见SMTP教程)

苏州上方山➔
遇见孔雀开屏了。 天气不热,动物园的老虎也很活跃 运气真好。 小米17&尼康ZFC

Elementor和WP-Cron会导致服务器负载异常➔
问题背景 一台配置为 2 核 4GB 内存的服务器用于部署 WordPress 企业站点,同时运行宝塔面板、PHP-FPM、MySQL 和 Redis。 服务器监控中出现负载 100% 的情况,但 CPU 使用率并不高,内存也没有耗尽。 系统负载显示: 1分钟负载:8.82 5分钟负载:11.54 15分钟负载:5.58 但是 CPU 状态: idle:97% iowait:0.2% 因此需要进一步确认负载来源。 问题一:服务器负载100%,为什么CPU没有占满? 原因 Linux 的 load average 不等同于 CPU 使用率。 服务器负载包含: 正在使用 CPU 的任务 等待 CPU 的任务 等待磁盘或其他资源的任务 因此可能出现: CPU 使用率很低 内存正常 但是 load average 很高 这种情况通常和以下因素有关: PHP 请求阻塞 数据库查询等待 磁盘 I/O 大量并发任务 问题二:如何确认是不是WordPress导致? 方法 查看访问日志: tail -5000 /www/wwwlogs/web-chang.com.log 统计访问接口: awk '{print $7}' log文件 | sort | uniq -c | sort -nr 排查结果发现大量请求集中在: /wp-admin/admin-ajax.php /wp-json/elementor/v1/checklist/user-progress /wp-json/elementor/v1/global-classes /wp-json/rankmath/v1/updateMeta /wp-cron.php admin-ajax.php?action=as_async_request_queue_runner 说明主要压力来自 WordPress 后台接口和插件任务。 问题三:具体是什么组件产生大量请求? 原因分析 主要来源: 1. Elementor Elementor 页面编辑器会持续调用: REST API 页面状态同步 全局样式同步 检查任务 例如: /wp-json/elementor/v1/checklist/user-progress 2. WP-Cron WordPress 默认使用访问触发定时任务。 访问: /wp-cron.php?doing_wp_cron 时会执行: 插件任务 数据同步 清理任务 队列任务 如果站点访问量增加,会产生额外 PHP 请求。 3. Action Scheduler 日志中出现: admin-ajax.php?action=as_async_request_queue_runner 说明 WordPress 异步任务队列正在运行。 常见来源: Elementor WooCommerce Rank Math 表单插件 问题四:如何降低WordPress服务器负载? 解决方案一:关闭WP-Cron自动触发 编辑: wp-config.php 增加: define('DISABLE_WP_CRON', true); 然后使用服务器计划任务执行: wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1 设置每10分钟执行一次。 这样可以避免用户访问触发后台任务。 解决方案二:限制PHP-FPM并发数量 2核4GB服务器不适合开启过多PHP进程。 建议: pm.max_children = 8 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 pm.max_requests = 500 避免大量 PHP 子进程同时占用内存。 解决方案三:开启PHP OPcache 开启: opcache.enable=1 opcache.memory_consumption=128 opcache.max_accelerated_files=10000 减少 PHP 重复编译消耗。 解决方案四:检查Action Scheduler任务 查询数据库: SELECT status, COUNT(*) FROM wp_actionscheduler_actions GROUP BY status; 如果大量任务处于: pending failed 需要清理或者检查对应插件。 问题五:为什么2核4GB服务器容易出现这种情况? 原因 WordPress运行环境通常包含: Nginx PHP-FPM MySQL Redis 宝塔面板 多个插件 2核4GB实际可分配给WordPress的资源有限。 当以下情况同时发生: Elementor编辑页面 WP-Cron执行任务 插件后台同步 数据库查询增加 PHP请求数量增加后,会导致: PHP-FPM进程增加 内存压力增加 Swap使用 系统负载升高 总结 本次服务器负载异常的主要原因: WordPress Elementor后台请求较多。 WP-Cron持续触发任务。 Action Scheduler异步队列执行。 PHP-FPM并发没有针对低配置服务器优化。 最终优化方向: 禁用WP-Cron默认触发。 使用计划任务执行Cron。 限制PHP-FPM进程数量。 开启OPcache。 检查插件产生的后台任务。 对于2核4GB服务器部署WordPress企业站,重点不是增加PHP进程数量,而是控制后台任务和插件请求数量。

纪念宾得K1➔
曾有过一台宾得 K1。 那会兜里比脸还干净,房租饭钱通勤,每一项都在提醒你有些东西留不住。出掉它时没敢多看,像送走一匹养不起的马。 现在手里是尼康 ZFC,轻巧利落,挂脖子上走一天也不累。它是称职的工具,可靠的伙伴。但宾得是需要盘的,重得让每一次快门都像个决定,机身敦厚扎实,不拍照时也忍不住拿出来擦一擦,转一转光圈环。 最放不下的是颜色。宾得的色彩有脾气,偏暖偏厚,像泡在琥珀里的黄昏。 那种绿是雨后山里蒸腾起来的样子。直出的片子带着胶片时代的体温,不用修,修反而是唐突。 ZFC 给的是自由,K1 给的是仪式。一个让你走得更远,一个让你走得更慢。 有些相机你拥有过就够了。它会一直活在你对光线和颜色的偏执里,机身不在了,那种看世界的方式还在。

USBCAN与SocketCAN技术对比及架构迁移实践➔
1、区别 USBCAN(虚拟串口方案):设备在Android应用层被识别为标准USB CDC设备,通过读写串口字符串(如SLCAN协议)实现CAN帧收发。 优势:无需修改Android底层镜像或Linux内核;即插即用;跨平台前端框架(如React Native)可通过成熟的USB串口库快速完成硬件接入。、 SocketCAN(内核网络方案):将CAN总线集成到Linux内核网络栈,硬件作为标准网络设备(如can0)挂载于系统底层。 优势:支持多进程并发读写;系统内核级调度,通信延迟极低;完全脱离对单一USB节点的物理依赖,稳定性达到车规级工业标准。 2、UsbCan的局限性 项目初期的核心诉求是在未经定制的纯净版Android开发板上,快速验证离线语音控制车身硬件的可行性。所以采用了USBCAN方案。 但是随着业务架构的演进,系统需要引入后台守护进程以实现“底层信号驱动上层UI”(例如:后台静默监听倒车CAN报文,收到信号后瞬间拉起环视界面),发现了很多问题。 硬件独占与多进程冲突:Android系统的USB端口同一时刻仅允许单个进程占用。如果后台服务占用了USB-CAN模块监听倒车信号,前台语音控制App将无法下发控制指令。SocketCAN基于网络套接字,天然支持任意数量的进程同时订阅和发送CAN报文。 物理节点枚举竞态条件:在复杂的真实环境中,其他USB设备(如鼠标、U盘)的热插拔会引发底层USB设备数组序列(VID/PID枚举)的重新洗牌,导致应用层频繁错认设备或引发崩溃。SocketCAN的can0节点由系统内核静态声明绑定,完全免疫外部USB外设的拔插干扰。 契合Android原生架构:为实现零延迟的界面图层切换,前端框架需由React Native整体迁移至Kotlin及Jetpack Compose。配合原生Android开发体系,使用SocketCAN更能发挥系统级Service保活与硬件协同处理的性能优势,是走向标准化车机架构的必然选择。 这三点都是非常关键的技术问题,无法避免。

烟台行➔
主要的设备是小米17和尼康ZFC,这里就只放拍的照片了,放视频站点流量撑不住(曾几何时我也许愿过我自己搞台本地服务器),因为都是4K 8K的高码率视频。 去的时候是工作日,又正值淡季,很难想象天天都是200的海景房,玩了三天,从烟台山开始沿着海边走,一直走到养马岛结束。 大海啊你全是水~ 下次打算故地重游青岛。

南京旧忆➔

PC本地压缩视频的免费软件-HandBrak➔
HandBrake 是一款专门用来给视频转换格式和压缩体积的电脑软件。 它是完全免费的。 里面没有任何广告。 压缩视频时不需要连接互联网。 可以一次性把多个视频排队进行转换。 软件完全在本地电脑运行,不会泄露视频隐私。 使用方法 打开软件并把需要转换的视频直接拖进软件窗口。 在预设菜单里选择适合的清晰度,通常选择默认设置即可。 在软件底部点击浏览按钮,选择新视频要保存的位置。 点击软件顶部正上方的开始编码按钮。 等待底部的进度条走完,就可以在对应的文件夹里找到新视频。 官方链接: https://github.com/HandBrake/HandBrake/releases 用之前需要一点时间研究一下视频格式 个人评价满分

PC本地批量给图片瘦身的免费软件 Caesium➔
Caesium Image Compressor 是一款专门用来给图片瘦身的电脑软件。 它可以把体积很大的图片变小,同时尽量保持原来的清晰度,非常适合用来节省存储空间。 它是完全开源且免费的。 里面没有任何广告。 压缩图片时不需要连接互联网。 可以一次性把几百张图片一起变小。 软件完全在本地电脑运行,不会泄露图片隐私。 使用方法 打开软件并把需要变小的图片直接拖进软件窗口。 在软件底部设置好想要保存新图片的位置。 调整图片质量参数,通常保持默认的百分之八十即可。 点击软件右下角的压缩按钮。 等待进度条走完,就可以在对应的文件夹里找到变小后的图片。 下载页面链接: https://github.com/Lymphatus/caesium-image-compressor/releases 个人评价满分 非常好用

桔梗➔
帮朋友拍完订婚宴,散场已是入夜。 一个人站在路边等车。一辆车刚好从面前驶过,车窗上滑过一片流动的光,恰好倒映出身后亮着的模样。 回过头,是一间还没打烊的花店。 隔着落地玻璃,在满屋子拥挤的色彩里,一眼就看到了那个熟悉的轮廓。 店员问买什么。我说,桔梗。 她抽出一束未开的花苞,用纸裹好递过来。花柄还是那样光滑。 第一次认识桔梗,也是这样的。那时有人把它塞进手里,白色的花苞鼓胀着,像装满心事的锦囊。笨拙地握着,花茎上的水珠沾湿了手指。那是人生中收到的第一束花。那个下午,阳光从窗帘缝隙漏进来,照在花苞上,花瓣紧紧闭合,像藏着什么秘密不肯说。 后来那束花开了。每天早上醒来,都先去看它。花开到最好的时候,那白色是干干净净的白,安安静静的,像月光照在瓷碗边缘那一圈淡淡的光。它开它的,不打扰任何人。 花谢那天,把枯萎的花瓣收进了一个小盒子里。那时候想,有些东西大概会永远留着的。可日子过着过着,那盒子也不知道收到哪里去了。只有桔梗留了下来。从那以后,经过花店总会多看它一眼。有时候买一束回家,插在玻璃瓶里,看它慢慢开,慢慢谢。 其实不是习惯。是这花已经长在了审美里。就像一个人在一个地方住久了,口音会变,口味会变,连看人的眼光也会变。那束白桔梗,就是带来改变的源头。 听说,桔梗的花语是“永恒的爱”。 花开了会谢,人来了会走,哪有什么永恒。 那年那束桔梗枯萎的时候,花瓣边缘先卷起来,白色渐渐发黄,再变成枯褐,轻轻一碰就碎了一手。可后来慢慢发现,有些东西确实留下来了。比如开始喜欢素净的花;比如对白桔梗有了一种近乎本能的亲近;比如每当看见花店里新到的白桔梗,总会想起某个下午,有人把一束花塞进手里,那样自然而然,好像这世上的花,本该就是这样送的。 这些细小的改变,像桔梗的花籽,落在生活的土壤里,生根,发芽,长成了生命的一部分。 把新买的白桔梗放进装了水的玻璃瓶里。花苞还闭着,不知道什么时候会开。但心里知道,它总会开的。 还是喜欢白桔梗。不关花语什么事,它活在眼睛和手指上。它在一个下午被递过来,从那以后,那抹微凉的白便生了根,成了生命里再也无法剥离的底色。

从零搭建一个低成本 Fabric Minecraft 私人服务器➔
前言 本文主要记录一次基于 Debian 服务器搭建 Minecraft Fabric 服务器会遇到哪些错误,包括服务器环境配置、Fabric 模组兼容处理、客户端连接、性能优化、崩溃排查。 服务器目标并不是大型多人商业服,而是提供 3~5 人的小型生存服务器环境,同时兼顾: Fabric 轻量化模组支持 较低服务器成本 稳定运行 支持客户端优化模组 尽量减少朋友安装配置成本 整个过程遇到了 Fabric Loader 版本冲突、Java 版本问题、客户端模组同步、区块加载速度、Watchdog 崩溃等问题。 一、服务器基础环境 1.1 硬件配置 最终服务器配置: 项目 配置 CPU 2 核 内存 8GB 系统 Debian 12 Bookworm Java Temurin JDK 21 Minecraft版本 1.20.1 服务端 Fabric Server 网络 6M 带宽 玩家数量 3~5人 最初服务器为: 2核4GB内存 Java 17 Fabric 1.20.1 后续升级到 8GB 内存主要考虑: 更多模组空间 减少区块生成时内存压力 提高多人同时在线稳定性 二、系统环境配置 2.1 Debian 12 选择原因 服务器系统选择 Debian 12,而不是 Ubuntu。 主要考虑: 系统资源占用更低 后台服务更少 稳定性较高 更适合作为长期运行服务器 基础环境: Debian 12 (追求稳定) Java 21 screen Fabric Server 别忘了开放端口 三、Fabric Server 安装过程 3.1 初始 Fabric Loader 问题 最初启动服务器出现: Incompatible mods found! 错误: Fabric API 0.92.8+1.20.1 requires Fabric Loader >=0.16.10 ModernFix requires Fabric Loader >=0.16.10 原因: 服务器使用: fabric-loader-0.15.11 但是安装的优化模组要求: Fabric Loader >=0.16.10 解决: 重新下载 Fabric Server: wget -O fabric-server.jar \ "https://meta.fabricmc.net/v2/versions/loader/1.20.1/0.16.10/1.0.1/server/jar" 升级 Fabric Loader 到: 0.16.10 四、Java版本兼容问题 4.1 Java 17导致启动失败 升级 Fabric 后出现: UnsupportedClassVersionError class file version 65.0 错误含义: class version 65 = Java 21 class version 61 = Java 17 说明: 部分组件已经使用 Java 21 编译,但是服务器仍然使用 Java 17。 4.2 安装 Java 21 Debian 软件源没有: apt install openjdk-21-jdk 因此采用 Temurin JDK: 安装目录: /opt/java/jdk-21.0.11+10 配置: update-alternatives \ --install /usr/bin/java java \ /opt/java/jdk-21.0.11+10/bin/java 2100 确认: java -version 服务器最终运行环境: Java 21 五、服务器启动参数优化 最终启动: java \ -Xms2800M \ -Xmx2800M \ -XX:+UseZGC \ -XX:+ZGenerational \ -XX:+AlwaysPreTouch \ -XX:+DisableExplicitGC \ -XX:+PerfDisableSharedMem \ -jar fabric-server.jar nogui 参数说明: ZGC 用于降低 GC 停顿: -XX:+UseZGC 适合: 多模组 长时间运行 大地图 固定内存 -Xms2800M -Xmx2800M 避免运行过程中动态扩容造成卡顿。 六、当前服务器安装模组 服务端模组 当前主要: 模组 作用 Fabric API Fabric基础接口 Lithium 游戏逻辑优化 ModernFix 内存优化、启动优化 FerriteCore 降低内存占用 Krypton 网络优化 Right Click Harvest 右键收割 EasyAuth 登录认证 七、客户端模组 客户端额外安装: 性能优化 模组 作用 Sodium 大幅提升FPS Iris 光影支持 Indium Sodium兼容 Entity Culling 减少不可见实体渲染 Dynamic Lights 动态光源 超视距 尝试: Distant Horizons 用于: LOD远景渲染 查看超远距离地形 但是发现: 服务器没有预生成地图时: 远处显示空白 出现断崖 小地图区域黑色 原因: Distant Horizons只能渲染客户端已有数据。 它不能凭空生成服务器没有发送过的区块。 八、区块加载问题分析 8.1 会看到断崖 Minecraft区块加载流程: 玩家移动 ↓ 客户端请求区块 ↓ 服务器生成/读取区块 ↓ 发送给客户端 ↓ 客户端显示 因此: 没有探索过的区域: 服务器不存在区块数据。 Distant Horizons无法显示真实地形。 8.2 6M带宽是否够? 对于3~5人: 6M带宽基本够。 区块加载慢主要不是带宽问题,而是: CPU生成区块 磁盘读取 Minecraft单线程限制 九、服务器崩溃排查 9.1 Watchdog问题 之前服务器出现崩溃。 主要原因: Minecraft Watchdog检测到: 主线程长时间无法响应 触发: 强制关闭服务器 常见原因: 大量新区块生成 玩家快速探索 模组占用主线程 GC停顿 十、服务器性能测试 使用: /spark tps 测试。 最终结果: TPS: 20.0 20.0 20.0 20.0 20.0 说明: 服务器保持满TPS。 Tick: 5.6 / 6.8 / 10.7 / 20.7 ms Minecraft标准: 50ms = 极限 当前最高: 20.7ms 属于正常范围。 CPU: system 7% process 6% 说明: CPU压力很低。 十一、当前服务器状态总结 当前环境: Minecraft 1.20.1 Fabric Loader 0.16.10 Java 21 2核8GB Fabric优化模组 Sodium客户端优化 Iris光影 Distant Horizons远景 性能: TPS: 20 MSPT: <30ms CPU: 低占用 内存: 充足 当前瓶颈: 不是服务器性能。 主要问题: 世界没有提前生成 Distant Horizons没有足够LOD数据 Minecraft默认区块生成机制限制 十二、后续优化方向 12.1 使用 Chunky 预生成地图 推荐: Chunky 提前生成: 例如: 出生点半径 512区块 流程: 无人在线 ↓ 服务器生成地图 ↓ 保存region文件 ↓ 玩家进入直接读取 可以明显改善: 探索卡顿 地形断裂 DH远景显示 12.2 添加玩法模组 由于客户端已经分发,不建议大量增加必须客户端安装的模组。 优先选择: 服务端可运行: 经济系统 任务系统 管理工具 世界优化 减少朋友维护成本。 总结 这次 Fabric Minecraft 服务器搭建过程主要解决了几个核心问题: Fabric Loader 与模组版本匹配 Java 17 与 Java 21兼容问题 客户端服务端模组同步 Watchdog崩溃排查 区块加载与远景显示问题 性能测试与优化 最终服务器已经达到稳定运行状态。 对于3~5人的生存服务器,目前配置已经足够,下一步优化重点应该从“提升硬件”转向: 地图预生成 模组选择 世界管理 长时间稳定运行优化 这类小型 Fabric 服务器的主要限制通常不是内存,而是 Minecraft 主线程处理能力和世界生成压力。

WordPress-添加侧边媒体栏➔
其实很简单 做一个全局悬浮块就行 代码在最下面 105-110行内容改成客户的联系方式 219行改图标是的形状的 圆形50% 正方形0% 圆角8%-16% <!-- 引入字体图标库 --> <link rel="stylesheet" href="https://at.alicdn.com/t/c/font_4210690_bfpz45tuycu.css"> <!-- 侧边栏:用于展示联系信息及快捷操作 --> <aside class="contact-sidebar"> <!-- 侧边栏隐藏/显示控制按钮 --> <div class="contact-sidebar-hide icon-box" onclick="toggleSidebar()"> <i id="hidetoright" class="icon iconfont icon-arrowrighttoright"></i> </div> <!-- 手机号联系入口(WhatsApp) --> <div class="contact-sidebar-phone icon-box"> <a class="a-open" href="" target="_blank" rel="noopener" id="whatsappLink"> <i class="icon iconfont icon-whatsapp"></i> </a> <div class="phone"> <span class="show-phone" id="phoneDisplay"></span> </div> </div> <!-- 邮箱联系入口 --> <div class="contact-sidebar-email icon-box"> <a class="a-open" href="" id="emailLink"> <i class="icon iconfont icon-mail"></i> </a> <div class="email"> <span class="show-email" id="emailDisplay"></span> </div> </div> <!-- 二维码展示(暂未启用) <div class="contact-sidebar-code icon-box"> <a class="a-open" href="#" target="_blank" rel="noopener"> <i class="icon iconfont icon-weixin"></i> </a> <div class="code"> <img class="show-code" src="../../images/code.png" alt="微信二维码"> </div> </div> --> <!-- 回到顶部按钮(暂未启用) <div class="contact-sidebar-totop icon-box" onclick="toTop()"> <i class="icon fa fa-light fa-arrow-up fa-lg"></i> </div> --> </aside> <!-- 底部栏:移动端联系信息展示 --> <footer class="footerbar" id="footerbar"> <!-- 底部栏隐藏/显示控制按钮 --> <div class="foot-hide foot-box" onclick="toggleFooter()"> <i id="hidetoleft" class="icon iconfont icon-arrowlefttoleft"></i> </div> <!-- 底部栏手机号入口 --> <div class="foot-phone foot-box"> <a class="a-open" href="" target="_blank" rel="noopener" id="footerWhatsappLink"> <i class="icon iconfont icon-whatsapp"></i> </a> <div class="show-footer_phone"> <span class="show-phone" id="footerPhoneDisplay"></span> </div> </div> <!-- 底部栏邮箱入口 --> <div class="foot-email foot-box"> <a class="a-open" href="" target="_blank" rel="noopener" id="footerEmailLink"> <i class="icon iconfont icon-mail"></i> </a> <div class="show-footer_email"> <span class="show-email" id="footerEmailDisplay"></span> </div> </div> <!-- 底部栏二维码(暂未启用) <div class="foot-code foot-box"> <i class="icon iconfont icon-erweima"></i> <div class="show-footer_code"> <img src="../../images/code.png" alt="联系客服二维码"> </div> </div> --> <!-- 底部栏回到顶部(暂未启用) <div class="foot-totop foot-box" onclick="toTop()"> <i class="icon-foot fa fa-light fa-arrow-up fa-lg "></i> </div> --> </footer> <script> // 配置信息 - 在此处修改联系方式,所有引用处会自动更新 const contactConfig = { phone: '8615862618383', // 电话号码(用于链接) phoneDisplay: '+86 15862618383', // 显示的电话号码 email: 'andrew@szosr.com' // 邮箱地址 }; // 初始化联系方式 function initContactInfo() { // 更新侧边栏联系方式 document.getElementById('whatsappLink').href = `https://api.whatsapp.com/send?phone=${contactConfig.phone}`; document.getElementById('phoneDisplay').textContent = contactConfig.phoneDisplay; document.getElementById('emailLink').href = `mailto:${contactConfig.email}`; document.getElementById('emailDisplay').textContent = contactConfig.email; // 更新底部栏联系方式 document.getElementById('footerWhatsappLink').href = `https://api.whatsapp.com/send?phone=${contactConfig.phone}`; document.getElementById('footerPhoneDisplay').textContent = contactConfig.phoneDisplay; document.getElementById('footerEmailLink').href = `mailto:${contactConfig.email}`; document.getElementById('footerEmailDisplay').textContent = contactConfig.email; } // 页面加载完成后初始化联系方式 window.addEventListener('DOMContentLoaded', initContactInfo); // 侧边栏切换状态计数器(用于控制图标旋转角度) let sidebarToggleCount = 0; // 底部栏切换状态计数器 let footerToggleCount = 0; /** * 切换侧边栏显示/隐藏状态 * 1. 控制切换按钮的旋转动画 * 2. 控制联系信息的平移显示/隐藏 */ function toggleSidebar() { // 获取DOM元素 const toggleIcon = document.getElementById('hidetoright'); const whatsappItem = document.querySelector('.contact-sidebar-phone'); const emailItem = document.querySelector('.contact-sidebar-email'); const codeItem = document.querySelector('.contact-sidebar-code'); // 更新旋转角度(每次点击旋转180度) sidebarToggleCount += 180; toggleIcon.style.transform = `translate(-50%, -50%) rotate(${sidebarToggleCount}deg)`; // 切换显示/隐藏状态 const isHidden = whatsappItem.style.transform === 'translateX(100px)'; if (isHidden) { // 显示元素 whatsappItem.style.transform = 'translateX(0)'; emailItem.style.transform = 'translateX(0)'; if (codeItem) codeItem.style.transform = 'translateX(0)'; } else { // 隐藏元素 whatsappItem.style.transform = 'translateX(100px)'; emailItem.style.transform = 'translateX(100px)'; if (codeItem) codeItem.style.transform = 'translateX(100px)'; } } /** * 切换底部栏显示/隐藏状态 * 1. 控制切换按钮的旋转动画 * 2. 控制联系信息的平移显示/隐藏 */ function toggleFooter() { // 获取DOM元素 const toggleIcon = document.getElementById('hidetoleft'); const whatsappItem = document.querySelector('.foot-phone'); const emailItem = document.querySelector('.foot-email'); const codeItem = document.querySelector('.foot-code'); // 更新旋转角度(每次点击旋转180度) footerToggleCount += 180; toggleIcon.style.transform = `translate(-50%, -50%) rotate(${footerToggleCount}deg)`; // 切换显示/隐藏状态 const isHidden = whatsappItem.style.transform === 'translateX(-1000px)'; if (isHidden) { // 显示元素 whatsappItem.style.transform = 'translateX(0)'; emailItem.style.transform = 'translateX(0)'; if (codeItem) codeItem.style.transform = 'translateX(0)'; } else { // 隐藏元素 whatsappItem.style.transform = 'translateX(-1000px)'; emailItem.style.transform = 'translateX(-1000px)'; if (codeItem) codeItem.style.transform = 'translateX(-1000px)'; } } /** * 回到页面顶部功能 * 兼容不同浏览器的滚动高度获取方式 */ function toTop() { // 现代浏览器 if (window.scrollTo) { window.scrollTo(0, 0); } else { // 兼容旧浏览器 document.body.scrollTop = 0; document.documentElement.scrollTop = 0; } } </script> <style> /* 图标容器基础样式 */ .icon-box { width: 50px; height: 50px; /* 圆形边框颜色 (当前注释掉) */ /* border: 2px solid #c9c9c9; */ border-radius: 50%; position: relative; /* 圆形框内背景色 */ background-color: rgba(255, 255, 255, 0.25); margin-bottom: 5px; } /* 图标通用样式 */ .icon { /* 侧边图标颜色 */ color: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); transform-origin: 50% 50%; } /* 特定图标样式调整 */ .icon-arrowrighttoright { font-size: 30px !important; } .icon-mail { font-size: 20px !important; top: 47%; /* 微调邮件图标垂直位置 */ } .icon-whatsapp { font-size: 20px !important; } .icon-weixin { font-size: 30px; } /* 侧边栏元素颜色定义 */ .contact-sidebar-hide { /* hide侧边按钮背景颜色 */ background-color: #1C5E9A; } .contact-sidebar-phone { /* 电话图标背景颜色 */ background-color: #1C5E9A; } .contact-sidebar-email { /* 邮箱图标背景颜色 */ background-color: #1C5E9A; } .contact-sidebar-code { /* 二维码图标背景颜色 */ background-color: #1C5E9A; } /* 底部栏元素样式 */ .foot-hide { /* 底部隐藏按钮背景颜色 */ background-color: #1C5E9A; width: 60px !important; height: 50px; /* 明确高度,确保图标显示完整 */ z-index: 9999; /* 确保在其他元素上方显示 */ } .foot-phone, .foot-email, .foot-code { background-color: #1C5E9A; transition: all 1s ease; /* 平滑过渡动画 */ } /* 底部栏容器样式 */ .foot-box { /* 底部边框颜色 (当前注释掉) */ /* border: 2px solid #fff; */ margin: 0px -1px 0px 0px; width: 50%; cursor: pointer; /* 鼠标悬停显示手型 */ position: relative; } /* 底部栏元素边框调整 */ .foot-box:nth-child(2), .foot-box:nth-child(3), .foot-box:nth-child(4) { border-left: 0px; } /* 底部栏整体样式 */ .footerbar { z-index: 9999; /* 确保在页面内容上方显示 */ position: fixed; left: 0; bottom: 0; width: 100%; height: 50px; text-align: center; display: flex; justify-content: space-around; transition: all 0.3s linear; /* 平滑过渡效果 */ } /* 底部栏图标样式 */ .icon-foot { /* 底部图标颜色 */ color: rgb(86, 86, 243); position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } /* 弹出信息样式 */ .code > .show-code, .email > .show-email, .phone > .show-phone { /* 侧边栏弹出文本大小 */ font-size: 18px; white-space: nowrap; /* 防止文本换行 */ padding: 0px 20px; } /* 侧边栏整体样式 */ .contact-sidebar { position: fixed; z-index: 9999; /* 确保在页面内容上方显示 */ right: 0; bottom: 15%; padding: 6px 6px 0; border-radius: 20px 0 0 20px; /* 右侧圆角 */ } /* 侧边栏元素动画效果 */ .contact-sidebar-phone { transition: all 1.2s; /* 电话图标的过渡时间 */ } .contact-sidebar-email { transition: all 0.9s; /* 邮箱图标的过渡时间 */ } .contact-sidebar-code { transition: all 0.6s; /* 二维码图标的过渡时间 */ } .icon-arrowlefttoleft { font-size: 30px !important; /* 增大底部箭头大小,与侧边栏箭头保持一致 */ } .icon-arrowlefttoleft, .icon-arrowrighttoright { transition: all 0.3s; /* 箭头图标的过渡时间 */ } /* 侧边栏内部元素样式 */ .contact-sidebar > div { cursor: pointer; /* 鼠标悬停显示手型 */ text-align: center; } .contact-sidebar > div h3 { font-size: 15px; font-weight: 500; color: #fff; } /* 侧边栏弹出框样式 */ .contact-sidebar > div .code, .contact-sidebar > div .email, .contact-sidebar > div .phone, .contact-sidebar > div .hide { padding: 6px; align-items: center; display: none; /* 默认隐藏 */ position: absolute; z-index: 9999; /* 确保在图标上方显示 */ top: 15%; right: 70px; /* 显示在图标左侧 */ border-radius: 8px; box-shadow: 8px 8px 8px 8px rgba(88, 95, 110, 0.22); /* 阴影效果 */ background-color: #f7f7f7; } /* 底部栏弹出框样式 */ .footerbar > div .show-footer_code, .footerbar > div .show-footer_email, .footerbar > div .show-footer_phone { padding: 6px; align-items: center; display: none; /* 默认隐藏 */ position: absolute; z-index: 9999; /* 确保在图标上方显示 */ bottom: 110%; /* 显示在图标上方 */ font-size: 10px; border-radius: 8px; box-shadow: 8px 8px 8px 8px rgba(88, 95, 110, 0.22); /* 阴影效果 */ background-color: #f7f7f7; } /* 侧边栏弹出框箭头样式 */ .contact-sidebar > div .email:after, .contact-sidebar > div .phone:after, .contact-sidebar > div .code:after { position: absolute; top: 50%; left: 100%; /* 箭头显示在右侧 */ content: ''; transform: translateY(-50%); border-width: 5px; border-style: solid; border-color: transparent transparent transparent #ffffff; /* 三角形箭头 */ } /* 悬停显示弹出框 */ .contact-sidebar-email:hover .email, .contact-sidebar-phone:hover .phone, .contact-sidebar-code:hover .code, .foot-email:hover .show-footer_email, .foot-phone:hover .show-footer_phone, .foot-code:hover .show-footer_code { display: block; } /* 二维码图片样式 */ .show-footer_code > img, .code > img { width: 80px !important; } /* 电话弹出框宽度 */ .phone { width: 200px; } /* 链接样式 */ .a-lg { width: 100px; height: 100px; } .a-open { display: block; height: 50px; border-radius: 50px; } /* 响应式显示控制 */ @media (min-width: 770px) { /* 大屏幕隐藏底部栏 */ .footerbar { display: none; } } @media (max-width: 770px) { /* 小屏幕隐藏侧边栏 */ .contact-sidebar { display: none; } } </style>

长条形的日光灯➔
我推开那扇门的时候是春天。 工作室的灯光很普通,就是那种长条形的日光灯,冷白色的,一点也不文艺。带队的是两位女老师,周老师和石老师。她们懂得很深,不论遇到多棘手的问题,脾气都极好。 这就是好老师的样子吧。不靠音量,不靠威严,靠的是她们真正懂你在做什么。 工作室的日子很纯粹。一张桌子,一台电脑,一杯水,从上午坐到晚上十点宿舍关门。我们在那里学Spring Boot的自动装配,学Vue的响应式原理,学MySQL的索引优化。那些知识现在回头看,很多已经过时了,技术栈更新换代比翻书还快。但庆幸的是,自己是在AI时代来临前学会了这些。 也是在那里,认识了她。 她比我来得早,第一次打交道,语气不太客气,还以为是学姐,过些天才知道是同年级同专业2班的。她说话快,走路快。 后来项目做久了,我们会一起去天台上吹风聊天,在那里经过分明的时光。 后来我们在一起了。 再后来,分开了。 关于分开的原因,不想说太多。大学里的感情,往往不是败给了什么轰轰烈烈的冲突,而是两个人都太年轻,不知道该怎么把一段关系折叠进各自尚未成形的人生里。 技术可以debug,感情有时候不行。有些bug是你知道问题在哪但不知道怎么改,改了一处又崩了另一处,最后只能回滚。 但从来没有后悔过。 因为在工作室认识的不只是她,而是那些让我变成如今模样的人。 春去秋来的日子里,时光过得飞快。大家只是习惯了每天推开门去那里,拉开椅子坐下,安静地对着屏幕把手头的事情做完。 即便日光灯不文艺,即便有的代码终将被淘汰,但那张桌子、那个天台和那些陪伴,实实在在地接住过我们。 后来,在夜里盯着屏幕时,还是会想起那扇门。 那些回忆,跟日光灯一样一直在脑海里亮着。

南京玄武湖荷花➔
拍摄用的宾得KS2+FA100-300 这镜头太性价比了,才200块,就是那吱吱的螺丝刀对焦太吵了。 去的时候荷花绽放处于尾期了,还好玄武湖够大,留有很多晚开的花。

