[{"data":1,"prerenderedAt":1643},["ShallowReactive",2],{"article-/topics/design/storybook-7-complete-practice":3,"related-design":404,"content-query-Eb6idhTCfy":1346},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":12,"image":18,"imageQuery":19,"pexelsPhotoId":20,"pexelsUrl":21,"featured":6,"readingTime":22,"body":23,"_type":398,"_id":399,"_source":400,"_file":401,"_stem":402,"_extension":403},"/topics/design/storybook-7-complete-practice","design",false,"","Storybook 7 完整实践：从组件展示工具走向团队协作基础设施","Storybook 7 的价值不只是预览组件，而是把组件文档、交互测试、视觉回归和设计协作放进同一条工作流。本文从落地边界与失败案例出发，讲清 Storybook 7 的完整实践方法。","2026-04-25","HTMLPAGE 团队",[13,14,15,16,17],"Storybook","Design System","Component Documentation","Frontend Engineering","UI Testing","/images/topics/design/storybook-7-complete-practice.jpg","storybook ui component documentation on laptop screen",907487,"https://www.pexels.com/photo/gray-laptop-computer-showing-calculator-application-with-codes-907487/",15,{"type":24,"children":25,"toc":387},"root",[26,34,39,44,69,74,81,86,91,114,119,125,130,148,153,158,181,186,192,197,202,225,230,236,241,246,264,269,275,280,303,308,313,341,346,351,356],{"type":27,"tag":28,"props":29,"children":30},"element","p",{},[31],{"type":32,"value":33},"text","很多团队第一次接触 Storybook 时，都会把它理解成一个“组件预览工具”。",{"type":27,"tag":28,"props":35,"children":36},{},[37],{"type":32,"value":38},"这个理解不算错，但如果只停留在这里，Storybook 的价值会被明显低估。",{"type":27,"tag":28,"props":40,"children":41},{},[42],{"type":32,"value":43},"在真实团队里，组件开发真正缺的往往不是一个展示页，而是这些能力：",{"type":27,"tag":45,"props":46,"children":47},"ul",{},[48,54,59,64],{"type":27,"tag":49,"props":50,"children":51},"li",{},[52],{"type":32,"value":53},"组件状态能不能被稳定复现",{"type":27,"tag":49,"props":55,"children":56},{},[57],{"type":32,"value":58},"文档和代码能不能一起更新",{"type":27,"tag":49,"props":60,"children":61},{},[62],{"type":32,"value":63},"视觉变更能不能更早暴露",{"type":27,"tag":49,"props":65,"children":66},{},[67],{"type":32,"value":68},"设计、开发、测试能不能共享同一套组件语义",{"type":27,"tag":28,"props":70,"children":71},{},[72],{"type":32,"value":73},"所以 Storybook 7 更适合被看成组件协作基础设施，而不是单纯的开发辅助工具。",{"type":27,"tag":75,"props":76,"children":78},"h2",{"id":77},"storybook-7-先解决的是组件可见性",[79],{"type":32,"value":80},"Storybook 7 先解决的是“组件可见性”",{"type":27,"tag":28,"props":82,"children":83},{},[84],{"type":32,"value":85},"很多组件库之所以越来越难维护，不是组件本身写得太差，而是团队看不清组件到底有哪些状态、边界和依赖。",{"type":27,"tag":28,"props":87,"children":88},{},[89],{"type":32,"value":90},"组件一旦离开真实业务页面，就更容易被稳定观察：",{"type":27,"tag":45,"props":92,"children":93},{},[94,99,104,109],{"type":27,"tag":49,"props":95,"children":96},{},[97],{"type":32,"value":98},"空态和异常态有没有被覆盖",{"type":27,"tag":49,"props":100,"children":101},{},[102],{"type":32,"value":103},"不同 props 组合会不会破版",{"type":27,"tag":49,"props":105,"children":106},{},[107],{"type":32,"value":108},"交互状态是否前后一致",{"type":27,"tag":49,"props":110,"children":111},{},[112],{"type":32,"value":113},"文档和示例是否仍然有效",{"type":27,"tag":28,"props":115,"children":116},{},[117],{"type":32,"value":118},"Storybook 的第一层价值，就是把组件从业务上下文中抽离出来，建立一个可独立理解的视图层。",{"type":27,"tag":75,"props":120,"children":122},{"id":121},"storybook-7-的重点不是写更多-stories而是建立稳定的组件语义",[123],{"type":32,"value":124},"Storybook 7 的重点不是写更多 stories，而是建立稳定的组件语义",{"type":27,"tag":28,"props":126,"children":127},{},[128],{"type":32,"value":129},"不少团队接入 Storybook 后，很快就会陷入一个误区：",{"type":27,"tag":45,"props":131,"children":132},{},[133,138,143],{"type":27,"tag":49,"props":134,"children":135},{},[136],{"type":32,"value":137},"每个组件写几条 stories",{"type":27,"tag":49,"props":139,"children":140},{},[141],{"type":32,"value":142},"页面能打开",{"type":27,"tag":49,"props":144,"children":145},{},[146],{"type":32,"value":147},"这件事就算完成了",{"type":27,"tag":28,"props":149,"children":150},{},[151],{"type":32,"value":152},"但如果 stories 只是机械列举状态，而没有清晰表达组件语义，后续维护仍然会很松散。",{"type":27,"tag":28,"props":154,"children":155},{},[156],{"type":32,"value":157},"更稳的方式通常是按组件任务来组织 stories：",{"type":27,"tag":45,"props":159,"children":160},{},[161,166,171,176],{"type":27,"tag":49,"props":162,"children":163},{},[164],{"type":32,"value":165},"基础状态",{"type":27,"tag":49,"props":167,"children":168},{},[169],{"type":32,"value":170},"关键边界",{"type":27,"tag":49,"props":172,"children":173},{},[174],{"type":32,"value":175},"高风险交互",{"type":27,"tag":49,"props":177,"children":178},{},[179],{"type":32,"value":180},"典型业务场景",{"type":27,"tag":28,"props":182,"children":183},{},[184],{"type":32,"value":185},"这样 stories 才会真正变成组件的可执行文档，而不是一组散乱样例。",{"type":27,"tag":75,"props":187,"children":189},{"id":188},"文档交互测试和视觉回归要放进同一条链路",[190],{"type":32,"value":191},"文档、交互测试和视觉回归要放进同一条链路",{"type":27,"tag":28,"props":193,"children":194},{},[195],{"type":32,"value":196},"Storybook 7 真正适合团队落地的地方，在于它不只是展示层。",{"type":27,"tag":28,"props":198,"children":199},{},[200],{"type":32,"value":201},"如果只把 Storybook 当“文档站”，价值有限；如果把它和测试、回归、设计协作连起来，收益会明显变大：",{"type":27,"tag":45,"props":203,"children":204},{},[205,210,215,220],{"type":27,"tag":49,"props":206,"children":207},{},[208],{"type":32,"value":209},"文档和组件状态同步",{"type":27,"tag":49,"props":211,"children":212},{},[213],{"type":32,"value":214},"交互测试覆盖关键路径",{"type":27,"tag":49,"props":216,"children":217},{},[218],{"type":32,"value":219},"视觉回归在 PR 阶段暴露问题",{"type":27,"tag":49,"props":221,"children":222},{},[223],{"type":32,"value":224},"设计 review 不再完全依赖业务页面截图",{"type":27,"tag":28,"props":226,"children":227},{},[228],{"type":32,"value":229},"这条链路一旦跑起来，Storybook 才会从工具升级为团队机制。",{"type":27,"tag":75,"props":231,"children":233},{"id":232},"组件上下文注入要克制否则-storybook-会逐渐失真",[234],{"type":32,"value":235},"组件上下文注入要克制，否则 Storybook 会逐渐失真",{"type":27,"tag":28,"props":237,"children":238},{},[239],{"type":32,"value":240},"Storybook 落地时另一个高频问题，是为了让组件“看起来正常”，不断往 preview 或 decorator 里塞各种全局上下文。",{"type":27,"tag":28,"props":242,"children":243},{},[244],{"type":32,"value":245},"短期能跑，长期风险很大：",{"type":27,"tag":45,"props":247,"children":248},{},[249,254,259],{"type":27,"tag":49,"props":250,"children":251},{},[252],{"type":32,"value":253},"组件真实依赖变得不清楚",{"type":27,"tag":49,"props":255,"children":256},{},[257],{"type":32,"value":258},"stories 运行环境越来越重",{"type":27,"tag":49,"props":260,"children":261},{},[262],{"type":32,"value":263},"某个全局 provider 改动会影响大量 stories",{"type":27,"tag":28,"props":265,"children":266},{},[267],{"type":32,"value":268},"所以 Storybook 环境要尽量只保留必要依赖，把业务复杂度控制在最小边界内。",{"type":27,"tag":75,"props":270,"children":272},{"id":271},"一个常见失败案例接了-storybook但团队半年后几乎不再维护",[273],{"type":32,"value":274},"一个常见失败案例：接了 Storybook，但团队半年后几乎不再维护",{"type":27,"tag":28,"props":276,"children":277},{},[278],{"type":32,"value":279},"这类情况通常不是因为 Storybook 不好用，而是接入姿势有问题：",{"type":27,"tag":45,"props":281,"children":282},{},[283,288,293,298],{"type":27,"tag":49,"props":284,"children":285},{},[286],{"type":32,"value":287},"stories 缺少治理标准",{"type":27,"tag":49,"props":289,"children":290},{},[291],{"type":32,"value":292},"文档没人维护",{"type":27,"tag":49,"props":294,"children":295},{},[296],{"type":32,"value":297},"视觉回归和测试没有接入流程",{"type":27,"tag":49,"props":299,"children":300},{},[301],{"type":32,"value":302},"业务组件和基础组件混在一起，边界越来越模糊",{"type":27,"tag":28,"props":304,"children":305},{},[306],{"type":32,"value":307},"结果就是工具还在，但团队不会再信任它。",{"type":27,"tag":75,"props":309,"children":311},{"id":310},"一份可直接复用的检查清单",[312],{"type":32,"value":310},{"type":27,"tag":45,"props":314,"children":315},{},[316,321,326,331,336],{"type":27,"tag":49,"props":317,"children":318},{},[319],{"type":32,"value":320},"Storybook 是否先承担了组件可见性与状态复现职责",{"type":27,"tag":49,"props":322,"children":323},{},[324],{"type":32,"value":325},"stories 是否围绕组件任务和边界组织，而不是机械罗列",{"type":27,"tag":49,"props":327,"children":328},{},[329],{"type":32,"value":330},"文档、交互测试和视觉回归是否形成同一条工作流",{"type":27,"tag":49,"props":332,"children":333},{},[334],{"type":32,"value":335},"preview 与 decorator 是否保持最小必要上下文",{"type":27,"tag":49,"props":337,"children":338},{},[339],{"type":32,"value":340},"是否建立了 stories 维护标准与回归机制",{"type":27,"tag":75,"props":342,"children":344},{"id":343},"总结",[345],{"type":32,"value":343},{"type":27,"tag":28,"props":347,"children":348},{},[349],{"type":32,"value":350},"Storybook 7 的真正价值，不在多一个组件展示站，而在把组件语义、文档、测试和协作放进同一条可维护链路。只要先把 stories 结构、上下文边界和回归流程设计好，Storybook 就会成为设计系统的长期基础设施。",{"type":27,"tag":28,"props":352,"children":353},{},[354],{"type":32,"value":355},"进一步阅读：",{"type":27,"tag":45,"props":357,"children":358},{},[359,369,378],{"type":27,"tag":49,"props":360,"children":361},{},[362],{"type":27,"tag":363,"props":364,"children":366},"a",{"href":365},"/topics/frontend/enterprise-component-library-complete-process",[367],{"type":32,"value":368},"构建企业级组件库完整流程",{"type":27,"tag":49,"props":370,"children":371},{},[372],{"type":27,"tag":363,"props":373,"children":375},{"href":374},"/topics/design/design-system-documentation-best-practices",[376],{"type":32,"value":377},"设计系统文档化最佳实践",{"type":27,"tag":49,"props":379,"children":380},{},[381],{"type":27,"tag":363,"props":382,"children":384},{"href":383},"/topics/next/nextjs-component-library-integration-best-practices",[385],{"type":32,"value":386},"组件库集成最佳实践",{"title":7,"searchDepth":388,"depth":388,"links":389},3,[390,392,393,394,395,396,397],{"id":77,"depth":391,"text":80},2,{"id":121,"depth":391,"text":124},{"id":188,"depth":391,"text":191},{"id":232,"depth":391,"text":235},{"id":271,"depth":391,"text":274},{"id":310,"depth":391,"text":310},{"id":343,"depth":391,"text":343},"markdown","content:topics:design:storybook-7-complete-practice.md","content","topics/design/storybook-7-complete-practice.md","topics/design/storybook-7-complete-practice","md",[405,765,1067],{"_path":406,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":407,"description":408,"keywords":409,"image":415,"author":11,"date":416,"readingTime":417,"topic":5,"body":418,"_type":398,"_id":762,"_source":400,"_file":763,"_stem":764,"_extension":403},"/topics/design/button-component-design","按钮组件设计详解","学习按钮样式、交互状态、无障碍性和最佳实践",[410,411,412,413,414],"按钮设计","Button Component","交互状态","UI 组件","用户体验","/images/topics/button-design.jpg","2025-12-08",18,{"type":24,"children":419,"toc":744},[420,424,429,434,441,454,460,469,475,484,488,494,505,511,520,526,535,540,549,554,565,570,579,584,597,631,642,685,690],{"type":27,"tag":75,"props":421,"children":422},{"id":407},[423],{"type":32,"value":407},{"type":27,"tag":28,"props":425,"children":426},{},[427],{"type":32,"value":428},"按钮是 UI 中最重要的交互元素。优秀的按钮设计能够指导用户行为。",{"type":27,"tag":75,"props":430,"children":432},{"id":431},"按钮类型",[433],{"type":32,"value":431},{"type":27,"tag":435,"props":436,"children":438},"h3",{"id":437},"primary-button主按钮",[439],{"type":32,"value":440},"Primary Button（主按钮）",{"type":27,"tag":442,"props":443,"children":448},"pre",{"className":444,"code":446,"language":447,"meta":7},[445],"language-css",".btn-primary {\n  background-color: #0066cc;\n  color: #ffffff;\n  padding: 12px 24px;\n  border: none;\n  border-radius: 4px;\n  font-weight: 600;\n  font-size: 16px;\n  cursor: pointer;\n  transition: all 0.2s ease;\n}\n\n.btn-primary:hover {\n  background-color: #0052a3;\n  box-shadow: 0 4px 12px rgba(0, 102, 204, 0.2);\n}\n\n.btn-primary:active {\n  background-color: #003d7a;\n  transform: scale(0.98);\n}\n\n.btn-primary:disabled {\n  background-color: #cccccc;\n  cursor: not-allowed;\n  opacity: 0.6;\n}\n","css",[449],{"type":27,"tag":450,"props":451,"children":452},"code",{"__ignoreMap":7},[453],{"type":32,"value":446},{"type":27,"tag":435,"props":455,"children":457},{"id":456},"secondary-button次按钮",[458],{"type":32,"value":459},"Secondary Button（次按钮）",{"type":27,"tag":442,"props":461,"children":464},{"className":462,"code":463,"language":447,"meta":7},[445],".btn-secondary {\n  background-color: transparent;\n  color: #0066cc;\n  border: 2px solid #0066cc;\n  padding: 10px 22px;\n  border-radius: 4px;\n  font-weight: 600;\n  cursor: pointer;\n  transition: all 0.2s;\n}\n\n.btn-secondary:hover {\n  background-color: rgba(0, 102, 204, 0.1);\n}\n\n.btn-secondary:active {\n  background-color: rgba(0, 102, 204, 0.2);\n}\n",[465],{"type":27,"tag":450,"props":466,"children":467},{"__ignoreMap":7},[468],{"type":32,"value":463},{"type":27,"tag":435,"props":470,"children":472},{"id":471},"danger-button危险按钮",[473],{"type":32,"value":474},"Danger Button（危险按钮）",{"type":27,"tag":442,"props":476,"children":479},{"className":477,"code":478,"language":447,"meta":7},[445],".btn-danger {\n  background-color: #cc0000;\n  color: #ffffff;\n  padding: 12px 24px;\n  border-radius: 4px;\n  cursor: pointer;\n  font-weight: 600;\n}\n\n.btn-danger:hover {\n  background-color: #990000;\n  box-shadow: 0 4px 12px rgba(204, 0, 0, 0.2);\n}\n",[480],{"type":27,"tag":450,"props":481,"children":482},{"__ignoreMap":7},[483],{"type":32,"value":478},{"type":27,"tag":75,"props":485,"children":486},{"id":412},[487],{"type":32,"value":412},{"type":27,"tag":435,"props":489,"children":491},{"id":490},"loading-状态",[492],{"type":32,"value":493},"Loading 状态",{"type":27,"tag":442,"props":495,"children":500},{"className":496,"code":498,"language":499,"meta":7},[497],"language-jsx","import { useState } from 'react';\n\nfunction Button({ children, onClick, loading, ...props }) {\n  const [isLoading, setIsLoading] = useState(false);\n  \n  const handleClick = async () => {\n    setIsLoading(true);\n    try {\n      await onClick();\n    } finally {\n      setIsLoading(false);\n    }\n  };\n  \n  return (\n    \u003Cbutton\n      onClick={handleClick}\n      disabled={isLoading || loading}\n      aria-busy={isLoading || loading}\n      {...props}\n    >\n      {isLoading ? (\n        \u003C>\n          \u003Cspan className=\"spinner\" aria-hidden=\"true\">\u003C/span>\n          {children}\n        \u003C/>\n      ) : (\n        children\n      )}\n    \u003C/button>\n  );\n}\n","jsx",[501],{"type":27,"tag":450,"props":502,"children":503},{"__ignoreMap":7},[504],{"type":32,"value":498},{"type":27,"tag":435,"props":506,"children":508},{"id":507},"disabled-状态",[509],{"type":32,"value":510},"Disabled 状态",{"type":27,"tag":442,"props":512,"children":515},{"className":513,"code":514,"language":447,"meta":7},[445],".btn:disabled {\n  opacity: 0.5;\n  cursor: not-allowed;\n  background-color: #cccccc;\n  color: #999999;\n}\n\n/* 禁用状态下隐藏指针光标 */\n.btn:disabled:hover {\n  box-shadow: none;\n  transform: none;\n}\n",[516],{"type":27,"tag":450,"props":517,"children":518},{"__ignoreMap":7},[519],{"type":32,"value":514},{"type":27,"tag":435,"props":521,"children":523},{"id":522},"focus-状态",[524],{"type":32,"value":525},"Focus 状态",{"type":27,"tag":442,"props":527,"children":530},{"className":528,"code":529,"language":447,"meta":7},[445],".btn:focus {\n  outline: none;\n  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1),\n              0 0 0 5px #0066cc;\n}\n\n/* 键盘导航焦点 */\n.btn:focus-visible {\n  outline: 2px solid #0066cc;\n  outline-offset: 2px;\n}\n",[531],{"type":27,"tag":450,"props":532,"children":533},{"__ignoreMap":7},[534],{"type":32,"value":529},{"type":27,"tag":75,"props":536,"children":538},{"id":537},"按钮大小",[539],{"type":32,"value":537},{"type":27,"tag":442,"props":541,"children":544},{"className":542,"code":543,"language":447,"meta":7},[445],"/* 小按钮 */\n.btn-sm {\n  padding: 6px 12px;\n  font-size: 12px;\n  min-height: 32px;\n  min-width: 32px;\n}\n\n/* 中等按钮（默认） */\n.btn-md {\n  padding: 12px 24px;\n  font-size: 16px;\n  min-height: 44px;\n  min-width: 44px;\n}\n\n/* 大按钮 */\n.btn-lg {\n  padding: 16px 32px;\n  font-size: 18px;\n  min-height: 56px;\n  min-width: 56px;\n}\n\n/* 全宽按钮 */\n.btn-block {\n  width: 100%;\n  display: block;\n}\n",[545],{"type":27,"tag":450,"props":546,"children":547},{"__ignoreMap":7},[548],{"type":32,"value":543},{"type":27,"tag":75,"props":550,"children":552},{"id":551},"无障碍性",[553],{"type":32,"value":551},{"type":27,"tag":442,"props":555,"children":560},{"className":556,"code":558,"language":559,"meta":7},[557],"language-html","\u003C!-- 语义正确 -->\n\u003Cbutton type=\"submit\" aria-label=\"提交表单\">\n  提交\n\u003C/button>\n\n\u003C!-- 加载状态 -->\n\u003Cbutton aria-busy=\"true\" disabled>\n  \u003Cspan aria-hidden=\"true\" class=\"spinner\">\u003C/span>\n  加载中...\n\u003C/button>\n\n\u003C!-- 图标按钮 -->\n\u003Cbutton aria-label=\"关闭\">\n  \u003Csvg aria-hidden=\"true\" width=\"24\" height=\"24\">\n    \u003Cline x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n    \u003Cline x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n  \u003C/svg>\n\u003C/button>\n\n\u003C!-- 切换按钮 -->\n\u003Cbutton aria-pressed=\"false\" aria-label=\"点赞\">\n  ♥\n\u003C/button>\n","html",[561],{"type":27,"tag":450,"props":562,"children":563},{"__ignoreMap":7},[564],{"type":32,"value":558},{"type":27,"tag":75,"props":566,"children":568},{"id":567},"完整组件示例",[569],{"type":32,"value":567},{"type":27,"tag":442,"props":571,"children":574},{"className":572,"code":573,"language":499,"meta":7},[497],"const Button = React.forwardRef((\n  {\n    children,\n    variant = 'primary',\n    size = 'md',\n    loading = false,\n    disabled = false,\n    icon,\n    className,\n    ...props\n  },\n  ref\n) => {\n  return (\n    \u003Cbutton\n      ref={ref}\n      className={`btn btn-${variant} btn-${size} ${className}`}\n      disabled={disabled || loading}\n      aria-busy={loading}\n      {...props}\n    >\n      {icon && \u003Cspan className=\"btn-icon\" aria-hidden=\"true\">{icon}\u003C/span>}\n      {loading ? (\n        \u003C>\n          \u003Cspan className=\"spinner\" aria-hidden=\"true\">\u003C/span>\n          {children}\n        \u003C/>\n      ) : (\n        children\n      )}\n    \u003C/button>\n  );\n});\n\nButton.displayName = 'Button';\n",[575],{"type":27,"tag":450,"props":576,"children":577},{"__ignoreMap":7},[578],{"type":32,"value":573},{"type":27,"tag":75,"props":580,"children":582},{"id":581},"最佳实践",[583],{"type":32,"value":581},{"type":27,"tag":28,"props":585,"children":586},{},[587,589,595],{"type":32,"value":588},"✅ ",{"type":27,"tag":590,"props":591,"children":592},"strong",{},[593],{"type":32,"value":594},"应该做的事",{"type":32,"value":596},":",{"type":27,"tag":45,"props":598,"children":599},{},[600,605,610,621,626],{"type":27,"tag":49,"props":601,"children":602},{},[603],{"type":32,"value":604},"最小触摸目标 44x44px",{"type":27,"tag":49,"props":606,"children":607},{},[608],{"type":32,"value":609},"清晰的视觉反馈",{"type":27,"tag":49,"props":611,"children":612},{},[613,615],{"type":32,"value":614},"使用语义 HTML ",{"type":27,"tag":450,"props":616,"children":618},{"className":617},[],[619],{"type":32,"value":620},"\u003Cbutton>",{"type":27,"tag":49,"props":622,"children":623},{},[624],{"type":32,"value":625},"提供加载状态反馈",{"type":27,"tag":49,"props":627,"children":628},{},[629],{"type":32,"value":630},"支持键盘导航",{"type":27,"tag":28,"props":632,"children":633},{},[634,636,641],{"type":32,"value":635},"❌ ",{"type":27,"tag":590,"props":637,"children":638},{},[639],{"type":32,"value":640},"不应该做的事",{"type":32,"value":596},{"type":27,"tag":45,"props":643,"children":644},{},[645,658,663,668,673],{"type":27,"tag":49,"props":646,"children":647},{},[648,650,656],{"type":32,"value":649},"使用 ",{"type":27,"tag":450,"props":651,"children":653},{"className":652},[],[654],{"type":32,"value":655},"\u003Cdiv>",{"type":32,"value":657}," 模拟按钮",{"type":27,"tag":49,"props":659,"children":660},{},[661],{"type":32,"value":662},"隐藏焦点指示器",{"type":27,"tag":49,"props":664,"children":665},{},[666],{"type":32,"value":667},"过多的按钮样式",{"type":27,"tag":49,"props":669,"children":670},{},[671],{"type":32,"value":672},"忽视禁用状态",{"type":27,"tag":49,"props":674,"children":675},{},[676,677,683],{"type":32,"value":649},{"type":27,"tag":450,"props":678,"children":680},{"className":679},[],[681],{"type":32,"value":682},"\u003Ca>",{"type":32,"value":684}," 代替按钮",{"type":27,"tag":75,"props":686,"children":688},{"id":687},"测试清单",[689],{"type":32,"value":687},{"type":27,"tag":45,"props":691,"children":694},{"className":692},[693],"contains-task-list",[695,708,717,726,735],{"type":27,"tag":49,"props":696,"children":699},{"className":697},[698],"task-list-item",[700,706],{"type":27,"tag":701,"props":702,"children":705},"input",{"disabled":703,"type":704},true,"checkbox",[],{"type":32,"value":707}," 在各种浏览器中测试",{"type":27,"tag":49,"props":709,"children":711},{"className":710},[698],[712,715],{"type":27,"tag":701,"props":713,"children":714},{"disabled":703,"type":704},[],{"type":32,"value":716}," 验证键盘导航",{"type":27,"tag":49,"props":718,"children":720},{"className":719},[698],[721,724],{"type":27,"tag":701,"props":722,"children":723},{"disabled":703,"type":704},[],{"type":32,"value":725}," 检查色彩对比度",{"type":27,"tag":49,"props":727,"children":729},{"className":728},[698],[730,733],{"type":27,"tag":701,"props":731,"children":732},{"disabled":703,"type":704},[],{"type":32,"value":734}," 测试触摸设备",{"type":27,"tag":49,"props":736,"children":738},{"className":737},[698],[739,742],{"type":27,"tag":701,"props":740,"children":741},{"disabled":703,"type":704},[],{"type":32,"value":743}," 屏幕阅读器兼容性",{"title":7,"searchDepth":388,"depth":388,"links":745},[746,747,752,757,758,759,760,761],{"id":407,"depth":391,"text":407},{"id":431,"depth":391,"text":431,"children":748},[749,750,751],{"id":437,"depth":388,"text":440},{"id":456,"depth":388,"text":459},{"id":471,"depth":388,"text":474},{"id":412,"depth":391,"text":412,"children":753},[754,755,756],{"id":490,"depth":388,"text":493},{"id":507,"depth":388,"text":510},{"id":522,"depth":388,"text":525},{"id":537,"depth":391,"text":537},{"id":551,"depth":391,"text":551},{"id":567,"depth":391,"text":567},{"id":581,"depth":391,"text":581},{"id":687,"depth":391,"text":687},"content:topics:design:button-component-design.md","topics/design/button-component-design.md","topics/design/button-component-design",{"_path":766,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":767,"description":768,"keywords":769,"image":774,"author":11,"date":416,"readingTime":775,"topic":5,"body":776,"_type":398,"_id":1064,"_source":400,"_file":1065,"_stem":1066,"_extension":403},"/topics/design/dark-mode-design","暗黑模式设计完整方案","学习暗黑模式实现、色彩方案、对比度管理和最佳实践",[770,771,772,773,414],"暗黑模式","Dark Mode","色彩系统","CSS 变量","/images/topics/dark-mode-design.jpg",20,{"type":24,"children":777,"toc":1047},[778,782,787,792,798,807,813,822,827,833,842,848,859,865,874,879,888,893,902,907,916,920,929,957,966,994,998],{"type":27,"tag":75,"props":779,"children":780},{"id":767},[781],{"type":32,"value":767},{"type":27,"tag":28,"props":783,"children":784},{},[785],{"type":32,"value":786},"暗黑模式已成为现代应用的标准功能。它能够减少眼睛疲劳、节省电池、改善用户体验。",{"type":27,"tag":75,"props":788,"children":790},{"id":789},"核心色彩系统",[791],{"type":32,"value":789},{"type":27,"tag":435,"props":793,"children":795},{"id":794},"light-mode-配色",[796],{"type":32,"value":797},"Light Mode 配色",{"type":27,"tag":442,"props":799,"children":802},{"className":800,"code":801,"language":447,"meta":7},[445],":root {\n  /* Light Mode */\n  --bg-primary: #ffffff;\n  --bg-secondary: #f5f5f5;\n  --bg-tertiary: #efefef;\n  \n  --text-primary: #1a1a1a;\n  --text-secondary: #666666;\n  --text-tertiary: #999999;\n  \n  --border-color: #e0e0e0;\n  --divider-color: #f0f0f0;\n}\n",[803],{"type":27,"tag":450,"props":804,"children":805},{"__ignoreMap":7},[806],{"type":32,"value":801},{"type":27,"tag":435,"props":808,"children":810},{"id":809},"dark-mode-配色",[811],{"type":32,"value":812},"Dark Mode 配色",{"type":27,"tag":442,"props":814,"children":817},{"className":815,"code":816,"language":447,"meta":7},[445],"@media (prefers-color-scheme: dark) {\n  :root {\n    /* Dark Mode */\n    --bg-primary: #1a1a1a;\n    --bg-secondary: #2d2d2d;\n    --bg-tertiary: #3a3a3a;\n    \n    --text-primary: #ffffff;\n    --text-secondary: #e0e0e0;\n    --text-tertiary: #a0a0a0;\n    \n    --border-color: #404040;\n    --divider-color: #2a2a2a;\n  }\n}\n",[818],{"type":27,"tag":450,"props":819,"children":820},{"__ignoreMap":7},[821],{"type":32,"value":816},{"type":27,"tag":75,"props":823,"children":825},{"id":824},"实现方案",[826],{"type":32,"value":824},{"type":27,"tag":435,"props":828,"children":830},{"id":829},"方案-1prefers-color-scheme",[831],{"type":32,"value":832},"方案 1：prefers-color-scheme",{"type":27,"tag":442,"props":834,"children":837},{"className":835,"code":836,"language":447,"meta":7},[445],"/* 自动跟随系统设置 */\n@media (prefers-color-scheme: light) {\n  :root {\n    --bg: #fff;\n    --text: #000;\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg: #1a1a1a;\n    --text: #fff;\n  }\n}\n\nbody {\n  background: var(--bg);\n  color: var(--text);\n}\n",[838],{"type":27,"tag":450,"props":839,"children":840},{"__ignoreMap":7},[841],{"type":32,"value":836},{"type":27,"tag":435,"props":843,"children":845},{"id":844},"方案-2javascript-切换",[846],{"type":32,"value":847},"方案 2：JavaScript 切换",{"type":27,"tag":442,"props":849,"children":854},{"className":850,"code":852,"language":853,"meta":7},[851],"language-javascript","// 检测和切换暗黑模式\nfunction initDarkMode() {\n  const isDark = localStorage.getItem('darkMode') === 'true' ||\n                 window.matchMedia('(prefers-color-scheme: dark)').matches;\n  \n  if (isDark) {\n    document.documentElement.setAttribute('data-theme', 'dark');\n  }\n}\n\nfunction toggleDarkMode() {\n  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';\n  const newTheme = isDark ? 'light' : 'dark';\n  \n  document.documentElement.setAttribute('data-theme', newTheme);\n  localStorage.setItem('darkMode', newTheme === 'dark');\n}\n\n// CSS 应用\nhtml[data-theme='light'] {\n  color-scheme: light;\n}\n\nhtml[data-theme='dark'] {\n  color-scheme: dark;\n}\n","javascript",[855],{"type":27,"tag":450,"props":856,"children":857},{"__ignoreMap":7},[858],{"type":32,"value":852},{"type":27,"tag":435,"props":860,"children":862},{"id":861},"方案-3css-variables-javascript",[863],{"type":32,"value":864},"方案 3：CSS Variables + JavaScript",{"type":27,"tag":442,"props":866,"children":869},{"className":867,"code":868,"language":853,"meta":7},[851],"const themes = {\n  light: {\n    '--bg-primary': '#ffffff',\n    '--text-primary': '#000000',\n    '--accent': '#0066cc',\n    '--border': '#e0e0e0',\n  },\n  dark: {\n    '--bg-primary': '#1a1a1a',\n    '--text-primary': '#ffffff',\n    '--accent': '#4da3ff',\n    '--border': '#404040',\n  },\n};\n\nfunction applyTheme(themeName) {\n  const theme = themes[themeName];\n  Object.entries(theme).forEach(([key, value]) => {\n    document.documentElement.style.setProperty(key, value);\n  });\n  localStorage.setItem('theme', themeName);\n}\n",[870],{"type":27,"tag":450,"props":871,"children":872},{"__ignoreMap":7},[873],{"type":32,"value":868},{"type":27,"tag":75,"props":875,"children":877},{"id":876},"对比度管理",[878],{"type":32,"value":876},{"type":27,"tag":442,"props":880,"children":883},{"className":881,"code":882,"language":447,"meta":7},[445],"/* Light Mode 对比度 */\n:root {\n  --contrast-high: #000000;     /* 21:1 */\n  --contrast-medium: #333333;   /* 12.6:1 */\n  --contrast-low: #666666;      /* 5.1:1 */\n}\n\n/* Dark Mode 对比度 */\n@media (prefers-color-scheme: dark) {\n  :root {\n    --contrast-high: #ffffff;    /* 21:1 */\n    --contrast-medium: #e0e0e0;  /* 11.6:1 */\n    --contrast-low: #a0a0a0;     /* 4.5:1 */\n  }\n}\n\n/* 应用对比度 */\n.text-primary { color: var(--contrast-high); }\n.text-secondary { color: var(--contrast-medium); }\n.text-tertiary { color: var(--contrast-low); }\n",[884],{"type":27,"tag":450,"props":885,"children":886},{"__ignoreMap":7},[887],{"type":32,"value":882},{"type":27,"tag":75,"props":889,"children":891},{"id":890},"图片和图表处理",[892],{"type":32,"value":890},{"type":27,"tag":442,"props":894,"children":897},{"className":895,"code":896,"language":559,"meta":7},[557],"\u003C!-- 针对不同主题的图片 -->\n\u003Cpicture>\n  \u003Csource \n    media=\"(prefers-color-scheme: dark)\" \n    srcset=\"chart-dark.svg\"\n  />\n  \u003Cimg src=\"chart-light.svg\" alt=\"图表\" />\n\u003C/picture>\n\n\u003C!-- SVG 颜色适配 -->\n\u003Csvg class=\"icon\">\n  \u003Ccircle cx=\"50\" cy=\"50\" r=\"40\" fill=\"currentColor\" />\n\u003C/svg>\n\n\u003Cstyle>\n  .icon {\n    color: var(--text-primary);\n  }\n\u003C/style>\n",[898],{"type":27,"tag":450,"props":899,"children":900},{"__ignoreMap":7},[901],{"type":32,"value":896},{"type":27,"tag":75,"props":903,"children":905},{"id":904},"完整示例",[906],{"type":32,"value":904},{"type":27,"tag":442,"props":908,"children":911},{"className":909,"code":910,"language":499,"meta":7},[497],"import { useState, useEffect } from 'react';\n\nfunction ThemeProvider({ children }) {\n  const [theme, setTheme] = useState('light');\n  const [mounted, setMounted] = useState(false);\n  \n  useEffect(() => {\n    setMounted(true);\n    \n    // 获取保存的主题或系统偏好\n    const savedTheme = localStorage.getItem('theme');\n    const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches \n      ? 'dark' \n      : 'light';\n    \n    const initialTheme = savedTheme || systemTheme;\n    setTheme(initialTheme);\n    document.documentElement.setAttribute('data-theme', initialTheme);\n  }, []);\n  \n  const toggleTheme = () => {\n    const newTheme = theme === 'light' ? 'dark' : 'light';\n    setTheme(newTheme);\n    localStorage.setItem('theme', newTheme);\n    document.documentElement.setAttribute('data-theme', newTheme);\n  };\n  \n  // 防止闪烁\n  if (!mounted) {\n    return null;\n  }\n  \n  return (\n    \u003CThemeContext.Provider value={{ theme, toggleTheme }}>\n      {children}\n      \u003CThemeToggle theme={theme} onChange={toggleTheme} />\n    \u003C/ThemeContext.Provider>\n  );\n}\n\nfunction ThemeToggle({ theme, onChange }) {\n  return (\n    \u003Cbutton \n      onClick={onChange}\n      aria-label={`切换到${theme === 'light' ? '暗黑' : '亮色'}模式`}\n    >\n      {theme === 'light' ? '🌙' : '☀️'}\n    \u003C/button>\n  );\n}\n",[912],{"type":27,"tag":450,"props":913,"children":914},{"__ignoreMap":7},[915],{"type":32,"value":910},{"type":27,"tag":75,"props":917,"children":918},{"id":581},[919],{"type":32,"value":581},{"type":27,"tag":28,"props":921,"children":922},{},[923,924,928],{"type":32,"value":588},{"type":27,"tag":590,"props":925,"children":926},{},[927],{"type":32,"value":594},{"type":32,"value":596},{"type":27,"tag":45,"props":930,"children":931},{},[932,937,942,947,952],{"type":27,"tag":49,"props":933,"children":934},{},[935],{"type":32,"value":936},"支持系统偏好",{"type":27,"tag":49,"props":938,"children":939},{},[940],{"type":32,"value":941},"提供手动切换选项",{"type":27,"tag":49,"props":943,"children":944},{},[945],{"type":32,"value":946},"确保足够的对比度",{"type":27,"tag":49,"props":948,"children":949},{},[950],{"type":32,"value":951},"优化图片和图表",{"type":27,"tag":49,"props":953,"children":954},{},[955],{"type":32,"value":956},"防止加载闪烁",{"type":27,"tag":28,"props":958,"children":959},{},[960,961,965],{"type":32,"value":635},{"type":27,"tag":590,"props":962,"children":963},{},[964],{"type":32,"value":640},{"type":32,"value":596},{"type":27,"tag":45,"props":967,"children":968},{},[969,974,979,984,989],{"type":27,"tag":49,"props":970,"children":971},{},[972],{"type":32,"value":973},"强制单一模式",{"type":27,"tag":49,"props":975,"children":976},{},[977],{"type":32,"value":978},"忽视性能影响",{"type":27,"tag":49,"props":980,"children":981},{},[982],{"type":32,"value":983},"使用相同的颜色",{"type":27,"tag":49,"props":985,"children":986},{},[987],{"type":32,"value":988},"忘记保存用户偏好",{"type":27,"tag":49,"props":990,"children":991},{},[992],{"type":32,"value":993},"过度使用深色背景",{"type":27,"tag":75,"props":995,"children":996},{"id":687},[997],{"type":32,"value":687},{"type":27,"tag":45,"props":999,"children":1001},{"className":1000},[693],[1002,1011,1020,1029,1038],{"type":27,"tag":49,"props":1003,"children":1005},{"className":1004},[698],[1006,1009],{"type":27,"tag":701,"props":1007,"children":1008},{"disabled":703,"type":704},[],{"type":32,"value":1010}," 在浅色和深色模式下测试所有页面",{"type":27,"tag":49,"props":1012,"children":1014},{"className":1013},[698],[1015,1018],{"type":27,"tag":701,"props":1016,"children":1017},{"disabled":703,"type":704},[],{"type":32,"value":1019}," 检查颜色对比度符合 WCAG 标准",{"type":27,"tag":49,"props":1021,"children":1023},{"className":1022},[698],[1024,1027],{"type":27,"tag":701,"props":1025,"children":1026},{"disabled":703,"type":704},[],{"type":32,"value":1028}," 验证图片和图表在两种模式下清晰",{"type":27,"tag":49,"props":1030,"children":1032},{"className":1031},[698],[1033,1036],{"type":27,"tag":701,"props":1034,"children":1035},{"disabled":703,"type":704},[],{"type":32,"value":1037}," 测试主题切换的平滑性",{"type":27,"tag":49,"props":1039,"children":1041},{"className":1040},[698],[1042,1045],{"type":27,"tag":701,"props":1043,"children":1044},{"disabled":703,"type":704},[],{"type":32,"value":1046}," 检查用户偏好是否被保存",{"title":7,"searchDepth":388,"depth":388,"links":1048},[1049,1050,1054,1059,1060,1061,1062,1063],{"id":767,"depth":391,"text":767},{"id":789,"depth":391,"text":789,"children":1051},[1052,1053],{"id":794,"depth":388,"text":797},{"id":809,"depth":388,"text":812},{"id":824,"depth":391,"text":824,"children":1055},[1056,1057,1058],{"id":829,"depth":388,"text":832},{"id":844,"depth":388,"text":847},{"id":861,"depth":388,"text":864},{"id":876,"depth":391,"text":876},{"id":890,"depth":391,"text":890},{"id":904,"depth":391,"text":904},{"id":581,"depth":391,"text":581},{"id":687,"depth":391,"text":687},"content:topics:design:dark-mode-design.md","topics/design/dark-mode-design.md","topics/design/dark-mode-design",{"_path":1068,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1069,"description":1070,"keywords":1071,"image":1076,"author":1077,"date":416,"readingTime":775,"topic":5,"body":1078,"_type":398,"_id":1343,"_source":400,"_file":1344,"_stem":1345,"_extension":403},"/topics/design/form-controls-design","表单控件设计规范","学习输入框、选择框、复选框等表单控件的设计和实现",[1072,1073,1074,1075,414],"表单设计","Form Controls","输入框","验证反馈","/images/topics/form-controls-design.jpg","AI Content Team",{"type":24,"children":1079,"toc":1329},[1080,1084,1089,1094,1099,1108,1113,1122,1126,1135,1140,1149,1154,1163,1168,1177,1182,1191,1195,1204,1230,1239,1267,1271],{"type":27,"tag":75,"props":1081,"children":1082},{"id":1069},[1083],{"type":32,"value":1069},{"type":27,"tag":28,"props":1085,"children":1086},{},[1087],{"type":32,"value":1088},"优秀的表单设计能够提高用户完成率和满意度。",{"type":27,"tag":75,"props":1090,"children":1092},{"id":1091},"输入框设计",[1093],{"type":32,"value":1091},{"type":27,"tag":435,"props":1095,"children":1097},{"id":1096},"基础文本输入",[1098],{"type":32,"value":1096},{"type":27,"tag":442,"props":1100,"children":1103},{"className":1101,"code":1102,"language":447,"meta":7},[445],".input {\n  width: 100%;\n  padding: 12px 16px;\n  font-size: 16px;\n  border: 2px solid #e0e0e0;\n  border-radius: 4px;\n  font-family: inherit;\n  transition: border-color 0.2s;\n}\n\n.input:hover {\n  border-color: #bdbdbd;\n}\n\n.input:focus {\n  outline: none;\n  border-color: #0066cc;\n  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);\n}\n\n.input:disabled {\n  background-color: #f5f5f5;\n  color: #999999;\n  cursor: not-allowed;\n}\n\n.input.error {\n  border-color: #cc0000;\n}\n\n.input.success {\n  border-color: #00cc00;\n}\n",[1104],{"type":27,"tag":450,"props":1105,"children":1106},{"__ignoreMap":7},[1107],{"type":32,"value":1102},{"type":27,"tag":435,"props":1109,"children":1111},{"id":1110},"标签和提示",[1112],{"type":32,"value":1110},{"type":27,"tag":442,"props":1114,"children":1117},{"className":1115,"code":1116,"language":559,"meta":7},[557],"\u003Cdiv class=\"form-group\">\n  \u003Clabel for=\"email\" class=\"form-label\">\n    邮箱地址 \u003Cspan class=\"required\">*\u003C/span>\n  \u003C/label>\n  \u003Cinput\n    id=\"email\"\n    type=\"email\"\n    placeholder=\"user@example.com\"\n    class=\"input\"\n    aria-describedby=\"email-hint\"\n  />\n  \u003Cp id=\"email-hint\" class=\"form-hint\">\n    我们永远不会分享你的邮箱\n  \u003C/p>\n\u003C/div>\n",[1118],{"type":27,"tag":450,"props":1119,"children":1120},{"__ignoreMap":7},[1121],{"type":32,"value":1116},{"type":27,"tag":75,"props":1123,"children":1124},{"id":1075},[1125],{"type":32,"value":1075},{"type":27,"tag":442,"props":1127,"children":1130},{"className":1128,"code":1129,"language":499,"meta":7},[497],"function FormInput({ label, error, success, helperText, value, onChange, ...props }) {\n  return (\n    \u003Cdiv className=\"form-group\">\n      \u003Clabel className=\"form-label\">{label}\u003C/label>\n      \u003Cinput\n        className={`input ${\n          error ? 'error' : success ? 'success' : ''\n        }`}\n        value={value}\n        onChange={onChange}\n        {...props}\n      />\n      {error && (\n        \u003Cp className=\"form-error\" role=\"alert\">\n          {error}\n        \u003C/p>\n      )}\n      {success && (\n        \u003Cp className=\"form-success\">\n          ✓ {success}\n        \u003C/p>\n      )}\n      {helperText && (\n        \u003Cp className=\"form-hint\">{helperText}\u003C/p>\n      )}\n    \u003C/div>\n  );\n}\n",[1131],{"type":27,"tag":450,"props":1132,"children":1133},{"__ignoreMap":7},[1134],{"type":32,"value":1129},{"type":27,"tag":75,"props":1136,"children":1138},{"id":1137},"选择框设计",[1139],{"type":32,"value":1137},{"type":27,"tag":442,"props":1141,"children":1144},{"className":1142,"code":1143,"language":447,"meta":7},[445],".select {\n  appearance: none;\n  width: 100%;\n  padding: 12px 16px;\n  border: 2px solid #e0e0e0;\n  border-radius: 4px;\n  background-image: url('data:image/svg+xml;...');\n  background-repeat: no-repeat;\n  background-position: right 12px center;\n  padding-right: 40px;\n  font-size: 16px;\n  cursor: pointer;\n}\n\n.select:hover {\n  border-color: #bdbdbd;\n}\n\n.select:focus {\n  outline: none;\n  border-color: #0066cc;\n  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);\n}\n",[1145],{"type":27,"tag":450,"props":1146,"children":1147},{"__ignoreMap":7},[1148],{"type":32,"value":1143},{"type":27,"tag":75,"props":1150,"children":1152},{"id":1151},"复选框和单选按钮",[1153],{"type":32,"value":1151},{"type":27,"tag":442,"props":1155,"children":1158},{"className":1156,"code":1157,"language":447,"meta":7},[445],".checkbox-group {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n}\n\n.checkbox-input {\n  width: 20px;\n  height: 20px;\n  cursor: pointer;\n  accent-color: #0066cc;\n}\n\n.checkbox-label {\n  cursor: pointer;\n  user-select: none;\n}\n\n/* 自定义复选框 */\n.custom-checkbox {\n  appearance: none;\n  width: 20px;\n  height: 20px;\n  border: 2px solid #e0e0e0;\n  border-radius: 4px;\n  cursor: pointer;\n  background-color: white;\n  transition: all 0.2s;\n}\n\n.custom-checkbox:checked {\n  background-color: #0066cc;\n  border-color: #0066cc;\n  background-image: url('data:image/svg+xml;...');\n}\n\n.custom-checkbox:focus {\n  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);\n}\n",[1159],{"type":27,"tag":450,"props":1160,"children":1161},{"__ignoreMap":7},[1162],{"type":32,"value":1157},{"type":27,"tag":75,"props":1164,"children":1166},{"id":1165},"文本区域",[1167],{"type":32,"value":1165},{"type":27,"tag":442,"props":1169,"children":1172},{"className":1170,"code":1171,"language":447,"meta":7},[445],".textarea {\n  width: 100%;\n  min-height: 120px;\n  padding: 12px 16px;\n  border: 2px solid #e0e0e0;\n  border-radius: 4px;\n  font-family: inherit;\n  font-size: 16px;\n  resize: vertical;\n  transition: border-color 0.2s;\n}\n\n.textarea:focus {\n  outline: none;\n  border-color: #0066cc;\n  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);\n}\n",[1173],{"type":27,"tag":450,"props":1174,"children":1175},{"__ignoreMap":7},[1176],{"type":32,"value":1171},{"type":27,"tag":75,"props":1178,"children":1180},{"id":1179},"完整表单示例",[1181],{"type":32,"value":1179},{"type":27,"tag":442,"props":1183,"children":1186},{"className":1184,"code":1185,"language":499,"meta":7},[497],"function SignupForm() {\n  const [formData, setFormData] = useState({\n    name: '',\n    email: '',\n    password: '',\n    confirmPassword: '',\n    subscribe: false,\n    terms: false,\n  });\n  \n  const [errors, setErrors] = useState({});\n  const [touched, setTouched] = useState({});\n  const [submitted, setSubmitted] = useState(false);\n  \n  const handleChange = (e) => {\n    const { name, value, type, checked } = e.target;\n    setFormData(prev => ({\n      ...prev,\n      [name]: type === 'checkbox' ? checked : value,\n    }));\n    \n    // 实时验证\n    if (touched[name]) {\n      validateField(name, type === 'checkbox' ? checked : value);\n    }\n  };\n  \n  const handleBlur = (e) => {\n    const { name } = e.target;\n    setTouched(prev => ({ ...prev, [name]: true }));\n    validateField(name, formData[name]);\n  };\n  \n  const validateField = (name, value) => {\n    const newErrors = { ...errors };\n    \n    switch (name) {\n      case 'name':\n        if (!value) newErrors.name = '名字不能为空';\n        else delete newErrors.name;\n        break;\n      case 'email':\n        if (!value) newErrors.email = '邮箱不能为空';\n        else if (!/^[^\\\\s@]+@[^\\\\s@]+\\\\.[^\\\\s@]+$/.test(value)) {\n          newErrors.email = '请输入有效的邮箱';\n        } else {\n          delete newErrors.email;\n        }\n        break;\n      case 'password':\n        if (!value) newErrors.password = '密码不能为空';\n        else if (value.length \u003C 8) newErrors.password = '密码至少 8 位';\n        else delete newErrors.password;\n        break;\n      case 'confirmPassword':\n        if (value !== formData.password) {\n          newErrors.confirmPassword = '两次密码输入不一致';\n        } else {\n          delete newErrors.confirmPassword;\n        }\n        break;\n      case 'terms':\n        if (!value) newErrors.terms = '必须同意服务条款';\n        else delete newErrors.terms;\n        break;\n      default:\n        break;\n    }\n    \n    setErrors(newErrors);\n  };\n  \n  const validate = () => {\n    const newErrors = {};\n    \n    if (!formData.name) newErrors.name = '名字不能为空';\n    if (!formData.email) newErrors.email = '邮箱不能为空';\n    if (formData.password.length \u003C 8) newErrors.password = '密码至少 8 位';\n    if (formData.password !== formData.confirmPassword) {\n      newErrors.confirmPassword = '两次密码输入不一致';\n    }\n    if (!formData.terms) newErrors.terms = '必须同意服务条款';\n    \n    return newErrors;\n  };\n  \n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    \n    // 标记所有字段已触碰\n    setTouched({\n      name: true,\n      email: true,\n      password: true,\n      confirmPassword: true,\n      terms: true,\n    });\n    \n    const newErrors = validate();\n    \n    if (Object.keys(newErrors).length === 0) {\n      setSubmitted(true);\n      // 提交表单\n      console.log('Form submitted:', formData);\n      // 重置表单\n      setFormData({\n        name: '',\n        email: '',\n        password: '',\n        confirmPassword: '',\n        subscribe: false,\n        terms: false,\n      });\n    } else {\n      setErrors(newErrors);\n    }\n  };\n  \n  return (\n    \u003Cform onSubmit={handleSubmit} noValidate>\n      {submitted && (\n        \u003Cdiv className=\"form-success-message\" role=\"alert\">\n          注册成功！\n        \u003C/div>\n      )}\n      \n      \u003CFormInput\n        label=\"姓名\"\n        name=\"name\"\n        value={formData.name}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        error={touched.name && errors.name}\n        helperText=\"请输入你的全名\"\n      />\n      \n      \u003CFormInput\n        label=\"邮箱\"\n        name=\"email\"\n        type=\"email\"\n        value={formData.email}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        error={touched.email && errors.email}\n      />\n      \n      \u003CFormInput\n        label=\"密码\"\n        name=\"password\"\n        type=\"password\"\n        value={formData.password}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        error={touched.password && errors.password}\n        helperText=\"至少 8 个字符\"\n      />\n      \n      \u003CFormInput\n        label=\"确认密码\"\n        name=\"confirmPassword\"\n        type=\"password\"\n        value={formData.confirmPassword}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        error={touched.confirmPassword && errors.confirmPassword}\n      />\n      \n      \u003Cdiv className=\"form-group\">\n        \u003Clabel className=\"checkbox-label\">\n          \u003Cinput\n            type=\"checkbox\"\n            name=\"subscribe\"\n            checked={formData.subscribe}\n            onChange={handleChange}\n            className=\"checkbox-input\"\n          />\n          订阅我们的新闻通讯\n        \u003C/label>\n      \u003C/div>\n      \n      \u003Cdiv className=\"form-group\">\n        \u003Clabel className=\"checkbox-label\">\n          \u003Cinput\n            type=\"checkbox\"\n            name=\"terms\"\n            checked={formData.terms}\n            onChange={handleChange}\n            onBlur={handleBlur}\n            className=\"checkbox-input\"\n          />\n          我同意\n          \u003Ca href=\"/terms\" target=\"_blank\" rel=\"noopener noreferrer\">\n            服务条款\n          \u003C/a>\n          和\n          \u003Ca href=\"/privacy\" target=\"_blank\" rel=\"noopener noreferrer\">\n            隐私政策\n          \u003C/a>\n        \u003C/label>\n        {touched.terms && errors.terms && (\n          \u003Cp className=\"form-error\">{errors.terms}\u003C/p>\n        )}\n      \u003C/div>\n      \n      \u003Cbutton type=\"submit\" className=\"btn btn-primary btn-block\">\n        注册\n      \u003C/button>\n    \u003C/form>\n  );\n}\n",[1187],{"type":27,"tag":450,"props":1188,"children":1189},{"__ignoreMap":7},[1190],{"type":32,"value":1185},{"type":27,"tag":75,"props":1192,"children":1193},{"id":581},[1194],{"type":32,"value":581},{"type":27,"tag":28,"props":1196,"children":1197},{},[1198,1199,1203],{"type":32,"value":588},{"type":27,"tag":590,"props":1200,"children":1201},{},[1202],{"type":32,"value":594},{"type":32,"value":596},{"type":27,"tag":45,"props":1205,"children":1206},{},[1207,1212,1217,1222,1226],{"type":27,"tag":49,"props":1208,"children":1209},{},[1210],{"type":32,"value":1211},"使用正确的输入类型",{"type":27,"tag":49,"props":1213,"children":1214},{},[1215],{"type":32,"value":1216},"提供实时验证反馈",{"type":27,"tag":49,"props":1218,"children":1219},{},[1220],{"type":32,"value":1221},"清晰的标签和提示",{"type":27,"tag":49,"props":1223,"children":1224},{},[1225],{"type":32,"value":604},{"type":27,"tag":49,"props":1227,"children":1228},{},[1229],{"type":32,"value":630},{"type":27,"tag":28,"props":1231,"children":1232},{},[1233,1234,1238],{"type":32,"value":635},{"type":27,"tag":590,"props":1235,"children":1236},{},[1237],{"type":32,"value":640},{"type":32,"value":596},{"type":27,"tag":45,"props":1240,"children":1241},{},[1242,1247,1252,1257,1262],{"type":27,"tag":49,"props":1243,"children":1244},{},[1245],{"type":32,"value":1246},"隐藏标签",{"type":27,"tag":49,"props":1248,"children":1249},{},[1250],{"type":32,"value":1251},"过度使用占位符",{"type":27,"tag":49,"props":1253,"children":1254},{},[1255],{"type":32,"value":1256},"验证后立即提交",{"type":27,"tag":49,"props":1258,"children":1259},{},[1260],{"type":32,"value":1261},"忽视无障碍性",{"type":27,"tag":49,"props":1263,"children":1264},{},[1265],{"type":32,"value":1266},"复杂的验证规则",{"type":27,"tag":75,"props":1268,"children":1269},{"id":687},[1270],{"type":32,"value":687},{"type":27,"tag":45,"props":1272,"children":1274},{"className":1273},[693],[1275,1284,1293,1302,1311,1320],{"type":27,"tag":49,"props":1276,"children":1278},{"className":1277},[698],[1279,1282],{"type":27,"tag":701,"props":1280,"children":1281},{"disabled":703,"type":704},[],{"type":32,"value":1283}," 所有控件都可用键盘导航",{"type":27,"tag":49,"props":1285,"children":1287},{"className":1286},[698],[1288,1291],{"type":27,"tag":701,"props":1289,"children":1290},{"disabled":703,"type":704},[],{"type":32,"value":1292}," 标签与输入框关联",{"type":27,"tag":49,"props":1294,"children":1296},{"className":1295},[698],[1297,1300],{"type":27,"tag":701,"props":1298,"children":1299},{"disabled":703,"type":704},[],{"type":32,"value":1301}," 验证消息清晰",{"type":27,"tag":49,"props":1303,"children":1305},{"className":1304},[698],[1306,1309],{"type":27,"tag":701,"props":1307,"children":1308},{"disabled":703,"type":704},[],{"type":32,"value":1310}," 色彩对比度足够",{"type":27,"tag":49,"props":1312,"children":1314},{"className":1313},[698],[1315,1318],{"type":27,"tag":701,"props":1316,"children":1317},{"disabled":703,"type":704},[],{"type":32,"value":1319}," 屏幕阅读器兼容",{"type":27,"tag":49,"props":1321,"children":1323},{"className":1322},[698],[1324,1327],{"type":27,"tag":701,"props":1325,"children":1326},{"disabled":703,"type":704},[],{"type":32,"value":1328}," 移动设备测试",{"title":7,"searchDepth":388,"depth":388,"links":1330},[1331,1332,1336,1337,1338,1339,1340,1341,1342],{"id":1069,"depth":391,"text":1069},{"id":1091,"depth":391,"text":1091,"children":1333},[1334,1335],{"id":1096,"depth":388,"text":1096},{"id":1110,"depth":388,"text":1110},{"id":1075,"depth":391,"text":1075},{"id":1137,"depth":391,"text":1137},{"id":1151,"depth":391,"text":1151},{"id":1165,"depth":391,"text":1165},{"id":1179,"depth":391,"text":1179},{"id":581,"depth":391,"text":581},{"id":687,"depth":391,"text":687},"content:topics:design:form-controls-design.md","topics/design/form-controls-design.md","topics/design/form-controls-design",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":1347,"image":18,"imageQuery":19,"pexelsPhotoId":20,"pexelsUrl":21,"featured":6,"readingTime":22,"body":1348,"_type":398,"_id":399,"_source":400,"_file":401,"_stem":402,"_extension":403},[13,14,15,16,17],{"type":24,"children":1349,"toc":1634},[1350,1354,1358,1362,1381,1385,1389,1393,1397,1416,1420,1424,1428,1443,1447,1451,1470,1474,1478,1482,1486,1505,1509,1513,1517,1521,1536,1540,1544,1548,1567,1571,1575,1598,1602,1606,1610],{"type":27,"tag":28,"props":1351,"children":1352},{},[1353],{"type":32,"value":33},{"type":27,"tag":28,"props":1355,"children":1356},{},[1357],{"type":32,"value":38},{"type":27,"tag":28,"props":1359,"children":1360},{},[1361],{"type":32,"value":43},{"type":27,"tag":45,"props":1363,"children":1364},{},[1365,1369,1373,1377],{"type":27,"tag":49,"props":1366,"children":1367},{},[1368],{"type":32,"value":53},{"type":27,"tag":49,"props":1370,"children":1371},{},[1372],{"type":32,"value":58},{"type":27,"tag":49,"props":1374,"children":1375},{},[1376],{"type":32,"value":63},{"type":27,"tag":49,"props":1378,"children":1379},{},[1380],{"type":32,"value":68},{"type":27,"tag":28,"props":1382,"children":1383},{},[1384],{"type":32,"value":73},{"type":27,"tag":75,"props":1386,"children":1387},{"id":77},[1388],{"type":32,"value":80},{"type":27,"tag":28,"props":1390,"children":1391},{},[1392],{"type":32,"value":85},{"type":27,"tag":28,"props":1394,"children":1395},{},[1396],{"type":32,"value":90},{"type":27,"tag":45,"props":1398,"children":1399},{},[1400,1404,1408,1412],{"type":27,"tag":49,"props":1401,"children":1402},{},[1403],{"type":32,"value":98},{"type":27,"tag":49,"props":1405,"children":1406},{},[1407],{"type":32,"value":103},{"type":27,"tag":49,"props":1409,"children":1410},{},[1411],{"type":32,"value":108},{"type":27,"tag":49,"props":1413,"children":1414},{},[1415],{"type":32,"value":113},{"type":27,"tag":28,"props":1417,"children":1418},{},[1419],{"type":32,"value":118},{"type":27,"tag":75,"props":1421,"children":1422},{"id":121},[1423],{"type":32,"value":124},{"type":27,"tag":28,"props":1425,"children":1426},{},[1427],{"type":32,"value":129},{"type":27,"tag":45,"props":1429,"children":1430},{},[1431,1435,1439],{"type":27,"tag":49,"props":1432,"children":1433},{},[1434],{"type":32,"value":137},{"type":27,"tag":49,"props":1436,"children":1437},{},[1438],{"type":32,"value":142},{"type":27,"tag":49,"props":1440,"children":1441},{},[1442],{"type":32,"value":147},{"type":27,"tag":28,"props":1444,"children":1445},{},[1446],{"type":32,"value":152},{"type":27,"tag":28,"props":1448,"children":1449},{},[1450],{"type":32,"value":157},{"type":27,"tag":45,"props":1452,"children":1453},{},[1454,1458,1462,1466],{"type":27,"tag":49,"props":1455,"children":1456},{},[1457],{"type":32,"value":165},{"type":27,"tag":49,"props":1459,"children":1460},{},[1461],{"type":32,"value":170},{"type":27,"tag":49,"props":1463,"children":1464},{},[1465],{"type":32,"value":175},{"type":27,"tag":49,"props":1467,"children":1468},{},[1469],{"type":32,"value":180},{"type":27,"tag":28,"props":1471,"children":1472},{},[1473],{"type":32,"value":185},{"type":27,"tag":75,"props":1475,"children":1476},{"id":188},[1477],{"type":32,"value":191},{"type":27,"tag":28,"props":1479,"children":1480},{},[1481],{"type":32,"value":196},{"type":27,"tag":28,"props":1483,"children":1484},{},[1485],{"type":32,"value":201},{"type":27,"tag":45,"props":1487,"children":1488},{},[1489,1493,1497,1501],{"type":27,"tag":49,"props":1490,"children":1491},{},[1492],{"type":32,"value":209},{"type":27,"tag":49,"props":1494,"children":1495},{},[1496],{"type":32,"value":214},{"type":27,"tag":49,"props":1498,"children":1499},{},[1500],{"type":32,"value":219},{"type":27,"tag":49,"props":1502,"children":1503},{},[1504],{"type":32,"value":224},{"type":27,"tag":28,"props":1506,"children":1507},{},[1508],{"type":32,"value":229},{"type":27,"tag":75,"props":1510,"children":1511},{"id":232},[1512],{"type":32,"value":235},{"type":27,"tag":28,"props":1514,"children":1515},{},[1516],{"type":32,"value":240},{"type":27,"tag":28,"props":1518,"children":1519},{},[1520],{"type":32,"value":245},{"type":27,"tag":45,"props":1522,"children":1523},{},[1524,1528,1532],{"type":27,"tag":49,"props":1525,"children":1526},{},[1527],{"type":32,"value":253},{"type":27,"tag":49,"props":1529,"children":1530},{},[1531],{"type":32,"value":258},{"type":27,"tag":49,"props":1533,"children":1534},{},[1535],{"type":32,"value":263},{"type":27,"tag":28,"props":1537,"children":1538},{},[1539],{"type":32,"value":268},{"type":27,"tag":75,"props":1541,"children":1542},{"id":271},[1543],{"type":32,"value":274},{"type":27,"tag":28,"props":1545,"children":1546},{},[1547],{"type":32,"value":279},{"type":27,"tag":45,"props":1549,"children":1550},{},[1551,1555,1559,1563],{"type":27,"tag":49,"props":1552,"children":1553},{},[1554],{"type":32,"value":287},{"type":27,"tag":49,"props":1556,"children":1557},{},[1558],{"type":32,"value":292},{"type":27,"tag":49,"props":1560,"children":1561},{},[1562],{"type":32,"value":297},{"type":27,"tag":49,"props":1564,"children":1565},{},[1566],{"type":32,"value":302},{"type":27,"tag":28,"props":1568,"children":1569},{},[1570],{"type":32,"value":307},{"type":27,"tag":75,"props":1572,"children":1573},{"id":310},[1574],{"type":32,"value":310},{"type":27,"tag":45,"props":1576,"children":1577},{},[1578,1582,1586,1590,1594],{"type":27,"tag":49,"props":1579,"children":1580},{},[1581],{"type":32,"value":320},{"type":27,"tag":49,"props":1583,"children":1584},{},[1585],{"type":32,"value":325},{"type":27,"tag":49,"props":1587,"children":1588},{},[1589],{"type":32,"value":330},{"type":27,"tag":49,"props":1591,"children":1592},{},[1593],{"type":32,"value":335},{"type":27,"tag":49,"props":1595,"children":1596},{},[1597],{"type":32,"value":340},{"type":27,"tag":75,"props":1599,"children":1600},{"id":343},[1601],{"type":32,"value":343},{"type":27,"tag":28,"props":1603,"children":1604},{},[1605],{"type":32,"value":350},{"type":27,"tag":28,"props":1607,"children":1608},{},[1609],{"type":32,"value":355},{"type":27,"tag":45,"props":1611,"children":1612},{},[1613,1620,1627],{"type":27,"tag":49,"props":1614,"children":1615},{},[1616],{"type":27,"tag":363,"props":1617,"children":1618},{"href":365},[1619],{"type":32,"value":368},{"type":27,"tag":49,"props":1621,"children":1622},{},[1623],{"type":27,"tag":363,"props":1624,"children":1625},{"href":374},[1626],{"type":32,"value":377},{"type":27,"tag":49,"props":1628,"children":1629},{},[1630],{"type":27,"tag":363,"props":1631,"children":1632},{"href":383},[1633],{"type":32,"value":386},{"title":7,"searchDepth":388,"depth":388,"links":1635},[1636,1637,1638,1639,1640,1641,1642],{"id":77,"depth":391,"text":80},{"id":121,"depth":391,"text":124},{"id":188,"depth":391,"text":191},{"id":232,"depth":391,"text":235},{"id":271,"depth":391,"text":274},{"id":310,"depth":391,"text":310},{"id":343,"depth":391,"text":343},1777086814593]