[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site":3,"post-sou-suo-mian-ban-ctrl-k-he-shu":59,"related-sou-suo-mian-ban-ctrl-k-he-shu":76},{"name":4,"url":5,"description":6,"bio":7,"avatar":8,"skills":9,"tools":16,"social":34,"hero":47,"heatmap":52,"icp":55,"police":56,"policeIcon":57,"shortName":58},"时过境迁技术笔记","https:\u002F\u002Fhsgjq.top","一个记录前端与 Node 实践的个人博客，内容围绕 Vue、Nuxt、NestJS 与工程化展开，附源码级的原理笔记。","一名热爱技术的全栈开发者，专注 TypeScript 与 Web 工程化，习惯把踩过的坑和思考记录在这里。","\u002Fuploads\u002Fc485c101-179e-48db-96d1-238693784c38.jpg",[10,11,12,13,14,15],"TypeScript","Vue 3","Nuxt","NestJS","Node.js","工程化",[17,23,28],{"items":18,"title":22},[19,20,21],"Cursor","Git","Chrome DevTools","开发工具",{"items":24,"title":27},[25,26],"Vite","npm","构建工具",{"items":29,"title":33},[30,31,32],"Docker","CI\u002FCD(GitHub Actions)","Vercel","部署运维",[35,39,43],{"href":36,"icon":37,"label":38},"https:\u002F\u002Fgithub.com\u002Fhsgjq","github","GitHub",{"href":40,"icon":41,"label":42},"2971861643@qq.com","mail","邮箱",{"href":44,"icon":45,"label":46},"\u002Frss.xml","rss","RSS",{"lines":48,"subtitle":51},[49,50],"人生若只如初见，","何事秋风悲画扇。","《木兰花·拟古决绝词柬友》",{"enabled":53,"platform":37,"username":54},true,"hsgjq","桂ICP备2024046985号","桂公网安备45011002000103号","\u002Fuploads\u002F72c67ef3-bc86-4b64-b0df-0aba71065050.png","时过境迁",{"slug":60,"title":61,"excerpt":62,"date":63,"updatedAt":64,"tags":65,"content":67,"series":68,"seriesIndex":69,"seriesNav":70,"prev":72,"next":73},"sou-suo-mian-ban-ctrl-k-he-shu","搜索面板：Ctrl+K 和输入框防抖","全局快捷键要判断输入焦点、Esc 关闭、上下键移动选中项。无障碍那部分我补了第二轮才加上。","2026-03-15","2026-10-17",[66],"Vue","一个命令面板式的搜索：按 Ctrl+K 唤起，输入即时出结果，上下键选，回车跳转，Esc 关闭。\n\n全局快捷键要处理三件事。\n\n监听在 window 上的 keydown，判断 e.key === 'k' && (e.metaKey || e.ctrlKey)。mac 是 Cmd、Windows 是 Ctrl，两个都要。\n\n第二件是别在输入框里抢键。用户在 textarea 里按 Ctrl+K 也许是想搜索，但按 Esc 一定是想退出输入。我加了一个判断：焦点在 input\u002Ftextarea 时不劫持方向键。\n\n第三件是卸载时解绑。组件卸载没清理监听，切两个页面之后按一次 Ctrl+K 弹两个面板。这类错误我在定时器那篇里写过，事件监听是完全一样的问题。\n\n输入防抖用了 200ms。这里有一个比防抖更影响体验的东西：请求竞态。\n\n```ts\nlet seq = 0\nasync function search(q: string) {\n  const my = ++seq\n  const res = await api.get('\u002Fsearch', { q })\n  if (my !== seq) return      \u002F\u002F 已经有更新的请求在跑，丢掉这个\n  results.value = res\n}\n```\n\n输入 \"vue\" 时先后发了 v、vu、vue 三个请求，返回顺序不保证。慢的那个后回来会把新结果覆盖掉，界面显示的是 \"v\" 的匹配。用一个自增序号，只认最后一次。\n\n键盘导航要维护一个 selectedIndex，输入变化时重置为 0，方向键加减并取模。滚动到可视区用 el.scrollIntoView({ block: 'nearest' })，不写 nearest 的话会把整个页面顶到中间。\n\n最后是无障碍：面板要 role=\"dialog\"、aria-modal，打开时焦点移进去、关闭时还给触发它的元素。这部分我第二版才补，第一版键盘能操作但读屏软件什么都念不出来。","Vue3 边用边学",8,[71],{"slug":60,"title":61,"seriesIndex":69},null,{"slug":74,"title":75},"yao-bu-yao-wei-xi-lie-dan-du","要不要为系列单独建一张表",[77,86,92,98,103],{"slug":78,"title":79,"excerpt":80,"date":81,"tags":82},"gang-deng-lu-jiu-guo-qi-yi-ci","一个 401，查出四个前端机制：缓存回放、隐藏页的 rAF、百分比圆角、缓动过冲","响应头里的 token 被 304 原样回放，把刚签发的新 token 顶掉，表现成刚登录就过期。顺带记三个前端细节：隐藏标签页里 rAF 不执行、扁盒子的 50% 圆角是胶囊、缓动曲线的过冲只有 0.0008。","2026-10-13",[83,84,66,85],"CSS","JavaScript","浏览器",{"slug":87,"title":88,"excerpt":89,"date":90,"tags":91},"biao-ge-cha-cao-li-de-row-shi","表格插槽里的 row 是宽类型：TS7053","组件库插槽给的 row 是 Record\u003Cstring, any> 之类，取字段时报错的是索引那一行。枚举映射写成按字段值的函数最省事。","2026-04-10",[10,66],{"slug":93,"title":94,"excerpt":95,"date":96,"tags":97},"dian-xiang-mu-ye-mian-jiu-si-yi","点「项目」页面就死：一次客户端无限重定向","为了保留旧地址大小写加的一条跳转规则，被注册成了客户端路由；路由匹配不区分大小写，于是它自己跳自己。","2026-10-02",[12,85],{"slug":99,"title":100,"excerpt":101,"date":96,"tags":102},"cong-da-bao-shang-chuan-dao-push-ji","从打包上传到 push 即上线：把 CD 接通","私有仓库的镜像包默认也是私有的，健康检查的探测地址在宿主机有 nginx 时会打错地方。两处都是配好之后才验出来的。",[30,15],{"slug":104,"title":105,"excerpt":106,"date":107,"tags":108},"shang-xian-na-tian-yu-ming-zheng-shu","上线那天：域名、证书，和一条我打错的 IP","把 69 打成 96，于是照着\"安全组没放行\"这条错线索查了一整晚。分层排查的前提是先确认敲的是哪扇门。","2026-10-01",[109],"随笔"]