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