[{"data":1,"prerenderedAt":1796},["ShallowReactive",2],{"\u002F2026-07-05":3,"\u002F2026-07-05-rel":306},{"id":4,"title":5,"body":6,"column":290,"date":291,"description":12,"extension":292,"hero_image":293,"meta":294,"navigation":295,"path":296,"seo":297,"series_id":293,"severity":293,"stem":298,"summary":299,"tags":300,"__hash__":305},"posts\u002F2026-07-05-克制的网页动效.md","这个动效，是在帮用户还是在展示我？",{"type":7,"value":8,"toc":273},"minimark",[9,13,17,22,25,28,31,39,43,46,49,52,55,61,65,68,74,77,84,87,91,98,101,109,116,120,127,134,149,152,155,158,164,170,176,179,189,198,213,216,219,222,225,228,231,238,241,244,257,263,270],[10,11,12],"p",{},"前端做了一轮动效设计，反复遇到同一个问题：这个动效是在帮助用户理解界面，还是只在展示\"我会做动效\"。区分的标准不是感觉，是三条具体的判据。",[14,15,16],"h2",{"id":16},"动效的三条判据",[18,19,21],"h3",{"id":20},"判据一是否传达了状态变化","判据一：是否传达了状态变化",[10,23,24],{},"用户操作后，系统进入新的状态。动效的作用是让这个状态变化可感知。",[10,26,27],{},"加载中。一个网络请求发出去了，但数据还没回来，用户看不到任何反馈会以为卡了。打字指示（三个点循环脉冲、错开时间）就是这类必做的动效。用户不需要理解\"这是一个脉冲\"，只需要看到\"系统在做什么\"。",[10,29,30],{},"成功和失败。表单提交后，服务器返回 200 还是 400，用户需要知道。Toast 从屏幕边滑入、带上颜色标记（绿色还是红色）和文案，用户秒懂结果。这里动效的信息密度很高：位置（从边界滑入说明是一个通知）、方向、颜色都在传达信息。",[10,32,33,34,38],{},"这一类动效 ",[35,36,37],"strong",{},"该做","。不做的代价是用户体验割裂——界面突然变了，用户需要花脑力去理解\"发生了什么\"。",[18,40,42],{"id":41},"判据二是否建立了空间关系","判据二：是否建立了空间关系",[10,44,45],{},"用户在多个界面间切换，每个界面有不同的内容。动效的作用是让用户在心理上形成\"我从 A 地来到了 B 地\"的感觉。",[10,47,48],{},"页面切换。新页面从右侧滑入或从中央缩放进来，老页面退出。用户的注意力自然跟随移动，不会觉得\"屏幕闪了一下\"；反而能通过位移方向推断页面的层级关系。",[10,50,51],{},"弹窗。一个模态框从中央向外弹出、带回弹感，用户知道\"这个内容是浮在上面的、暂时的\"。而如果弹窗突然出现，用户需要花力气解析\"这是什么、它和后面的页面什么关系\"。",[10,53,54],{},"展开与收起。列表条目逐个滑入，后出现的条目在前面条目之后，这叫 stagger 编排。用户可以跟踪每一项的出现，而不是\"列表突然全填满了，我从哪里开始看\"。导航栏的 active 指示器平移而不是闪烁切换，用户能看到\"我从这里跳到那里\"。",[10,56,57,58,60],{},"这一类动效也 ",[35,59,37],{},"。代价同样高：没有这些线索，界面就像一个无生命的状态表格，用户要主动扫描才能判断当前在哪、怎么走。",[18,62,64],{"id":63},"判据三装饰性动效","判据三：装饰性动效",[10,66,67],{},"一个元素 hover 时发光、卡片有光扫效果、logo 漂浮、按钮涟漪……这些都是装饰。它们不传达任何状态变化，不建立空间关系，就是\"看起来更高级\"。",[10,69,70,71],{},"品牌要求这些吗？可能。会提升用户体验吗？证据不足。",[35,72,73],{},"该砍。",[10,75,76],{},"这里的逻辑是：每一帧动画都要用户的 GPU 和浏览器去渲染。装饰性动效的每一帧都是在\"花钱不赚钱\"。当装饰堆积到一定程度，就会出现性能问题。",[10,78,79,80,83],{},"实践中的折衷是：装饰性动效只保留在",[35,81,82],{},"关键时刻","。用户花钱了、获得了成功的反馈，这时撒纸屑。余额减少了，扣点粒子迸发。这些装饰是在庆祝或强调，而不是无处不在。",[14,85,86],{"id":86},"性能与可访问性的硬要求",[18,88,90],{"id":89},"_60fps-底线","60fps 底线",[10,92,93,94,97],{},"动效要稳定在 60fps，否则就是卡顿伪装成流畅。这意味着",[35,95,96],{},"只动 transform、opacity 和 filter，不动 layout 属性","。",[10,99,100],{},"为什么？改变 width、height、margin 或 left\u002Ftop 会触发重排（reflow）。浏览器要重算文档流、重排页面，这会花很长时间，远超单帧预算。动画自动掉帧。",[10,102,103,104,108],{},"实例：一个列表容器用 height 从隐藏到展开的动画，看起来像\"列表展开\"，实际上每帧都在触发重排。改成用 ",[105,106,107],"code",{},"scaleY"," transform 动画，性能立刻改善。",[10,110,111,112,115],{},"另一个细节：长列表的逐项入场动画。当列表项数足够多时，用 stagger 从上往下依次滑入，屏幕可见范围内的项动画效果还不错，但超出视口的项同样在运行动画逻辑——整个列表的动画状态都要参与计算，对内存和渲染管线造成压力。硬要求是：",[35,113,114],{},"大列表只对首屏可见项做动画","，不可见项直接渲染到最终状态。",[18,117,119],{"id":118},"prefers-reduced-motion必须尊重用户的选择","prefers-reduced-motion：必须尊重用户的选择",[10,121,122,123,126],{},"系统级的「减少动态效果」开关（macOS 辅助功能、Windows 显示设置）对应 CSS 媒体查询 ",[105,124,125],{},"prefers-reduced-motion: reduce","。一部分用户打开这个设置（通常因为前庭功能障碍、晕动症或认知障碍），动效会让他们头晕或注意力分散。",[10,128,129,130,133],{},"硬要求：",[35,131,132],{},"当系统选择 reduced-motion 时，所有动效必须降级","。降级方案是什么？最简单且有效的方案是淡入淡出。用户看不到位移、没有高频闪烁，界面从无到有、从有到无。装饰性的粒子、纸屑、光扫一律不渲染。",[10,135,136,137,140,141,144,145,148],{},"实现方式是在应用根部用一个全局的 ",[105,138,139],{},"MotionConfig"," 接管 ",[105,142,143],{},"reducedMotion=\"user\"","。React 的 ",[105,146,147],{},"useReducedMotion()"," hook 会读系统偏好。检查返回值，该短路的短路，该降速的降速。",[10,150,151],{},"没有这一步，上线的产品会有一小部分用户会因为你的动效而无法使用你的产品。",[14,153,154],{"id":154},"参数选择的逻辑",[10,156,157],{},"时长有三个档位。",[10,159,160,163],{},[35,161,162],{},"Micro（0.17 秒 \u002F 170ms）","：按钮 hover 抬升、开关滑动、输入框 focus 发光。这些是每秒可能发生多次的交互，用户期望立刻看到反馈。太慢了会让交互感觉迟钝。",[10,165,166,169],{},[35,167,168],{},"Standard（0.34 秒 \u002F 340ms）","：消息气泡进场、列表项滑入、Toast 弹出。这是用户注意到的变化，需要一些时间让眼睛跟随，但不能太长。动效过长就开始让人觉得\"卡\"而不是\"流畅\"。",[10,171,172,175],{},[35,173,174],{},"Emphasis（0.48 秒 \u002F 480ms）","：页面切换的主体。新页面的进入需要最长的时间，因为整个屏幕都在变。但退出只需要 ~0.36 秒（75% 的进入时长），用户离开某个页面应该更快、不需要过度演示。",[10,177,178],{},"缓动曲线有两种常见类型。",[10,180,181,184,185,188],{},[35,182,183],{},"OutExpo","（",[105,186,187],{},"[0.16, 1, 0.3, 1]","）：开始快、结束慢。用在列表项进场、消息气泡滑入。因为是内容从无到有，用户的注意力已经被吸引，动画可以前置地快速到达目标位置，最后用缓冲平稳落地。感觉是\"内容飘进来了\"。",[10,190,191,184,194,197],{},[35,192,193],{},"SoftSpring",[105,195,196],{},"[0.34, 1.4, 0.5, 1]","）：轻微的超调回弹。用在弹窗进出、输入框 focus。因为是用户主动触发的强交互，回弹感可以传递\"系统在听我\"的反馈。不是生硬的到位，而是\"弹\"到位。",[10,199,200,201,204,205,208,209,212],{},"Spring 预设有三档：",[105,202,203],{},"snappy","（stiffness 520, damping 30）用于最快反应、",[105,206,207],{},"bouncy","（380, 22）用于入场、",[105,210,211],{},"smooth","（260, 32）用于页面转场。数字越大振荡越少，但响应越快；数字小了振荡次数多、柔软感强。",[10,214,215],{},"这些参数都是经过测试的。如果你随意加长动效时间想让用户\"看清每一帧\"，代价是整个产品的交互节奏都会崩。快动效之间的间隙会积累，让用户觉得系统在思考，而不是在响应。",[14,217,218],{"id":218},"时长过长的代价",[10,220,221],{},"某些设计师认为慢动效显得\"精致\"。实践中这是错的。",[10,223,224],{},"首先，从用户体验的角度：动效的目的是传达信息，不是让用户等待。当一个必要的动效（如页面转场）变得太长，用户在反馈到来之前就开始扫视下一个操作目标，或者认为系统在卡。更长的等待就变成了对用户时间的浪费。",[10,226,227],{},"其次，从性能的角度：更长的动效意味着更多的帧要渲染。一个快速的动效和一个冗长的动效相比，后者不仅占用的渲染时间更多，对 GPU 和内存的压力也更大。在移动设备上、在 GPU 受限的情况下，这直接转化成帧率下降、电池耗尽、设备发热。",[10,229,230],{},"第三，从注意力的角度：冗长的动效会让用户的焦点分散。正在播放的动画仍在吸引视觉注意，用户无法提前扫视接下来的内容。这不是在尊重用户，而是在强制停留。",[10,232,233,234,237],{},"在实施 yun-claude 的动效设计时，判断的标准是：",[35,235,236],{},"动效的时长应该足够用户感知状态变化，但不足够让用户等待","。所以页面进入用 0.48s（完整展示），退出只用 0.36s（快速离开），打字指示用 0.17s（微交互要快）。这个分层是为了加速工作流，而不是为了炫技。",[14,239,240],{"id":240},"小结",[10,242,243],{},"写三条判据是为了在每个 feature 决策点问自己：",[245,246,247,251,254],"ul",{},[248,249,250],"li",{},"这个动效在传达用户需要知道的状态变化吗？",[248,252,253],{},"这个动效在建立空间或层级关系吗？",[248,255,256],{},"如果都不是，它只是装饰。",[10,258,259,260,262],{},"如果是前两者，动效 ",[35,261,37],{},"。保证 60fps、尊重 reduced-motion、控制好时长参数。",[10,264,265,266,269],{},"如果是装饰，",[35,267,268],{},"该砍","。或者保留到特定时刻（成功、失败、关键反馈），而不是无处不在。",[10,271,272],{},"动效不是越多越好，也不是越慢越高级。克制地用、精准地用，动效才能真正帮助用户理解界面。",{"title":274,"searchDepth":275,"depth":275,"links":276},"",2,[277,283,287,288,289],{"id":16,"depth":275,"text":16,"children":278},[279,281,282],{"id":20,"depth":280,"text":21},3,{"id":41,"depth":280,"text":42},{"id":63,"depth":280,"text":64},{"id":86,"depth":275,"text":86,"children":284},[285,286],{"id":89,"depth":280,"text":90},{"id":118,"depth":280,"text":119},{"id":154,"depth":275,"text":154},{"id":218,"depth":275,"text":218},{"id":240,"depth":275,"text":240},"工程手记","2026-07-05","md",null,{},true,"\u002F2026-07-05",{"title":5,"description":12},"2026-07-05-克制的网页动效","三条判据分辨哪些动效值得做、哪些该砍，以及性能与可访问性的硬要求。",[301,302,303,304],"交互动效","Web 性能","无障碍","用户体验","nAZWp03XcfG3E4VVq7VsNOXAWYCi5XBAj6SGZflH3Hk",[307,504,863],{"id":308,"title":309,"body":310,"column":290,"date":489,"description":490,"extension":292,"hero_image":293,"meta":491,"navigation":295,"path":492,"seo":493,"series_id":293,"severity":293,"stem":494,"summary":495,"tags":496,"__hash__":503},"posts\u002F2026-09-10-Ai项目群的工程化分水岭.md","AI 项目做多以后，真正难的是把边界接起来",{"type":7,"value":311,"toc":478},[312,319,322,325,329,333,343,346,349,365,368,372,391,394,398,407,410,414,417,420,440,443,447,450,453,456,459,462,465,469,472,475],[10,313,314,315,318],{},"这段时间回看 ",[105,316,317],{},"\u002FUsers\u002Fxingye\u002FAi"," 下的项目，最明显的变化不是项目数量增加，而是问题的重心变了。",[10,320,321],{},"早期问题集中在“功能能不能跑起来”：语音推理、网页生成、桌面端启动、小程序页面、视频服务。进入平台化阶段以后，问题变成了另一组：任务是否可追踪，失败是否能恢复，费用是否能对账，版本是否能回滚，用户是否知道发生了什么。",[10,323,324],{},"这是一条从功能开发走向系统工程的分水岭。",[14,326,328],{"id":327},"一项目群已经形成几条清晰的产品线","一、项目群已经形成几条清晰的产品线",[18,330,332],{"id":331},"agent-与多租户平台","Agent 与多租户平台",[10,334,335,338,339,342],{},[105,336,337],{},"yun-claw"," 与 ",[105,340,341],{},"yun-claude"," 的记录显示，平台主线已经覆盖用户容器、模型接入、画布工作流、代理渠道、后台管理和运营工具。近期提交涉及画布自绘 Select、资产大图预览、代理激活码申请，以及渠道注册页的差异化展示。",[10,344,345],{},"这些功能看起来分属不同页面，底层却共享一套问题：用户身份、资源权限、异步任务和状态反馈必须保持一致。只改页面，不补状态契约，功能就会在跨页面操作时暴露断点。",[18,347,348],{"id":348},"内容生产流水线",[10,350,351,354,355,354,358,338,361,364],{},[105,352,353],{},"ai-write","、",[105,356,357],{},"instantlink-site",[105,359,360],{},"ai-auto-edit",[105,362,363],{},"ai-digital-human-workflow"," 分别覆盖写作、漫剧、自动剪辑和数字人视频。提交记录里反复出现几个关键词：任务网关、切片下载、共享卷、FFmpeg、提示词优化、失败提示和构建复现。",[10,366,367],{},"这说明内容生产已经不是单次调用模型，而是一条有中间产物的流水线。文本、图片、视频、音频和项目资产都有生命周期，任何一步的状态丢失都会让用户无法判断下一步该做什么。",[18,369,371],{"id":370},"交付网关与桌面端","交付、网关与桌面端",[10,373,374,354,377,354,380,354,383,386,387,390],{},[105,375,376],{},"newCodex",[105,378,379],{},"new-openclaw",[105,381,382],{},"openclaw",[105,384,385],{},"服务器管理"," 和 ",[105,388,389],{},"SSHTS"," 的记录，集中在更新、恢复、远程诊断、单实例生命周期和离线环境上。这里的核心指标不是“新版本发布了没有”，而是安装失败能否定位、更新中断能否恢复、客户机器能否留下可审计证据。",[10,392,393],{},"这也是为什么桌面端工程会逐渐靠近运维工程：安装器、后台服务、日志、配置和回滚必须一起设计。",[18,395,397],{"id":396},"网关计费与数据观察","网关、计费与数据观察",[10,399,400,338,403,406],{},[105,401,402],{},"NewAPI",[105,404,405],{},"new-api"," 的近期记录包括按模型统计 Token 消耗、扣费量、时间粒度聚合、昨日活跃用户和订单搜索。网关不只是转发请求，它还承担了模型选择、消耗记录、后台观察和问题追踪。",[10,408,409],{},"一旦计费数据进入用户体验，接口成功与业务成功就不再是同一个概念。请求返回 200，只能说明一次网络调用完成；扣费、产物落盘和任务终态是否一致，需要单独核对。",[14,411,413],{"id":412},"二git-记录里最有价值的变化是边界被写出来了","二、Git 记录里最有价值的变化，是“边界被写出来了”",[10,415,416],{},"多个项目的文档都在补同一类内容：部署手册、验收清单、测试报告、故障复盘、模型环境快照和用户协议。",[10,418,419],{},"这类文档的价值不在于把代码再讲一遍，而在于把系统边界写清楚：",[245,421,422,425,428,431,434,437],{},[248,423,424],{},"输入是什么，哪些字段必须存在；",[248,426,427],{},"状态有哪些，什么条件才能进入下一状态；",[248,429,430],{},"外部服务超时后，如何判断请求是否已经执行；",[248,432,433],{},"产物写入失败时，是否允许重试；",[248,435,436],{},"更新中断后，哪个版本可以安全恢复；",[248,438,439],{},"用户看到的成功，是否和后台记录的成功一致。",[10,441,442],{},"当这些问题没有答案时，系统只能靠人工经验维持。一旦项目数量增加，经验就会变成不可复制的隐性依赖。",[14,444,446],{"id":445},"三下一阶段的重点不是再加一个入口","三、下一阶段的重点不是再加一个入口",[10,448,449],{},"从现有项目的提交和文档可以看到，下一阶段更值得投入的是四个基础能力。",[10,451,452],{},"第一，统一任务状态。不同项目都在处理异步任务，但状态命名、失败原因和重试策略不应各自发明。",[10,454,455],{},"第二，统一产物账本。一个任务产生了哪些文件、消耗了哪些资源、是否对用户可见，需要能从提交到终态完整追踪。",[10,457,458],{},"第三，统一故障出口。401、超时、部分成功、未知执行和版本回滚，应该有明确的用户反馈与后台证据。",[10,460,461],{},"第四，统一发布验收。构建通过不代表线上可用，应该把健康检查、代理路径、真实端口、关键日志和浏览器操作放到同一份验收清单里。",[10,463,464],{},"项目多以后，工程能力不再体现在“能写多少功能”，而体现在“能否让不同功能遵守同一套边界”。",[14,466,468],{"id":467},"四我会保留的判断","四、我会保留的判断",[10,470,471],{},"AI 项目最容易被低估的部分，不是模型调用，而是模型调用之后的系统。",[10,473,474],{},"生成只是起点。身份、任务、费用、存储、交付、恢复和反馈，决定了一个功能能不能进入真实使用。",[10,476,477],{},"当 Git 提交开始频繁出现“补状态”“补恢复”“补验收”“补审计”时，这不是工程变慢了，而是产品从演示阶段进入了可运营阶段。",{"title":274,"searchDepth":275,"depth":275,"links":479},[480,486,487,488],{"id":327,"depth":275,"text":328,"children":481},[482,483,484,485],{"id":331,"depth":280,"text":332},{"id":348,"depth":280,"text":348},{"id":370,"depth":280,"text":371},{"id":396,"depth":280,"text":397},{"id":412,"depth":275,"text":413},{"id":445,"depth":275,"text":446},{"id":467,"depth":275,"text":468},"2026-09-10","这段时间回看 \u002FUsers\u002Fxingye\u002FAi 下的项目，最明显的变化不是项目数量增加，而是问题的重心变了。",{},"\u002F2026-09-10-ai",{"title":309,"description":490},"2026-09-10-Ai项目群的工程化分水岭","从多个 AI 项目的 Git 记录和设计文档看，工程难点已经从“能不能生成”转向任务、计费、交付、恢复与用户反馈能否形成闭环。",[497,498,499,500,501,502],"AI 平台","Agent","内容生产","网关","桌面端","工程化","DzOpOX92EsanPp8pLNTusbzBmKH3WOTf3qcaZAFmhM8",{"id":505,"title":506,"body":507,"column":290,"date":850,"description":511,"extension":292,"hero_image":293,"meta":851,"navigation":295,"path":852,"seo":853,"series_id":293,"severity":293,"stem":854,"summary":855,"tags":856,"__hash__":862},"posts\u002F2026-08-15-上线前我把整个仓库审了一遍.md","上线前，我把整个仓库审了一遍",{"type":7,"value":508,"toc":839},[509,512,515,519,522,543,546,549,553,556,559,562,565,569,572,575,641,644,650,654,660,663,666,673,677,680,691,694,698,701,710,713,726,729,732,736,739,746,752,755,759,762,767,784,789,803,806,809,813,816,836],[10,510,511],{},"上线前做了一次全量静态审查。",[10,513,514],{},"本文只讲方法和结论分布。审查报告本身不入库，也不在这里复述——那份报告带内网拓扑、密钥存放位置和尚未修复问题的文件行号证据，公开它就等于公开一份攻击者的地图。",[14,516,518],{"id":517},"一审查怎么进行","一、审查怎么进行",[10,520,521],{},"三个部分：",[523,524,525,531,537],"ol",{},[248,526,527,530],{},[35,528,529],{},"人工列清单。"," 先按「一个系统可能从哪些地方被打穿」列出检查面，而不是按目录扫。",[248,532,533,536],{},[35,534,535],{},"四个并行只读探查代理。"," 每个代理负责一组检查面，只读，不允许改任何文件。",[248,538,539,542],{},[35,540,541],{},"关键证据人工逐条复核。"," 代理解释现象可以，但「这条算不算风险、算多重」由人定。",[10,544,545],{},"第三步是必要的。代理很容易把「写法不理想」报成「高危」，也很容易漏掉需要跨文件才能看出的问题。它适合做的是穷举和取证，不适合定级。",[10,547,548],{},"审查方式记为：静态审查（人工 + 4 个并行只读探查代理）+ 关键证据人工逐条复核。",[14,550,552],{"id":551},"二只读约束的作用","二、只读约束的作用",[10,554,555],{},"让探查代理只能读，有两个好处。",[10,557,558],{},"一是没有副作用。审查过程中仓库保持原样，报告的结论可以对着同一个 commit 复现。",[10,560,561],{},"二是省掉了「代理顺手改一下」的干扰。审查和修改混在一起，会出现「报告说有问题的地方已经被改过」的状态，后续复核就无法对着证据走。",[10,563,564],{},"修改是审查之后单独一轮的事。",[14,566,568],{"id":567},"三20-条与四个级别","三、20 条与四个级别",[10,570,571],{},"产出 20 条风险，编号 R01 到 R20。每一条有：位置、触发条件、影响、修复优先级。",[10,573,574],{},"按修复优先级分四档：",[576,577,578,594],"table",{},[579,580,581],"thead",{},[582,583,584,588,591],"tr",{},[585,586,587],"th",{},"优先级",[585,589,590],{},"条数",[585,592,593],{},"排期",[595,596,597,609,620,631],"tbody",{},[582,598,599,603,606],{},[600,601,602],"td",{},"P0",[600,604,605],{},"2",[600,607,608],{},"本周内",[582,610,611,614,617],{},[600,612,613],{},"P1",[600,615,616],{},"6",[600,618,619],{},"两周内",[582,621,622,625,628],{},[600,623,624],{},"P2",[600,626,627],{},"5",[600,629,630],{},"一个月内",[582,632,633,636,639],{},[600,634,635],{},"P3",[600,637,638],{},"7",[600,640,630],{},[10,642,643],{},"另外有一个独立的「严重级别」列，按严重 \u002F 高 \u002F 中 \u002F 低分：1 \u002F 4 \u002F 7 \u002F 8。两列不对应是有意的——「有多严重」和「该多快修」是两件事。一条严重但需要停机才能修的问题，排期上反而不能放在本周。",[10,645,646,647,97],{},"整体评级：",[35,648,649],{},"高",[14,651,653],{"id":652},"四结论里最值得记的一句","四、结论里最值得记的一句",[655,656,657],"blockquote",{},[10,658,659],{},"未发现 SQL 注入、越权与 IDOR、存储型 XSS 等直接可利用的应用漏洞。",[10,661,662],{},"这一句和「整体评级高」摆在一起看，是这个月里最值得记的一条工程结论。",[10,664,665],{},"风险分布和直觉不一致。业务代码那一侧没查出可直接利用的漏洞——那部分每天在改、每天在跑、每天有人看。问题更多集中在业务代码之外：构建、部署、配置、凭据流转、依赖引入、运行时加固。",[10,667,668,669,672],{},"这些东西的共同点是",[35,670,671],{},"平时不跑","。部署脚本一天执行几次，Dockerfile 改一次放很久，集群配置是照着文档抄的。没有日常反馈的东西，问题会一直留在那里，而且不会有人因为「它今天没出事」而去核对它。",[14,674,676],{"id":675},"五修复的排期方式","五、修复的排期方式",[10,678,679],{},"路线图按三档排：P0 本周、P1 两周内、P2\u002FP3 一个月内。",[10,681,682,683,686,687,690],{},"排期依据不是严重级别，是两件事：",[35,684,685],{},"能不能立即修","，和",[35,688,689],{},"修它会不会动到线上","。一条严重但需要停机或改基础设施配置的问题，得等一个发布窗口；一条中等但改动只在一处代码里的问题，可以马上做。P2\u002FP3 是加固项，攒到下一个版本一起走。",[10,692,693],{},"这样做的好处是排期可执行。「两个月内修完 20 条」听起来完整，实际会卡在第一条需要停机的问题上。",[14,695,697],{"id":696},"六报告为什么不入库","六、报告为什么不入库",[10,699,700],{},"仓库的忽略规则里专门有一条：",[702,703,708],"pre",{"className":704,"code":706,"language":707},[705],"language-text","# 安全审查报告：含内网拓扑、密钥存放路径与未修复漏洞的 file:line 证据，绝不入库\n","text",[105,709,706],{"__ignoreMap":274},[10,711,712],{},"三条理由，最后一条最关键：",[245,714,715,718,721],{},[248,716,717],{},"内网拓扑",[248,719,720],{},"密钥存放位置",[248,722,723],{},[35,724,725],{},"未修复漏洞的精确定位",[10,727,728],{},"前两条是常识。第三条是这类文档的特例：一份「待修复清单」在修完之前，本身就是攻击说明书。而它又必须有精确定位，否则修不动。",[10,730,731],{},"所以这份文档的流转方式和代码相反。代码进仓库、留历史；它不进仓库，修完一条删一条，修完的结论改记到提交信息里。",[14,733,735],{"id":734},"七审查之后同一天修的东西","七、审查之后，同一天修的东西",[10,737,738],{},"审查和修复放在同一天做，这是这次的一个做法。",[10,740,741,742,745],{},"当天修的一批，主题集中在一类：",[35,743,744],{},"不报错的失败","。队列投递端断线后不重连、依赖返回的包装形状读错导致判定恒为假、入队不带权重插到最前面、参数格式不对被上游全部拒收、重试链被自己的去重压短、管理操作的幂等键缺失、用户可控的文件名直接进对象存储键、唯一漏掉来源校验的 IPC handler、并发删除后返回假成功、过期验证码被复活、临时文件不清理。",[10,747,748,749,97],{},"这些已经有单独一篇记录，这里只提它们的共性：",[35,750,751],{},"都不抛异常",[10,753,754],{},"审查报告里那些「不进仓库」的条目和这些「当天修完」的条目，其实是同一批检查过出来的。区别只在于前者需要动基础设施、要等窗口，后者可以立刻改。",[14,756,758],{"id":757},"八静态审查的边界","八、静态审查的边界",[10,760,761],{},"这一轮做的是静态审查，能覆盖和不能覆盖的东西都比较明确。",[10,763,764],{},[35,765,766],{},"能覆盖：",[245,768,769,772,775,778,781],{},[248,770,771],{},"配置与依赖：镜像标签、依赖来源、加密参数、权限配置。",[248,773,774],{},"错误处理：异常的吞掉与上抛、失败路径有没有留痕。",[248,776,777],{},"凭据流转：什么地方读、什么地方写、会不会进构建产物。",[248,779,780],{},"部署清单：运行用户、资源限制、网络策略、滚动更新参数。",[248,782,783],{},"代码层面的模式：路径拼接、命令拼接、输入校验的缺失。",[10,785,786],{},[35,787,788],{},"覆盖不到：",[245,790,791,794,797,800],{},[248,792,793],{},"运行时竞态。两个请求同时到、并发删除、租约过期，这些要在真实并发下才暴露。",[248,795,796],{},"真实的流量特征。哪些入口真的被外部打到、参数实际长什么样，静态看只能推测。",[248,798,799],{},"业务逻辑漏洞。越权取数据这类问题需要理解「这个人该不该看到这条」，而业务意图不在代码里。",[248,801,802],{},"组合链。单看每一步都合规，串起来才是问题——这类要动态测试或者真实对抗。",[10,804,805],{},"第三类是最容易被静态审查漏掉的：代码里所有校验都写了，但校验的规则和业务规则不一致。",[10,807,808],{},"所以静态审查的产出不是「系统安全了」，而是「在它能看见的那一面里，还剩哪些要做」。",[14,810,812],{"id":811},"九一次审查真正留下的东西","九、一次审查真正留下的东西",[10,814,815],{},"审完当天修掉的那批，留下的是代码。报告里那些没修的，留下的是三样东西：",[523,817,818,824,830],{},[248,819,820,823],{},[35,821,822],{},"一份带精确定位和优先级的清单","，不进仓库，逐条消掉。",[248,825,826,829],{},[35,827,828],{},"一个判断","：风险集中的地方和日常注意力集中的地方不一样。业务代码天天改，反而干净；构建部署配置平时没人动。",[248,831,832,835],{},[35,833,834],{},"一次复核的习惯","：审查的结论要能对着一个 commit 复现，所以探查只读、修改单独一轮。",[10,837,838],{},"第 2 条比第 1 条重要。清单会修完，但「平时不跑的东西没人核对」这件事不会自己消失。",{"title":274,"searchDepth":275,"depth":275,"links":840},[841,842,843,844,845,846,847,848,849],{"id":517,"depth":275,"text":518},{"id":551,"depth":275,"text":552},{"id":567,"depth":275,"text":568},{"id":652,"depth":275,"text":653},{"id":675,"depth":275,"text":676},{"id":696,"depth":275,"text":697},{"id":734,"depth":275,"text":735},{"id":757,"depth":275,"text":758},{"id":811,"depth":275,"text":812},"2026-08-15",{},"\u002F2026-08-15",{"title":506,"description":511},"2026-08-15-上线前我把整个仓库审了一遍","一次上线前的静态安全审查：四个并行只读代理加人工逐条复核，产出 20 条分级风险与三档修复路线图。本文记录方法，不复述条目。",[857,858,859,860,861],"安全审查","代码审查","风险分级","修复排期","Agent 协作","bnkC_2bMe8OfgB00HIfDUS9bdRwLUZb_WqDBvXtq__M",{"id":864,"title":865,"body":866,"column":290,"date":1782,"description":870,"extension":292,"hero_image":293,"meta":1783,"navigation":295,"path":1784,"seo":1785,"series_id":293,"severity":293,"stem":1786,"summary":1787,"tags":1788,"__hash__":1795},"posts\u002F2026-08-02-首页改成八屏.md","首页改成八屏",{"type":7,"value":867,"toc":1769},[868,871,877,880,884,887,898,907,910,913,921,928,934,940,1005,1008,1027,1032,1037,1040,1046,1050,1053,1056,1064,1067,1070,1076,1079,1107,1110,1224,1227,1233,1237,1240,1256,1270,1273,1289,1292,1295,1299,1305,1308,1311,1317,1320,1370,1374,1377,1380,1383,1389,1392,1395,1400,1406,1410,1413,1416,1422,1426,1431,1472,1482,1485,1514,1519,1522,1572,1578,1581,1638,1650,1653,1657,1660,1663,1668,1671,1674,1679,1683,1686,1756,1759,1765],[10,869,870],{},"这个博客的首页原来是连续滚动。改成了整屏分页，一页一件事，共八屏：",[702,872,875],{"className":873,"code":874,"language":707},[705],"首屏 \u002F 自我介绍 01 \u002F 自我介绍 02 \u002F 做过的事 \u002F 精选复盘 \u002F 项目 \u002F 专栏 \u002F 联系\n",[105,876,874],{"__ignoreMap":274},[10,878,879],{},"一屏容纳是逐档实测调出来的，不是估的。",[14,881,883],{"id":882},"一吸附用谁的","一、吸附用谁的",[10,885,886],{},"第一个决定是吸附用哪套机制。",[10,888,889,890,893,894,897],{},"原生有 ",[105,891,892],{},"scroll-snap","，CSS 几行就能写。但这里用不了——页面上的平滑滚动是 Lenis 提供的，它用 JS 驱动 ",[105,895,896],{},"scrollTop","：",[655,899,900],{},[10,901,902,903,906],{},"吸附用 Lenis 自带的 Snap 而不是 CSS scroll-snap——Lenis 是用 JS 驱动 scrollTop 的，和原生吸附会互相抢控制权、滚起来发抖。Snap 挂在同一个 Lenis 实例上没有这个问题，",[105,904,905],{},"onSnapComplete"," 还能驱动指示器。",[10,908,909],{},"两个机制都想控制滚动位置，结果就是抖。",[10,911,912],{},"挂在同一个实例上之后，吸附完成后还能拿到回调，右侧的页码指示器直接跟着它更新。",[14,914,916,917,920],{"id":915},"二mandatory-是错的","二、",[105,918,919],{},"mandatory"," 是错的",[10,922,923,924,927],{},"第一版用 ",[105,925,926],{},"type: 'mandatory'","。用户反馈手感是「被抢走了」。",[10,929,930,931,933],{},"原因是 ",[105,932,919],{}," 在用户还在滑的时候就强行把页面拽向最近一页。滑动过程被插手，手感不是吸附，是失控。",[10,935,936,937,897],{},"改成 ",[105,938,939],{},"proximity",[702,941,945],{"className":942,"code":943,"language":944,"meta":274,"style":274},"language-ts shiki shiki-themes github-light github-dark","type: 'proximity',\ndistanceThreshold: '30%',\nduration: 1.1,\ndebounce: 500,\n","ts",[105,946,947,967,979,992],{"__ignoreMap":274},[948,949,952,956,960,964],"span",{"class":950,"line":951},"line",1,[948,953,955],{"class":954},"sScJk","type",[948,957,959],{"class":958},"sVt8B",": ",[948,961,963],{"class":962},"sZZnC","'proximity'",[948,965,966],{"class":958},",\n",[948,968,969,972,974,977],{"class":950,"line":275},[948,970,971],{"class":954},"distanceThreshold",[948,973,959],{"class":958},[948,975,976],{"class":962},"'30%'",[948,978,966],{"class":958},[948,980,981,984,986,990],{"class":950,"line":280},[948,982,983],{"class":954},"duration",[948,985,959],{"class":958},[948,987,989],{"class":988},"sj4cs","1.1",[948,991,966],{"class":958},[948,993,995,998,1000,1003],{"class":950,"line":994},4,[948,996,997],{"class":954},"debounce",[948,999,959],{"class":958},[948,1001,1002],{"class":988},"500",[948,1004,966],{"class":958},[10,1006,1007],{},"三条参数各管一件事：",[245,1009,1010,1015,1021],{},[248,1011,1012,1014],{},[105,1013,939],{}," 只在停下来且已经接近边界时才轻推一把。",[248,1016,1017,1020],{},[105,1018,1019],{},"distanceThreshold: '30%'"," 定义「接近」——落点离边界不到 30% 视口高才吸附，停在页面中间就让它停着。",[248,1022,1023,1026],{},[105,1024,1025],{},"debounce: 500"," 等惯性完全停下来再判断，不在滑行中途插手。",[10,1028,1029,1031],{},[105,1030,997],{}," 从 220 提到 500：",[655,1033,1034],{},[10,1035,1036],{},"debounce 太小会在惯性还没停时就抢着吸附，手感发涩。",[10,1038,1039],{},"实测结果：",[702,1041,1044],{"className":1042,"code":1043,"language":707},[705],"滑动全程 0 反向帧（原来会被拽回）\n停在离边界 306px（超过 30% 阈值）时保持不动\n停在边界附近才对齐整页\n",[105,1045,1043],{"__ignoreMap":274},[14,1047,1049],{"id":1048},"三一屏装不装得下","三、一屏装不装得下",[10,1051,1052],{},"分页的前提是内容能装进一屏。这一条逐档量过。",[10,1054,1055],{},"首屏原高 921px：",[245,1057,1058,1061],{},[248,1059,1060],{},"1440×900 超出 89px",[248,1062,1063],{},"1366×768 超出 201px",[10,1065,1066],{},"精选复盘 788px，在 1366×768 超出 88px。",[10,1068,1069],{},"两个方向可选：砍内容，或者按档收紧间距。选了后者，分三档：",[702,1071,1074],{"className":1072,"code":1073,"language":707},[705],"min-width: 1024px and max-height: 820px   深压\nmin-width: 1024px and max-height: 900px   只压首屏\nmin-width: 1024px and min-height: 1000px  放大间距吃大屏留白\n",[105,1075,1073],{"__ignoreMap":274},[10,1077,1078],{},"中间那档的调整方式值得记一笔。为了挤出 27px，第一版直接砍了字号：",[702,1080,1084],{"className":1081,"code":1082,"language":1083,"meta":274,"style":274},"language-css shiki shiki-themes github-light github-dark","font-size: clamp(46px, 9.2vw, 132px)  →  clamp(30px, 5.4vw, 62px)\n","css",[105,1085,1086],{"__ignoreMap":274},[948,1087,1088,1092,1095,1098,1101,1104],{"class":950,"line":951},[948,1089,1091],{"class":1090},"s9eBZ","font-size",[948,1093,1094],{"class":958},": clamp(46px, 9",[948,1096,1097],{"class":954},".2vw",[948,1099,1100],{"class":958},", 132px)  →  clamp(30px, 5",[948,1102,1103],{"class":954},".4vw",[948,1105,1106],{"class":958},", 62px)\n",[10,1108,1109],{},"砍掉一半多。首屏标题是整页的主体，砍字号会让整屏塌下来。改成全部从内边距要：",[702,1111,1113],{"className":1081,"code":1112,"language":1083,"meta":274,"style":274},"@media (min-width: 1024px) and (max-height: 900px) {\n  .page-hero :deep(.hero) { padding-top: 20px; padding-bottom: 24px; }\n  .page-hero :deep(.hero-foot) { margin-top: 26px; }\n}\n",[105,1114,1115,1156,1196,1219],{"__ignoreMap":274},[948,1116,1117,1121,1124,1127,1129,1132,1135,1138,1141,1143,1146,1148,1151,1153],{"class":950,"line":951},[948,1118,1120],{"class":1119},"szBVR","@media",[948,1122,1123],{"class":958}," (",[948,1125,1126],{"class":988},"min-width",[948,1128,959],{"class":958},[948,1130,1131],{"class":988},"1024",[948,1133,1134],{"class":1119},"px",[948,1136,1137],{"class":958},") ",[948,1139,1140],{"class":1119},"and",[948,1142,1123],{"class":958},[948,1144,1145],{"class":988},"max-height",[948,1147,959],{"class":958},[948,1149,1150],{"class":988},"900",[948,1152,1134],{"class":1119},[948,1154,1155],{"class":958},") {\n",[948,1157,1158,1161,1164,1167,1170,1173,1175,1178,1180,1183,1186,1188,1191,1193],{"class":950,"line":275},[948,1159,1160],{"class":954},"  .page-hero",[948,1162,1163],{"class":958}," :deep(",[948,1165,1166],{"class":954},".hero",[948,1168,1169],{"class":958},") { ",[948,1171,1172],{"class":988},"padding-top",[948,1174,959],{"class":958},[948,1176,1177],{"class":988},"20",[948,1179,1134],{"class":1119},[948,1181,1182],{"class":958},"; ",[948,1184,1185],{"class":988},"padding-bottom",[948,1187,959],{"class":958},[948,1189,1190],{"class":988},"24",[948,1192,1134],{"class":1119},[948,1194,1195],{"class":958},"; }\n",[948,1197,1198,1200,1202,1205,1207,1210,1212,1215,1217],{"class":950,"line":280},[948,1199,1160],{"class":954},[948,1201,1163],{"class":958},[948,1203,1204],{"class":954},".hero-foot",[948,1206,1169],{"class":958},[948,1208,1209],{"class":988},"margin-top",[948,1211,959],{"class":958},[948,1213,1214],{"class":988},"26",[948,1216,1134],{"class":1119},[948,1218,1195],{"class":958},[948,1220,1221],{"class":950,"line":994},[948,1222,1223],{"class":958},"}\n",[10,1225,1226],{},"只有 700px 可用高度那一档才让一档字号。三档实测：",[702,1228,1231],{"className":1229,"code":1230,"language":707},[705],"1440×900    132px\n1920×1080   132px\n1366×768     92.9px\n",[105,1232,1230],{"__ignoreMap":274},[14,1234,1236],{"id":1235},"四检测判据写错过一次","四、检测判据写错过一次",[10,1238,1239],{},"判断页面有没有溢出，第一版用的是：",[702,1241,1243],{"className":942,"code":1242,"language":944,"meta":274,"style":274},"scrollHeight > clientHeight\n",[105,1244,1245],{"__ignoreMap":274},[948,1246,1247,1250,1253],{"class":950,"line":951},[948,1248,1249],{"class":958},"scrollHeight ",[948,1251,1252],{"class":1119},">",[948,1254,1255],{"class":958}," clientHeight\n",[10,1257,1258,1259,1262,1263,386,1266,1269],{},"这个判据永远为假。页面用的是 ",[105,1260,1261],{},"min-height","，内容超了页面会自己长高——",[105,1264,1265],{},"scrollHeight",[105,1267,1268],{},"clientHeight"," 一起变大，比值不变。",[10,1271,1272],{},"改成直接比可用高度：",[702,1274,1276],{"className":942,"code":1275,"language":944,"meta":274,"style":274},"\u002F\u002F 检测判据从 scrollHeight > clientHeight 改为直接比可用高度——\n\u002F\u002F 页面用的是 min-height，内容超了页面会自己长高，前者永远比不出溢出\n",[105,1277,1278,1284],{"__ignoreMap":274},[948,1279,1280],{"class":950,"line":951},[948,1281,1283],{"class":1282},"sJ8bj","\u002F\u002F 检测判据从 scrollHeight > clientHeight 改为直接比可用高度——\n",[948,1285,1286],{"class":950,"line":275},[948,1287,1288],{"class":1282},"\u002F\u002F 页面用的是 min-height，内容超了页面会自己长高，前者永远比不出溢出\n",[10,1290,1291],{},"这个错误值得单独说：判据本身写错了，所以「三档零溢出」这个结论在修正之前是不成立的。",[10,1293,1294],{},"修正之后三档均为 8\u002F8 页零溢出。",[14,1296,1298],{"id":1297},"五不分页的三种情况","五、不分页的三种情况",[702,1300,1303],{"className":1301,"code":1302,"language":707},[705],"窄屏（\u003C 1024px）\n矮视口（\u003C 620px）\n减弱动效（prefers-reduced-motion）\n",[105,1304,1302],{"__ignoreMap":274},[10,1306,1307],{},"三种都退回连续滚动。",[10,1309,1310],{},"前两种有实测依据：",[702,1312,1315],{"className":1313,"code":1314,"language":707},[705],"实测 390px 下自我介绍 1448px、精选复盘 1114px，强行一屏只会截断内容\n",[105,1316,1314],{"__ignoreMap":274},[10,1318,1319],{},"第三种是硬要求。整屏吸附会强制改变滚动位置，这和「尊重减弱动效偏好」直接冲突。CSS 里也要同步禁用：",[702,1321,1323],{"className":1081,"code":1322,"language":1083,"meta":274,"style":274},"@media (prefers-reduced-motion: reduce) {\n  .page { min-height: 0; }\n  .pager { display: none; }\n}\n",[105,1324,1325,1332,1349,1366],{"__ignoreMap":274},[948,1326,1327,1329],{"class":950,"line":951},[948,1328,1120],{"class":1119},[948,1330,1331],{"class":958}," (prefers-reduced-motion: reduce) {\n",[948,1333,1334,1337,1340,1342,1344,1347],{"class":950,"line":275},[948,1335,1336],{"class":954},"  .page",[948,1338,1339],{"class":958}," { ",[948,1341,1261],{"class":988},[948,1343,959],{"class":958},[948,1345,1346],{"class":988},"0",[948,1348,1195],{"class":958},[948,1350,1351,1354,1356,1359,1361,1364],{"class":950,"line":280},[948,1352,1353],{"class":954},"  .pager",[948,1355,1339],{"class":958},[948,1357,1358],{"class":988},"display",[948,1360,959],{"class":958},[948,1362,1363],{"class":988},"none",[948,1365,1195],{"class":958},[948,1367,1368],{"class":950,"line":994},[948,1369,1223],{"class":958},[14,1371,1373],{"id":1372},"六分页之后每页只剩半屏内容","六、分页之后，每页只剩半屏内容",[10,1375,1376],{},"八屏分配完之后，出现一个反向问题：每页内容太少。",[10,1378,1379],{},"实测 1440×900 下多数页只填了 47%~63%，「做过的事」那页 63% 是空的。",[10,1381,1382],{},"既然分了页，每页就得撑得住。逐页补实：",[702,1384,1387],{"className":1385,"code":1386,"language":707},[705],"做过的事：加页首概览条（项目数\u002F时间跨度\u002F人手），每项补详细说明、关键指标与技术标签。308px → 548px\n项目：3 张卡补到 6 张，两行三列。362px → 511px\n联系：补引言、about 与 timeline 两个入口、站点信息带。452px → 791px\n专栏：每张卡补该栏最新一篇标题。512px → 597px\n自我介绍 01：四张能力卡各补首组的真实条目。504px → 596px\n自我介绍 02：补技术栈横带。522px → 603px\n",[105,1388,1386],{"__ignoreMap":274},[10,1390,1391],{},"填充率从 47%~63% 提到 61%~100%。",[10,1393,1394],{},"同一轮里把两个区块的入场动画也补了常驻循环，因为：",[655,1396,1397],{},[10,1398,1399],{},"实测入场动画本来就在跑（轴线 scaleX 0→0.99、圆点带回弹、卡片 clip-path 从 78% 揭到 4%、指标读数往上跳），问题是太隐蔽：一次性、1 秒内结束、触发点又在区块刚露头时，等正眼看过去通常已经播完。",[702,1401,1404],{"className":1402,"code":1403,"language":707},[705],"触发点 top 88% → 95%，轴线 0.9s → 1.25s，圆点与节点错峰 0.11 → 0.16\n",[105,1405,1403],{"__ignoreMap":274},[14,1407,1409],{"id":1408},"七断点从-900-降到-820","七、断点从 900 降到 820",[10,1411,1412],{},"第一版把「矮视口」定在 900px 高。1440×900 的屏幕因此被当成矮视口，白白砍掉了列表摘要——精选复盘从 788px 缩到 389px。",[10,1414,1415],{},"而 1440×900 明明有空间。断点降到 820：",[702,1417,1420],{"className":1418,"code":1419,"language":707},[705],"820 以下   深压\n900 以下   只压首屏\n1000 以上  放大间距吃掉大屏留白\n",[105,1421,1419],{"__ignoreMap":274},[14,1423,1425],{"id":1424},"八两个-css-优先级问题","八、两个 CSS 优先级问题",[10,1427,1428],{},[35,1429,1430],{},"首屏被挤成一个框。",[702,1432,1434],{"className":1081,"code":1433,"language":1083,"meta":274,"style":274},".page { padding: 24px 0; }\n.page-hero { padding: 0; }\n",[105,1435,1436,1457],{"__ignoreMap":274},[948,1437,1438,1441,1443,1446,1448,1450,1452,1455],{"class":950,"line":951},[948,1439,1440],{"class":954},".page",[948,1442,1339],{"class":958},[948,1444,1445],{"class":988},"padding",[948,1447,959],{"class":958},[948,1449,1190],{"class":988},[948,1451,1134],{"class":1119},[948,1453,1454],{"class":988}," 0",[948,1456,1195],{"class":958},[948,1458,1459,1462,1464,1466,1468,1470],{"class":950,"line":275},[948,1460,1461],{"class":954},".page-hero",[948,1463,1339],{"class":958},[948,1465,1445],{"class":988},[948,1467,959],{"class":958},[948,1469,1346],{"class":988},[948,1471,1195],{"class":958},[10,1473,1474,1477,1478,1481],{},[105,1475,1476],{},".page-hero { padding: 0 }"," 写在媒体查询之前，被后面同优先级的 ",[105,1479,1480],{},".page { padding: ... }"," 覆盖了。首屏于是四边都留出内边距，看起来像一个框。",[10,1483,1484],{},"修法是在每一档里重新声明一次：",[702,1486,1488],{"className":1081,"code":1487,"language":1083,"meta":274,"style":274},"\u002F* 首屏必须满幅出血。这行不能省——上面那条 .page 同优先级且更靠后，\n   会把顶部声明的 .page-hero { padding: 0 } 覆盖掉。 *\u002F\n.page-hero { padding: 0; }\n",[105,1489,1490,1495,1500],{"__ignoreMap":274},[948,1491,1492],{"class":950,"line":951},[948,1493,1494],{"class":1282},"\u002F* 首屏必须满幅出血。这行不能省——上面那条 .page 同优先级且更靠后，\n",[948,1496,1497],{"class":950,"line":275},[948,1498,1499],{"class":1282},"   会把顶部声明的 .page-hero { padding: 0 } 覆盖掉。 *\u002F\n",[948,1501,1502,1504,1506,1508,1510,1512],{"class":950,"line":280},[948,1503,1461],{"class":954},[948,1505,1339],{"class":958},[948,1507,1445],{"class":988},[948,1509,959],{"class":958},[948,1511,1346],{"class":988},[948,1513,1195],{"class":958},[10,1515,1516],{},[35,1517,1518],{},"标题被大屏规则压小。",[10,1520,1521],{},"大屏档里有一条：",[702,1523,1525],{"className":1081,"code":1524,"language":1083,"meta":274,"style":274},".page :deep(.big) { font-size: clamp(34px, 3.4vw, 56px); }\n",[105,1526,1527],{"__ignoreMap":274},[948,1528,1529,1531,1533,1536,1538,1540,1542,1545,1548,1551,1553,1556,1559,1562,1564,1567,1569],{"class":950,"line":951},[948,1530,1440],{"class":954},[948,1532,1163],{"class":958},[948,1534,1535],{"class":954},".big",[948,1537,1169],{"class":958},[948,1539,1091],{"class":988},[948,1541,959],{"class":958},[948,1543,1544],{"class":988},"clamp",[948,1546,1547],{"class":958},"(",[948,1549,1550],{"class":988},"34",[948,1552,1134],{"class":1119},[948,1554,1555],{"class":958},", ",[948,1557,1558],{"class":988},"3.4",[948,1560,1561],{"class":1119},"vw",[948,1563,1555],{"class":958},[948,1565,1566],{"class":988},"56",[948,1568,1134],{"class":1119},[948,1570,1571],{"class":958},"); }\n",[10,1573,1574,1575,1577],{},"首屏主标题的类名也是 ",[105,1576,1535],{},"。1920×1080 下它从 132px 被压到 56px，首屏整个塌掉。",[10,1579,1580],{},"改成排除首屏：",[702,1582,1584],{"className":1081,"code":1583,"language":1083,"meta":274,"style":274},"\u002F* 必须排除首屏——hero 里的 .big 是那个巨大的主标题，\n   被这条命中会从 132px 压到 56px，整个首屏塌掉 *\u002F\n.page:not(.page-hero) :deep(.big) { font-size: clamp(34px, 3.4vw, 56px); }\n",[105,1585,1586,1591,1596],{"__ignoreMap":274},[948,1587,1588],{"class":950,"line":951},[948,1589,1590],{"class":1282},"\u002F* 必须排除首屏——hero 里的 .big 是那个巨大的主标题，\n",[948,1592,1593],{"class":950,"line":275},[948,1594,1595],{"class":1282},"   被这条命中会从 132px 压到 56px，整个首屏塌掉 *\u002F\n",[948,1597,1598,1601,1603,1605,1608,1610,1612,1614,1616,1618,1620,1622,1624,1626,1628,1630,1632,1634,1636],{"class":950,"line":280},[948,1599,1600],{"class":954},".page:not",[948,1602,1547],{"class":958},[948,1604,1461],{"class":954},[948,1606,1607],{"class":958},") :deep(",[948,1609,1535],{"class":954},[948,1611,1169],{"class":958},[948,1613,1091],{"class":988},[948,1615,959],{"class":958},[948,1617,1544],{"class":988},[948,1619,1547],{"class":958},[948,1621,1550],{"class":988},[948,1623,1134],{"class":1119},[948,1625,1555],{"class":958},[948,1627,1558],{"class":988},[948,1629,1561],{"class":1119},[948,1631,1555],{"class":958},[948,1633,1566],{"class":988},[948,1635,1134],{"class":1119},[948,1637,1571],{"class":958},[10,1639,1640,1641,97,1644,1646,1647,1649],{},"两个问题的成因相同：",[35,1642,1643],{},"同一个类名在两种语义下被复用",[105,1645,1440],{}," 既是容器也是列表页的内边距来源，",[105,1648,1535],{}," 既是区块小标题也是首屏主标题。分开之后就没这个问题。",[10,1651,1652],{},"修完实测：hero 左 0、底 1、右 10（右侧那 10px 是站点滚动条本身的宽度），标题 132px 满值，8\u002F8 页仍零溢出。",[14,1654,1656],{"id":1655},"九加载页期间的一个时序漏洞","九、加载页期间的一个时序漏洞",[10,1658,1659],{},"这个站有一个启动加载页，首次访问时盖几秒。加载页盖着的时候，入场动画被暂停了——否则动画会在遮挡期间自己跑完。",[10,1661,1662],{},"副作用是页面切换的幕布动画也被冻住：",[655,1664,1665],{},[10,1666,1667],{},"实测（加载页 3s 时点击）：t=745 路由已切到 \u002Fcolumns 而幕布 display:none，t=1406 幕布才出现并扫过。",[10,1669,1670],{},"路由在没有幕布的情况下切换了，等加载页结束、时间线恢复之后，被冻住的补间才补播。",[10,1672,1673],{},"结论是补一行守卫：加载页盖着时直接放行，不放幕布。理由是：",[655,1675,1676],{},[10,1677,1678],{},"真实用户点不到——加载页是全屏遮罩。但浏览器前进\u002F后退、程序化跳转仍可能在窗口内触发导航。",[14,1680,1682],{"id":1681},"十整屏分页的账","十、整屏分页的账",[10,1684,1685],{},"这套改动最后收敛成一张数字表：",[576,1687,1688,1698],{},[579,1689,1690],{},[582,1691,1692,1695],{},[585,1693,1694],{},"项目",[585,1696,1697],{},"数值",[595,1699,1700,1708,1716,1724,1732,1740,1748],{},[582,1701,1702,1705],{},[600,1703,1704],{},"页数",[600,1706,1707],{},"8",[582,1709,1710,1713],{},[600,1711,1712],{},"吸附类型",[600,1714,1715],{},"proximity，阈值 30%，防抖 500ms",[582,1717,1718,1721],{},[600,1719,1720],{},"吸附时长",[600,1722,1723],{},"1.1s",[582,1725,1726,1729],{},[600,1727,1728],{},"分页生效条件",[600,1730,1731],{},"宽 ≥ 1024px 且 高 ≥ 620px 且 未开启减弱动效",[582,1733,1734,1737],{},[600,1735,1736],{},"溢出",[600,1738,1739],{},"三档均 0\u002F8",[582,1741,1742,1745],{},[600,1743,1744],{},"填充率",[600,1746,1747],{},"61%~100%（改前 47%~63%）",[582,1749,1750,1753],{},[600,1751,1752],{},"首屏标题",[600,1754,1755],{},"132px（1440×900、1920×1080）、92.9px（1366×768）",[10,1757,1758],{},"每一条都有一次实测在后面。这类改动没别的办法——「一屏装不装得下」这件事只能量，量完才知道该压间距还是该砍内容。",[10,1760,1761,1762,1764],{},"中间还有两处是判断上的错，不是量的问题：一是用 ",[105,1763,919],{}," 吸附（把用户的滑动抢走了），二是把「矮视口」定在 900（把有空间的屏幕当成没空间）。前者的修法是换模式，后者的修法是挪断点。",[1766,1767,1768],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":274,"searchDepth":275,"depth":275,"links":1770},[1771,1772,1774,1775,1776,1777,1778,1779,1780,1781],{"id":882,"depth":275,"text":883},{"id":915,"depth":275,"text":1773},"二、mandatory 是错的",{"id":1048,"depth":275,"text":1049},{"id":1235,"depth":275,"text":1236},{"id":1297,"depth":275,"text":1298},{"id":1372,"depth":275,"text":1373},{"id":1408,"depth":275,"text":1409},{"id":1424,"depth":275,"text":1425},{"id":1655,"depth":275,"text":1656},{"id":1681,"depth":275,"text":1682},"2026-08-02",{},"\u002F2026-08-02",{"title":865,"description":870},"2026-08-02-首页改成八屏","把博客首页从连续滚动改成八屏整屏分页。吸附用 Lenis 自带能力而不是 CSS 原生，断点按实测调整，每一档的溢出与填充率都有数字。",[1789,1790,1791,1792,1793,1794],"GSAP","Lenis","滚动吸附","响应式断点","页面结构","prefers-reduced-motion","2o9dKTJYPwCLxF__bLLN46O2x_0PMJoQgyZSxZpO428",1789212573192]