[{"data":1,"prerenderedAt":1974},["ShallowReactive",2],{"article-/topics/design/navigation-component-guide":3,"related-design":582,"content-query-GkEfBpVWbJ":1518},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"topic":5,"author":11,"tags":12,"image":18,"featured":6,"readingTime":19,"body":20,"_type":576,"_id":577,"_source":578,"_file":579,"_stem":580,"_extension":581},"/topics/design/navigation-component-guide","design",false,"","导航组件设计最佳实践指南","深入解析导航组件的设计原则、常见模式和实现方法。掌握响应式导航、多级菜单、面包屑等核心组件的设计与开发，构建清晰易用的导航体系。","2024-12-30","HTMLPAGE 团队",[13,14,15,16,17],"导航设计","组件系统","响应式设计","用户体验","Vue组件","/images/topics/design/navigation-component-guide.jpg","26分钟",{"type":21,"children":22,"toc":543},"root",[23,31,37,83,88,93,99,109,114,122,127,132,143,148,157,162,167,176,181,186,195,200,211,216,221,230,235,244,249,254,263,268,277,282,287,427,432,485,490,495,538],{"type":24,"tag":25,"props":26,"children":28},"element","h2",{"id":27},"导航设计的重要性",[29],{"type":30,"value":27},"text",{"type":24,"tag":32,"props":33,"children":34},"p",{},[35],{"type":30,"value":36},"导航是用户探索应用的路标系统。良好的导航设计能够：",{"type":24,"tag":38,"props":39,"children":40},"ul",{},[41,53,63,73],{"type":24,"tag":42,"props":43,"children":44},"li",{},[45,51],{"type":24,"tag":46,"props":47,"children":48},"strong",{},[49],{"type":30,"value":50},"提升可发现性",{"type":30,"value":52}," - 用户快速找到所需功能",{"type":24,"tag":42,"props":54,"children":55},{},[56,61],{"type":24,"tag":46,"props":57,"children":58},{},[59],{"type":30,"value":60},"建立心智模型",{"type":30,"value":62}," - 帮助用户理解应用结构",{"type":24,"tag":42,"props":64,"children":65},{},[66,71],{"type":24,"tag":46,"props":67,"children":68},{},[69],{"type":30,"value":70},"减少认知负担",{"type":30,"value":72}," - 清晰的层级降低决策成本",{"type":24,"tag":42,"props":74,"children":75},{},[76,81],{"type":24,"tag":46,"props":77,"children":78},{},[79],{"type":30,"value":80},"增强用户信心",{"type":30,"value":82}," - 随时知道\"我在哪里\"",{"type":24,"tag":32,"props":84,"children":85},{},[86],{"type":30,"value":87},"研究表明，超过 50% 的用户会在导航不清晰时直接离开网站。本文将详解各类导航组件的设计与实现。",{"type":24,"tag":25,"props":89,"children":91},{"id":90},"导航类型与适用场景",[92],{"type":30,"value":90},{"type":24,"tag":94,"props":95,"children":97},"h3",{"id":96},"导航类型对比",[98],{"type":30,"value":96},{"type":24,"tag":100,"props":101,"children":103},"pre",{"code":102},"┌─────────────────────────────────────────────────────────────────┐\n│                     导航类型选择矩阵                              │\n├──────────────┬──────────────┬──────────────┬───────────────────┤\n│    类型       │   适用场景    │    优点       │      缺点         │\n├──────────────┼──────────────┼──────────────┼───────────────────┤\n│  顶部导航     │ 5-7个主要项   │ 可见性高      │ 空间有限          │\n│  侧边导航     │ 多级深层结构  │ 可容纳更多项  │ 占用水平空间       │\n│  底部导航     │ 移动端核心功能 │ 易于触达      │ 仅限少量入口       │\n│  汉堡菜单     │ 空间受限场景  │ 节省空间      │ 可发现性低         │\n│  标签页       │ 并列内容切换  │ 直观清晰      │ 数量受限          │\n└──────────────┴──────────────┴──────────────┴───────────────────┘\n",[104],{"type":24,"tag":105,"props":106,"children":107},"code",{"__ignoreMap":7},[108],{"type":30,"value":102},{"type":24,"tag":94,"props":110,"children":112},{"id":111},"选择决策树",[113],{"type":30,"value":111},{"type":24,"tag":100,"props":115,"children":117},{"code":116},"开始\n  │\n  ├─ 导航项 ≤ 5 个？\n  │    ├─ 是 → 移动端核心功能？\n  │    │         ├─ 是 → 底部导航\n  │    │         └─ 否 → 顶部导航\n  │    │\n  │    └─ 否 → 有多级结构？\n  │              ├─ 是 → 侧边导航\n  │              └─ 否 → 顶部导航 + 下拉菜单\n",[118],{"type":24,"tag":105,"props":119,"children":120},{"__ignoreMap":7},[121],{"type":30,"value":116},{"type":24,"tag":25,"props":123,"children":125},{"id":124},"顶部导航栏组件",[126],{"type":30,"value":124},{"type":24,"tag":94,"props":128,"children":130},{"id":129},"基础导航栏",[131],{"type":30,"value":129},{"type":24,"tag":100,"props":133,"children":138},{"code":134,"language":135,"meta":7,"className":136},"\u003C!-- components/navigation/Navbar.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface NavItem {\n  label: string\n  href?: string\n  to?: string\n  children?: NavItem[]\n  icon?: string\n}\n\ninterface Props {\n  items: NavItem[]\n  logo?: string\n  logoAlt?: string\n  sticky?: boolean\n  transparent?: boolean\n}\n\nconst props = withDefaults(defineProps\u003CProps>(), {\n  sticky: true,\n  transparent: false,\n})\n\nconst route = useRoute()\nconst isScrolled = ref(false)\nconst isMobileMenuOpen = ref(false)\n\n// 滚动检测\nonMounted(() => {\n  if (props.sticky) {\n    window.addEventListener('scroll', handleScroll)\n  }\n})\n\nonUnmounted(() => {\n  window.removeEventListener('scroll', handleScroll)\n})\n\nconst handleScroll = () => {\n  isScrolled.value = window.scrollY > 20\n}\n\n// 检查是否为当前路由\nconst isActive = (item: NavItem): boolean => {\n  if (item.to) {\n    return route.path === item.to || route.path.startsWith(item.to + '/')\n  }\n  return false\n}\n\n// 切换移动端菜单\nconst toggleMobileMenu = () => {\n  isMobileMenuOpen.value = !isMobileMenuOpen.value\n  \n  // 防止背景滚动\n  document.body.style.overflow = isMobileMenuOpen.value ? 'hidden' : ''\n}\n\n// 路由变化时关闭菜单\nwatch(() => route.path, () => {\n  isMobileMenuOpen.value = false\n  document.body.style.overflow = ''\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cheader\n    class=\"navbar\"\n    :class=\"{\n      'navbar--sticky': sticky,\n      'navbar--scrolled': isScrolled,\n      'navbar--transparent': transparent && !isScrolled,\n    }\"\n  >\n    \u003Cdiv class=\"navbar__container\">\n      \u003C!-- Logo -->\n      \u003CNuxtLink to=\"/\" class=\"navbar__logo\">\n        \u003Cimg v-if=\"logo\" :src=\"logo\" :alt=\"logoAlt || 'Logo'\" />\n        \u003Cslot v-else name=\"logo\">\n          \u003Cspan class=\"navbar__brand\">HTMLPAGE\u003C/span>\n        \u003C/slot>\n      \u003C/NuxtLink>\n      \n      \u003C!-- 桌面端导航 -->\n      \u003Cnav class=\"navbar__nav\" role=\"navigation\" aria-label=\"主导航\">\n        \u003Cul class=\"navbar__menu\">\n          \u003Cli\n            v-for=\"item in items\"\n            :key=\"item.label\"\n            class=\"navbar__item\"\n            :class=\"{ 'navbar__item--has-children': item.children }\"\n          >\n            \u003C!-- 有子菜单的项 -->\n            \u003CNavDropdown\n              v-if=\"item.children\"\n              :item=\"item\"\n              :is-active=\"isActive(item)\"\n            />\n            \n            \u003C!-- 普通链接 -->\n            \u003CNuxtLink\n              v-else-if=\"item.to\"\n              :to=\"item.to\"\n              class=\"navbar__link\"\n              :class=\"{ 'navbar__link--active': isActive(item) }\"\n            >\n              \u003CIcon v-if=\"item.icon\" :name=\"item.icon\" />\n              {{ item.label }}\n            \u003C/NuxtLink>\n            \n            \u003C!-- 外部链接 -->\n            \u003Ca\n              v-else-if=\"item.href\"\n              :href=\"item.href\"\n              class=\"navbar__link\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              \u003CIcon v-if=\"item.icon\" :name=\"item.icon\" />\n              {{ item.label }}\n              \u003CIcon name=\"external-link\" class=\"navbar__external-icon\" />\n            \u003C/a>\n          \u003C/li>\n        \u003C/ul>\n      \u003C/nav>\n      \n      \u003C!-- 右侧操作区 -->\n      \u003Cdiv class=\"navbar__actions\">\n        \u003Cslot name=\"actions\" />\n        \n        \u003C!-- 移动端菜单按钮 -->\n        \u003Cbutton\n          class=\"navbar__toggle\"\n          :aria-expanded=\"isMobileMenuOpen\"\n          aria-controls=\"mobile-menu\"\n          aria-label=\"打开菜单\"\n          @click=\"toggleMobileMenu\"\n        >\n          \u003Cspan class=\"navbar__toggle-icon\" :class=\"{ 'is-open': isMobileMenuOpen }\">\n            \u003Cspan>\u003C/span>\n            \u003Cspan>\u003C/span>\n            \u003Cspan>\u003C/span>\n          \u003C/span>\n        \u003C/button>\n      \u003C/div>\n    \u003C/div>\n    \n    \u003C!-- 移动端菜单 -->\n    \u003CTeleport to=\"body\">\n      \u003CTransition name=\"mobile-menu\">\n        \u003Cdiv\n          v-if=\"isMobileMenuOpen\"\n          id=\"mobile-menu\"\n          class=\"navbar__mobile-menu\"\n        >\n          \u003Cdiv class=\"navbar__mobile-overlay\" @click=\"toggleMobileMenu\" />\n          \n          \u003Cnav class=\"navbar__mobile-nav\" role=\"navigation\">\n            \u003Cul class=\"navbar__mobile-list\">\n              \u003Cli v-for=\"item in items\" :key=\"item.label\">\n                \u003CMobileNavItem :item=\"item\" @close=\"toggleMobileMenu\" />\n              \u003C/li>\n            \u003C/ul>\n            \n            \u003Cdiv class=\"navbar__mobile-actions\">\n              \u003Cslot name=\"mobile-actions\" />\n            \u003C/div>\n          \u003C/nav>\n        \u003C/div>\n      \u003C/Transition>\n    \u003C/Teleport>\n  \u003C/header>\n\u003C/template>\n\n\u003Cstyle scoped>\n.navbar {\n  position: relative;\n  width: 100%;\n  height: var(--navbar-height, 64px);\n  background: var(--color-bg-primary);\n  border-bottom: 1px solid var(--color-border-light);\n  z-index: 100;\n}\n\n.navbar--sticky {\n  position: sticky;\n  top: 0;\n}\n\n.navbar--scrolled {\n  box-shadow: var(--shadow-sm);\n}\n\n.navbar--transparent {\n  background: transparent;\n  border-bottom: none;\n}\n\n.navbar__container {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  max-width: var(--container-max-width, 1280px);\n  height: 100%;\n  margin: 0 auto;\n  padding: 0 24px;\n}\n\n/* Logo */\n.navbar__logo {\n  display: flex;\n  align-items: center;\n  text-decoration: none;\n}\n\n.navbar__logo img {\n  height: 32px;\n  width: auto;\n}\n\n.navbar__brand {\n  font-size: 20px;\n  font-weight: 700;\n  color: var(--color-text-primary);\n}\n\n/* 桌面导航 */\n.navbar__nav {\n  display: none;\n}\n\n@media (min-width: 768px) {\n  .navbar__nav {\n    display: block;\n  }\n}\n\n.navbar__menu {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n.navbar__link {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 8px 16px;\n  font-size: 14px;\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  text-decoration: none;\n  border-radius: var(--radius-md);\n  transition: all 0.2s ease;\n}\n\n.navbar__link:hover {\n  color: var(--color-text-primary);\n  background: var(--color-bg-secondary);\n}\n\n.navbar__link--active {\n  color: var(--color-primary);\n  background: var(--color-primary-light);\n}\n\n.navbar__external-icon {\n  width: 12px;\n  height: 12px;\n  opacity: 0.5;\n}\n\n/* 移动端菜单按钮 */\n.navbar__toggle {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 40px;\n  height: 40px;\n  padding: 0;\n  border: none;\n  background: transparent;\n  cursor: pointer;\n}\n\n@media (min-width: 768px) {\n  .navbar__toggle {\n    display: none;\n  }\n}\n\n.navbar__toggle-icon {\n  position: relative;\n  width: 24px;\n  height: 18px;\n}\n\n.navbar__toggle-icon span {\n  position: absolute;\n  left: 0;\n  width: 100%;\n  height: 2px;\n  background: var(--color-text-primary);\n  border-radius: 1px;\n  transition: all 0.3s ease;\n}\n\n.navbar__toggle-icon span:nth-child(1) { top: 0; }\n.navbar__toggle-icon span:nth-child(2) { top: 50%; transform: translateY(-50%); }\n.navbar__toggle-icon span:nth-child(3) { bottom: 0; }\n\n.navbar__toggle-icon.is-open span:nth-child(1) {\n  top: 50%;\n  transform: translateY(-50%) rotate(45deg);\n}\n\n.navbar__toggle-icon.is-open span:nth-child(2) {\n  opacity: 0;\n}\n\n.navbar__toggle-icon.is-open span:nth-child(3) {\n  bottom: 50%;\n  transform: translateY(50%) rotate(-45deg);\n}\n\n/* 移动端菜单 */\n.navbar__mobile-menu {\n  position: fixed;\n  inset: 0;\n  z-index: 1000;\n}\n\n.navbar__mobile-overlay {\n  position: absolute;\n  inset: 0;\n  background: rgba(0, 0, 0, 0.5);\n}\n\n.navbar__mobile-nav {\n  position: absolute;\n  top: 0;\n  right: 0;\n  width: min(320px, 80vw);\n  height: 100%;\n  background: var(--color-bg-primary);\n  padding: 80px 24px 24px;\n  overflow-y: auto;\n}\n\n.navbar__mobile-list {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n/* 动画 */\n.mobile-menu-enter-active,\n.mobile-menu-leave-active {\n  transition: all 0.3s ease;\n}\n\n.mobile-menu-enter-active .navbar__mobile-nav,\n.mobile-menu-leave-active .navbar__mobile-nav {\n  transition: transform 0.3s ease;\n}\n\n.mobile-menu-enter-from,\n.mobile-menu-leave-to {\n  opacity: 0;\n}\n\n.mobile-menu-enter-from .navbar__mobile-nav,\n.mobile-menu-leave-to .navbar__mobile-nav {\n  transform: translateX(100%);\n}\n\u003C/style>\n","vue",[137],"language-vue",[139],{"type":24,"tag":105,"props":140,"children":141},{"__ignoreMap":7},[142],{"type":30,"value":134},{"type":24,"tag":94,"props":144,"children":146},{"id":145},"下拉菜单组件",[147],{"type":30,"value":145},{"type":24,"tag":100,"props":149,"children":152},{"code":150,"language":135,"meta":7,"className":151},"\u003C!-- components/navigation/NavDropdown.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface NavItem {\n  label: string\n  href?: string\n  to?: string\n  children?: NavItem[]\n  icon?: string\n  description?: string\n}\n\ninterface Props {\n  item: NavItem\n  isActive?: boolean\n}\n\nconst props = defineProps\u003CProps>()\n\nconst isOpen = ref(false)\nconst dropdownRef = ref\u003CHTMLElement>()\n\nlet closeTimeout: ReturnType\u003Ctypeof setTimeout>\n\nconst handleMouseEnter = () => {\n  clearTimeout(closeTimeout)\n  isOpen.value = true\n}\n\nconst handleMouseLeave = () => {\n  closeTimeout = setTimeout(() => {\n    isOpen.value = false\n  }, 150)\n}\n\n// 点击外部关闭\nonClickOutside(dropdownRef, () => {\n  isOpen.value = false\n})\n\n// 键盘支持\nconst handleKeydown = (e: KeyboardEvent) => {\n  switch (e.key) {\n    case 'Enter':\n    case ' ':\n      e.preventDefault()\n      isOpen.value = !isOpen.value\n      break\n    case 'Escape':\n      isOpen.value = false\n      break\n    case 'ArrowDown':\n      e.preventDefault()\n      if (!isOpen.value) {\n        isOpen.value = true\n      }\n      break\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv\n    ref=\"dropdownRef\"\n    class=\"nav-dropdown\"\n    :class=\"{ 'nav-dropdown--open': isOpen }\"\n    @mouseenter=\"handleMouseEnter\"\n    @mouseleave=\"handleMouseLeave\"\n  >\n    \u003C!-- 触发器 -->\n    \u003Cbutton\n      class=\"nav-dropdown__trigger\"\n      :class=\"{ 'nav-dropdown__trigger--active': isActive }\"\n      :aria-expanded=\"isOpen\"\n      :aria-haspopup=\"true\"\n      @keydown=\"handleKeydown\"\n    >\n      \u003CIcon v-if=\"item.icon\" :name=\"item.icon\" />\n      {{ item.label }}\n      \u003CIcon\n        name=\"chevron-down\"\n        class=\"nav-dropdown__arrow\"\n        :class=\"{ 'nav-dropdown__arrow--rotated': isOpen }\"\n      />\n    \u003C/button>\n    \n    \u003C!-- 下拉面板 -->\n    \u003CTransition name=\"dropdown\">\n      \u003Cdiv v-if=\"isOpen\" class=\"nav-dropdown__panel\">\n        \u003Cul class=\"nav-dropdown__list\" role=\"menu\">\n          \u003Cli\n            v-for=\"child in item.children\"\n            :key=\"child.label\"\n            role=\"menuitem\"\n          >\n            \u003CNuxtLink\n              v-if=\"child.to\"\n              :to=\"child.to\"\n              class=\"nav-dropdown__item\"\n              @click=\"isOpen = false\"\n            >\n              \u003CIcon v-if=\"child.icon\" :name=\"child.icon\" class=\"nav-dropdown__item-icon\" />\n              \u003Cdiv class=\"nav-dropdown__item-content\">\n                \u003Cspan class=\"nav-dropdown__item-label\">{{ child.label }}\u003C/span>\n                \u003Cspan v-if=\"child.description\" class=\"nav-dropdown__item-desc\">\n                  {{ child.description }}\n                \u003C/span>\n              \u003C/div>\n            \u003C/NuxtLink>\n            \n            \u003Ca\n              v-else-if=\"child.href\"\n              :href=\"child.href\"\n              class=\"nav-dropdown__item\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              \u003CIcon v-if=\"child.icon\" :name=\"child.icon\" class=\"nav-dropdown__item-icon\" />\n              \u003Cdiv class=\"nav-dropdown__item-content\">\n                \u003Cspan class=\"nav-dropdown__item-label\">{{ child.label }}\u003C/span>\n                \u003Cspan v-if=\"child.description\" class=\"nav-dropdown__item-desc\">\n                  {{ child.description }}\n                \u003C/span>\n              \u003C/div>\n              \u003CIcon name=\"external-link\" class=\"nav-dropdown__external\" />\n            \u003C/a>\n          \u003C/li>\n        \u003C/ul>\n      \u003C/div>\n    \u003C/Transition>\n  \u003C/div>\n\u003C/template>\n\n\u003Cstyle scoped>\n.nav-dropdown {\n  position: relative;\n}\n\n.nav-dropdown__trigger {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 8px 16px;\n  font-size: 14px;\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  background: transparent;\n  border: none;\n  border-radius: var(--radius-md);\n  cursor: pointer;\n  transition: all 0.2s ease;\n}\n\n.nav-dropdown__trigger:hover,\n.nav-dropdown--open .nav-dropdown__trigger {\n  color: var(--color-text-primary);\n  background: var(--color-bg-secondary);\n}\n\n.nav-dropdown__trigger--active {\n  color: var(--color-primary);\n}\n\n.nav-dropdown__arrow {\n  width: 16px;\n  height: 16px;\n  transition: transform 0.2s ease;\n}\n\n.nav-dropdown__arrow--rotated {\n  transform: rotate(180deg);\n}\n\n.nav-dropdown__panel {\n  position: absolute;\n  top: 100%;\n  left: 50%;\n  transform: translateX(-50%);\n  min-width: 240px;\n  margin-top: 8px;\n  background: var(--color-bg-primary);\n  border: 1px solid var(--color-border-light);\n  border-radius: var(--radius-lg);\n  box-shadow: var(--shadow-lg);\n  overflow: hidden;\n  z-index: 100;\n}\n\n.nav-dropdown__list {\n  list-style: none;\n  margin: 0;\n  padding: 8px;\n}\n\n.nav-dropdown__item {\n  display: flex;\n  align-items: flex-start;\n  gap: 12px;\n  padding: 12px;\n  text-decoration: none;\n  border-radius: var(--radius-md);\n  transition: background 0.15s ease;\n}\n\n.nav-dropdown__item:hover {\n  background: var(--color-bg-secondary);\n}\n\n.nav-dropdown__item-icon {\n  width: 20px;\n  height: 20px;\n  color: var(--color-primary);\n  flex-shrink: 0;\n  margin-top: 2px;\n}\n\n.nav-dropdown__item-content {\n  flex: 1;\n}\n\n.nav-dropdown__item-label {\n  display: block;\n  font-size: 14px;\n  font-weight: 500;\n  color: var(--color-text-primary);\n}\n\n.nav-dropdown__item-desc {\n  display: block;\n  font-size: 12px;\n  color: var(--color-text-secondary);\n  margin-top: 2px;\n}\n\n.nav-dropdown__external {\n  width: 14px;\n  height: 14px;\n  color: var(--color-text-tertiary);\n  flex-shrink: 0;\n}\n\n/* 动画 */\n.dropdown-enter-active,\n.dropdown-leave-active {\n  transition: all 0.2s ease;\n}\n\n.dropdown-enter-from,\n.dropdown-leave-to {\n  opacity: 0;\n  transform: translateX(-50%) translateY(-8px);\n}\n\u003C/style>\n",[137],[153],{"type":24,"tag":105,"props":154,"children":155},{"__ignoreMap":7},[156],{"type":30,"value":150},{"type":24,"tag":25,"props":158,"children":160},{"id":159},"侧边导航组件",[161],{"type":30,"value":159},{"type":24,"tag":94,"props":163,"children":165},{"id":164},"侧边栏导航",[166],{"type":30,"value":164},{"type":24,"tag":100,"props":168,"children":171},{"code":169,"language":135,"meta":7,"className":170},"\u003C!-- components/navigation/Sidebar.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface NavItem {\n  label: string\n  to?: string\n  icon?: string\n  badge?: string | number\n  children?: NavItem[]\n}\n\ninterface Props {\n  items: NavItem[]\n  collapsed?: boolean\n  width?: number\n  collapsedWidth?: number\n}\n\nconst props = withDefaults(defineProps\u003CProps>(), {\n  collapsed: false,\n  width: 256,\n  collapsedWidth: 64,\n})\n\nconst emit = defineEmits\u003C{\n  'update:collapsed': [value: boolean]\n}>()\n\nconst route = useRoute()\nconst expandedItems = ref\u003CSet\u003Cstring>>(new Set())\n\n// 当前宽度\nconst currentWidth = computed(() => \n  props.collapsed ? props.collapsedWidth : props.width\n)\n\n// 检查是否激活\nconst isActive = (item: NavItem): boolean => {\n  if (item.to) {\n    return route.path === item.to\n  }\n  return false\n}\n\n// 检查是否有激活的子项\nconst hasActiveChild = (item: NavItem): boolean => {\n  if (!item.children) return false\n  return item.children.some(child => \n    isActive(child) || hasActiveChild(child)\n  )\n}\n\n// 切换展开\nconst toggleExpand = (label: string) => {\n  if (expandedItems.value.has(label)) {\n    expandedItems.value.delete(label)\n  } else {\n    expandedItems.value.add(label)\n  }\n}\n\n// 检查是否展开\nconst isExpanded = (label: string): boolean => {\n  return expandedItems.value.has(label)\n}\n\n// 自动展开包含激活项的父级\nwatchEffect(() => {\n  props.items.forEach(item => {\n    if (item.children && hasActiveChild(item)) {\n      expandedItems.value.add(item.label)\n    }\n  })\n})\n\n// 切换收起状态\nconst toggleCollapse = () => {\n  emit('update:collapsed', !props.collapsed)\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Caside\n    class=\"sidebar\"\n    :class=\"{ 'sidebar--collapsed': collapsed }\"\n    :style=\"{ width: `${currentWidth}px` }\"\n  >\n    \u003C!-- 导航列表 -->\n    \u003Cnav class=\"sidebar__nav\" role=\"navigation\">\n      \u003Cul class=\"sidebar__list\">\n        \u003Cli v-for=\"item in items\" :key=\"item.label\" class=\"sidebar__item\">\n          \u003C!-- 有子菜单 -->\n          \u003Ctemplate v-if=\"item.children\">\n            \u003Cbutton\n              class=\"sidebar__link sidebar__link--expandable\"\n              :class=\"{ 'sidebar__link--expanded': isExpanded(item.label) }\"\n              :aria-expanded=\"isExpanded(item.label)\"\n              @click=\"toggleExpand(item.label)\"\n            >\n              \u003CIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"sidebar__icon\" />\n              \u003Cspan v-if=\"!collapsed\" class=\"sidebar__label\">{{ item.label }}\u003C/span>\n              \u003CIcon\n                v-if=\"!collapsed\"\n                name=\"chevron-right\"\n                class=\"sidebar__arrow\"\n                :class=\"{ 'sidebar__arrow--rotated': isExpanded(item.label) }\"\n              />\n            \u003C/button>\n            \n            \u003C!-- 子菜单 -->\n            \u003CTransition name=\"expand\">\n              \u003Cul\n                v-if=\"isExpanded(item.label) && !collapsed\"\n                class=\"sidebar__submenu\"\n              >\n                \u003Cli v-for=\"child in item.children\" :key=\"child.label\">\n                  \u003CNuxtLink\n                    :to=\"child.to\"\n                    class=\"sidebar__sublink\"\n                    :class=\"{ 'sidebar__sublink--active': isActive(child) }\"\n                  >\n                    \u003CIcon v-if=\"child.icon\" :name=\"child.icon\" class=\"sidebar__subicon\" />\n                    \u003Cspan class=\"sidebar__sublabel\">{{ child.label }}\u003C/span>\n                    \u003Cspan v-if=\"child.badge\" class=\"sidebar__badge\">{{ child.badge }}\u003C/span>\n                  \u003C/NuxtLink>\n                \u003C/li>\n              \u003C/ul>\n            \u003C/Transition>\n          \u003C/template>\n          \n          \u003C!-- 无子菜单 -->\n          \u003CNuxtLink\n            v-else\n            :to=\"item.to\"\n            class=\"sidebar__link\"\n            :class=\"{ 'sidebar__link--active': isActive(item) }\"\n          >\n            \u003CIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"sidebar__icon\" />\n            \u003Cspan v-if=\"!collapsed\" class=\"sidebar__label\">{{ item.label }}\u003C/span>\n            \u003Cspan v-if=\"!collapsed && item.badge\" class=\"sidebar__badge\">\n              {{ item.badge }}\n            \u003C/span>\n          \u003C/NuxtLink>\n        \u003C/li>\n      \u003C/ul>\n    \u003C/nav>\n    \n    \u003C!-- 收起按钮 -->\n    \u003Cbutton\n      class=\"sidebar__toggle\"\n      :aria-label=\"collapsed ? '展开侧边栏' : '收起侧边栏'\"\n      @click=\"toggleCollapse\"\n    >\n      \u003CIcon\n        name=\"chevrons-left\"\n        class=\"sidebar__toggle-icon\"\n        :class=\"{ 'sidebar__toggle-icon--rotated': collapsed }\"\n      />\n    \u003C/button>\n  \u003C/aside>\n\u003C/template>\n\n\u003Cstyle scoped>\n.sidebar {\n  display: flex;\n  flex-direction: column;\n  height: 100vh;\n  background: var(--color-bg-primary);\n  border-right: 1px solid var(--color-border-light);\n  transition: width 0.3s ease;\n  overflow: hidden;\n}\n\n.sidebar--collapsed {\n  width: 64px;\n}\n\n.sidebar__nav {\n  flex: 1;\n  overflow-y: auto;\n  padding: 16px 12px;\n}\n\n.sidebar__list {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n.sidebar__item {\n  margin-bottom: 4px;\n}\n\n.sidebar__link {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  width: 100%;\n  padding: 10px 12px;\n  font-size: 14px;\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  text-decoration: none;\n  background: transparent;\n  border: none;\n  border-radius: var(--radius-md);\n  cursor: pointer;\n  transition: all 0.15s ease;\n}\n\n.sidebar__link:hover {\n  color: var(--color-text-primary);\n  background: var(--color-bg-secondary);\n}\n\n.sidebar__link--active {\n  color: var(--color-primary);\n  background: var(--color-primary-light);\n}\n\n.sidebar__icon {\n  width: 20px;\n  height: 20px;\n  flex-shrink: 0;\n}\n\n.sidebar__label {\n  flex: 1;\n  text-align: left;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.sidebar__arrow {\n  width: 16px;\n  height: 16px;\n  transition: transform 0.2s ease;\n}\n\n.sidebar__arrow--rotated {\n  transform: rotate(90deg);\n}\n\n.sidebar__badge {\n  padding: 2px 8px;\n  font-size: 12px;\n  font-weight: 500;\n  color: var(--color-primary);\n  background: var(--color-primary-light);\n  border-radius: 9999px;\n}\n\n/* 子菜单 */\n.sidebar__submenu {\n  list-style: none;\n  margin: 4px 0 0;\n  padding: 0 0 0 32px;\n}\n\n.sidebar__sublink {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 8px 12px;\n  font-size: 13px;\n  color: var(--color-text-secondary);\n  text-decoration: none;\n  border-radius: var(--radius-md);\n  transition: all 0.15s ease;\n}\n\n.sidebar__sublink:hover {\n  color: var(--color-text-primary);\n  background: var(--color-bg-secondary);\n}\n\n.sidebar__sublink--active {\n  color: var(--color-primary);\n  font-weight: 500;\n}\n\n.sidebar__subicon {\n  width: 16px;\n  height: 16px;\n}\n\n/* 收起按钮 */\n.sidebar__toggle {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 48px;\n  border: none;\n  border-top: 1px solid var(--color-border-light);\n  background: transparent;\n  cursor: pointer;\n  transition: background 0.15s ease;\n}\n\n.sidebar__toggle:hover {\n  background: var(--color-bg-secondary);\n}\n\n.sidebar__toggle-icon {\n  width: 20px;\n  height: 20px;\n  color: var(--color-text-secondary);\n  transition: transform 0.3s ease;\n}\n\n.sidebar__toggle-icon--rotated {\n  transform: rotate(180deg);\n}\n\n/* 展开动画 */\n.expand-enter-active,\n.expand-leave-active {\n  transition: all 0.3s ease;\n  overflow: hidden;\n}\n\n.expand-enter-from,\n.expand-leave-to {\n  opacity: 0;\n  max-height: 0;\n}\n\n.expand-enter-to,\n.expand-leave-from {\n  max-height: 500px;\n}\n\u003C/style>\n",[137],[172],{"type":24,"tag":105,"props":173,"children":174},{"__ignoreMap":7},[175],{"type":30,"value":169},{"type":24,"tag":25,"props":177,"children":179},{"id":178},"面包屑导航",[180],{"type":30,"value":178},{"type":24,"tag":94,"props":182,"children":184},{"id":183},"面包屑组件",[185],{"type":30,"value":183},{"type":24,"tag":100,"props":187,"children":190},{"code":188,"language":135,"meta":7,"className":189},"\u003C!-- components/navigation/Breadcrumb.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface BreadcrumbItem {\n  label: string\n  to?: string\n  icon?: string\n}\n\ninterface Props {\n  items: BreadcrumbItem[]\n  separator?: string\n  showHome?: boolean\n  homeIcon?: string\n  maxItems?: number\n}\n\nconst props = withDefaults(defineProps\u003CProps>(), {\n  separator: '/',\n  showHome: true,\n  homeIcon: 'home',\n  maxItems: 0,\n})\n\n// 处理显示项\nconst displayItems = computed(() => {\n  let items = props.items\n  \n  // 添加首页\n  if (props.showHome) {\n    items = [{ label: '首页', to: '/', icon: props.homeIcon }, ...items]\n  }\n  \n  // 截断处理\n  if (props.maxItems > 0 && items.length > props.maxItems) {\n    const firstItem = items[0]\n    const lastItems = items.slice(-props.maxItems + 1)\n    return [firstItem, { label: '...', collapsed: true }, ...lastItems]\n  }\n  \n  return items\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cnav class=\"breadcrumb\" aria-label=\"面包屑导航\">\n    \u003Col class=\"breadcrumb__list\" itemscope itemtype=\"https://schema.org/BreadcrumbList\">\n      \u003Cli\n        v-for=\"(item, index) in displayItems\"\n        :key=\"index\"\n        class=\"breadcrumb__item\"\n        itemprop=\"itemListElement\"\n        itemscope\n        itemtype=\"https://schema.org/ListItem\"\n      >\n        \u003C!-- 分隔符 -->\n        \u003Cspan\n          v-if=\"index > 0\"\n          class=\"breadcrumb__separator\"\n          aria-hidden=\"true\"\n        >\n          \u003Cslot name=\"separator\">\n            {{ separator }}\n          \u003C/slot>\n        \u003C/span>\n        \n        \u003C!-- 折叠项 -->\n        \u003Cspan v-if=\"item.collapsed\" class=\"breadcrumb__collapsed\">\n          ...\n        \u003C/span>\n        \n        \u003C!-- 链接项 -->\n        \u003CNuxtLink\n          v-else-if=\"item.to && index \u003C displayItems.length - 1\"\n          :to=\"item.to\"\n          class=\"breadcrumb__link\"\n          itemprop=\"item\"\n        >\n          \u003CIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"breadcrumb__icon\" />\n          \u003Cspan itemprop=\"name\">{{ item.label }}\u003C/span>\n        \u003C/NuxtLink>\n        \n        \u003C!-- 当前项（最后一项） -->\n        \u003Cspan\n          v-else\n          class=\"breadcrumb__current\"\n          aria-current=\"page\"\n          itemprop=\"name\"\n        >\n          \u003CIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"breadcrumb__icon\" />\n          {{ item.label }}\n        \u003C/span>\n        \n        \u003Cmeta itemprop=\"position\" :content=\"String(index + 1)\" />\n      \u003C/li>\n    \u003C/ol>\n  \u003C/nav>\n\u003C/template>\n\n\u003Cstyle scoped>\n.breadcrumb {\n  padding: 12px 0;\n}\n\n.breadcrumb__list {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n  gap: 4px;\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n.breadcrumb__item {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n}\n\n.breadcrumb__separator {\n  color: var(--color-text-tertiary);\n  margin: 0 4px;\n}\n\n.breadcrumb__link {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  font-size: 14px;\n  color: var(--color-text-secondary);\n  text-decoration: none;\n  transition: color 0.15s ease;\n}\n\n.breadcrumb__link:hover {\n  color: var(--color-primary);\n}\n\n.breadcrumb__current {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  font-size: 14px;\n  font-weight: 500;\n  color: var(--color-text-primary);\n}\n\n.breadcrumb__icon {\n  width: 16px;\n  height: 16px;\n}\n\n.breadcrumb__collapsed {\n  color: var(--color-text-tertiary);\n}\n\u003C/style>\n",[137],[191],{"type":24,"tag":105,"props":192,"children":193},{"__ignoreMap":7},[194],{"type":30,"value":188},{"type":24,"tag":94,"props":196,"children":198},{"id":197},"自动生成面包屑",[199],{"type":30,"value":197},{"type":24,"tag":100,"props":201,"children":206},{"code":202,"language":203,"meta":7,"className":204},"// composables/useBreadcrumb.ts\ninterface BreadcrumbItem {\n  label: string\n  to: string\n}\n\nexport function useBreadcrumb() {\n  const route = useRoute()\n  \n  // 路由名称映射\n  const routeLabels: Record\u003Cstring, string> = {\n    'topics': '主题',\n    'seo': 'SEO优化',\n    'performance': '性能优化',\n    'nuxt': 'Nuxt开发',\n    'frontend': '前端开发',\n    'design': '设计系统',\n  }\n  \n  // 根据路由生成面包屑\n  const breadcrumbs = computed\u003CBreadcrumbItem[]>(() => {\n    const paths = route.path.split('/').filter(Boolean)\n    const items: BreadcrumbItem[] = []\n    \n    let currentPath = ''\n    \n    for (const segment of paths) {\n      currentPath += `/${segment}`\n      \n      // 获取标签\n      const label = routeLabels[segment] || \n                    route.meta.title as string ||\n                    segment.replace(/-/g, ' ')\n      \n      items.push({\n        label,\n        to: currentPath,\n      })\n    }\n    \n    return items\n  })\n  \n  return {\n    breadcrumbs,\n  }\n}\n","typescript",[205],"language-typescript",[207],{"type":24,"tag":105,"props":208,"children":209},{"__ignoreMap":7},[210],{"type":30,"value":202},{"type":24,"tag":25,"props":212,"children":214},{"id":213},"标签页导航",[215],{"type":30,"value":213},{"type":24,"tag":94,"props":217,"children":219},{"id":218},"标签页组件",[220],{"type":30,"value":218},{"type":24,"tag":100,"props":222,"children":225},{"code":223,"language":135,"meta":7,"className":224},"\u003C!-- components/navigation/Tabs.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface TabItem {\n  key: string\n  label: string\n  icon?: string\n  disabled?: boolean\n  badge?: string | number\n}\n\ninterface Props {\n  items: TabItem[]\n  modelValue?: string\n  variant?: 'line' | 'card' | 'pill'\n  size?: 'sm' | 'md' | 'lg'\n}\n\nconst props = withDefaults(defineProps\u003CProps>(), {\n  variant: 'line',\n  size: 'md',\n})\n\nconst emit = defineEmits\u003C{\n  'update:modelValue': [value: string]\n  'change': [value: string]\n}>()\n\nconst activeKey = computed({\n  get: () => props.modelValue || props.items[0]?.key,\n  set: (val) => {\n    emit('update:modelValue', val)\n    emit('change', val)\n  },\n})\n\nconst indicatorStyle = ref({})\nconst tabRefs = ref\u003CHTMLElement[]>([])\n\n// 更新指示器位置\nconst updateIndicator = () => {\n  const activeIndex = props.items.findIndex(item => item.key === activeKey.value)\n  const activeTab = tabRefs.value[activeIndex]\n  \n  if (activeTab && props.variant === 'line') {\n    indicatorStyle.value = {\n      width: `${activeTab.offsetWidth}px`,\n      transform: `translateX(${activeTab.offsetLeft}px)`,\n    }\n  }\n}\n\n// 监听变化更新指示器\nwatch([activeKey, () => props.items], () => {\n  nextTick(updateIndicator)\n})\n\nonMounted(() => {\n  nextTick(updateIndicator)\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv\n    class=\"tabs\"\n    :class=\"[`tabs--${variant}`, `tabs--${size}`]\"\n  >\n    \u003Cdiv class=\"tabs__nav\" role=\"tablist\">\n      \u003Cbutton\n        v-for=\"(item, index) in items\"\n        :key=\"item.key\"\n        :ref=\"(el) => tabRefs[index] = el as HTMLElement\"\n        class=\"tabs__tab\"\n        :class=\"{\n          'tabs__tab--active': item.key === activeKey,\n          'tabs__tab--disabled': item.disabled,\n        }\"\n        role=\"tab\"\n        :aria-selected=\"item.key === activeKey\"\n        :aria-disabled=\"item.disabled\"\n        :tabindex=\"item.disabled ? -1 : 0\"\n        @click=\"!item.disabled && (activeKey = item.key)\"\n      >\n        \u003CIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"tabs__icon\" />\n        \u003Cspan class=\"tabs__label\">{{ item.label }}\u003C/span>\n        \u003Cspan v-if=\"item.badge\" class=\"tabs__badge\">{{ item.badge }}\u003C/span>\n      \u003C/button>\n      \n      \u003C!-- 滑动指示器（线条变体） -->\n      \u003Cdiv\n        v-if=\"variant === 'line'\"\n        class=\"tabs__indicator\"\n        :style=\"indicatorStyle\"\n      />\n    \u003C/div>\n    \n    \u003C!-- 内容区域 -->\n    \u003Cdiv class=\"tabs__content\" role=\"tabpanel\">\n      \u003Cslot :active-key=\"activeKey\" />\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cstyle scoped>\n.tabs {\n  width: 100%;\n}\n\n.tabs__nav {\n  position: relative;\n  display: flex;\n  gap: 4px;\n}\n\n/* 线条变体 */\n.tabs--line .tabs__nav {\n  border-bottom: 1px solid var(--color-border-light);\n}\n\n.tabs--line .tabs__tab {\n  padding: 12px 16px;\n  background: transparent;\n  border: none;\n  border-bottom: 2px solid transparent;\n  margin-bottom: -1px;\n}\n\n.tabs--line .tabs__tab--active {\n  color: var(--color-primary);\n}\n\n.tabs--line .tabs__indicator {\n  position: absolute;\n  bottom: 0;\n  height: 2px;\n  background: var(--color-primary);\n  transition: all 0.3s ease;\n}\n\n/* 卡片变体 */\n.tabs--card .tabs__nav {\n  background: var(--color-bg-secondary);\n  padding: 4px;\n  border-radius: var(--radius-lg);\n}\n\n.tabs--card .tabs__tab {\n  padding: 8px 16px;\n  background: transparent;\n  border: none;\n  border-radius: var(--radius-md);\n}\n\n.tabs--card .tabs__tab--active {\n  background: var(--color-bg-primary);\n  box-shadow: var(--shadow-sm);\n}\n\n/* 药片变体 */\n.tabs--pill .tabs__tab {\n  padding: 8px 20px;\n  background: transparent;\n  border: 1px solid var(--color-border);\n  border-radius: 9999px;\n}\n\n.tabs--pill .tabs__tab--active {\n  background: var(--color-primary);\n  border-color: var(--color-primary);\n  color: white;\n}\n\n/* 通用样式 */\n.tabs__tab {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  font-size: 14px;\n  font-weight: 500;\n  color: var(--color-text-secondary);\n  cursor: pointer;\n  transition: all 0.2s ease;\n}\n\n.tabs__tab:hover:not(.tabs__tab--disabled) {\n  color: var(--color-text-primary);\n}\n\n.tabs__tab--disabled {\n  opacity: 0.5;\n  cursor: not-allowed;\n}\n\n.tabs__icon {\n  width: 16px;\n  height: 16px;\n}\n\n.tabs__badge {\n  padding: 2px 6px;\n  font-size: 11px;\n  background: var(--color-bg-tertiary);\n  border-radius: 9999px;\n}\n\n.tabs__tab--active .tabs__badge {\n  background: var(--color-primary-light);\n  color: var(--color-primary);\n}\n\n/* 尺寸变体 */\n.tabs--sm .tabs__tab {\n  padding: 8px 12px;\n  font-size: 13px;\n}\n\n.tabs--lg .tabs__tab {\n  padding: 14px 20px;\n  font-size: 15px;\n}\n\n.tabs__content {\n  padding-top: 16px;\n}\n\u003C/style>\n",[137],[226],{"type":24,"tag":105,"props":227,"children":228},{"__ignoreMap":7},[229],{"type":30,"value":223},{"type":24,"tag":25,"props":231,"children":233},{"id":232},"移动端底部导航",[234],{"type":30,"value":232},{"type":24,"tag":100,"props":236,"children":239},{"code":237,"language":135,"meta":7,"className":238},"\u003C!-- components/navigation/BottomNav.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface NavItem {\n  key: string\n  label: string\n  to: string\n  icon: string\n  activeIcon?: string\n  badge?: string | number\n}\n\ninterface Props {\n  items: NavItem[]\n}\n\nconst props = defineProps\u003CProps>()\n\nconst route = useRoute()\n\nconst isActive = (item: NavItem): boolean => {\n  return route.path === item.to || route.path.startsWith(item.to + '/')\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cnav class=\"bottom-nav\" role=\"navigation\" aria-label=\"底部导航\">\n    \u003CNuxtLink\n      v-for=\"item in items\"\n      :key=\"item.key\"\n      :to=\"item.to\"\n      class=\"bottom-nav__item\"\n      :class=\"{ 'bottom-nav__item--active': isActive(item) }\"\n    >\n      \u003Cdiv class=\"bottom-nav__icon-wrapper\">\n        \u003CIcon\n          :name=\"isActive(item) && item.activeIcon ? item.activeIcon : item.icon\"\n          class=\"bottom-nav__icon\"\n        />\n        \u003Cspan v-if=\"item.badge\" class=\"bottom-nav__badge\">\n          {{ item.badge }}\n        \u003C/span>\n      \u003C/div>\n      \u003Cspan class=\"bottom-nav__label\">{{ item.label }}\u003C/span>\n    \u003C/NuxtLink>\n  \u003C/nav>\n\u003C/template>\n\n\u003Cstyle scoped>\n.bottom-nav {\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  display: flex;\n  justify-content: space-around;\n  height: 64px;\n  background: var(--color-bg-primary);\n  border-top: 1px solid var(--color-border-light);\n  padding-bottom: env(safe-area-inset-bottom);\n  z-index: 100;\n}\n\n@media (min-width: 768px) {\n  .bottom-nav {\n    display: none;\n  }\n}\n\n.bottom-nav__item {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  flex: 1;\n  padding: 8px 4px;\n  text-decoration: none;\n  color: var(--color-text-secondary);\n  transition: color 0.2s ease;\n}\n\n.bottom-nav__item--active {\n  color: var(--color-primary);\n}\n\n.bottom-nav__icon-wrapper {\n  position: relative;\n}\n\n.bottom-nav__icon {\n  width: 24px;\n  height: 24px;\n}\n\n.bottom-nav__badge {\n  position: absolute;\n  top: -4px;\n  right: -8px;\n  min-width: 16px;\n  height: 16px;\n  padding: 0 4px;\n  font-size: 10px;\n  font-weight: 600;\n  color: white;\n  background: var(--color-error);\n  border-radius: 8px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.bottom-nav__label {\n  font-size: 11px;\n  margin-top: 4px;\n}\n\u003C/style>\n",[137],[240],{"type":24,"tag":105,"props":241,"children":242},{"__ignoreMap":7},[243],{"type":30,"value":237},{"type":24,"tag":25,"props":245,"children":247},{"id":246},"导航可访问性",[248],{"type":30,"value":246},{"type":24,"tag":94,"props":250,"children":252},{"id":251},"键盘导航支持",[253],{"type":30,"value":251},{"type":24,"tag":100,"props":255,"children":258},{"code":256,"language":203,"meta":7,"className":257},"// composables/useNavigationA11y.ts\nexport function useNavigationA11y(containerRef: Ref\u003CHTMLElement | undefined>) {\n  // 获取所有可导航项\n  const getNavigableItems = (): HTMLElement[] => {\n    if (!containerRef.value) return []\n    return Array.from(\n      containerRef.value.querySelectorAll\u003CHTMLElement>(\n        'a, button:not([disabled]), [tabindex=\"0\"]'\n      )\n    )\n  }\n  \n  // 当前焦点索引\n  const focusIndex = ref(-1)\n  \n  // 聚焦指定索引\n  const focusItem = (index: number) => {\n    const items = getNavigableItems()\n    if (index >= 0 && index \u003C items.length) {\n      items[index].focus()\n      focusIndex.value = index\n    }\n  }\n  \n  // 键盘事件处理\n  const handleKeydown = (e: KeyboardEvent) => {\n    const items = getNavigableItems()\n    const currentIndex = items.indexOf(document.activeElement as HTMLElement)\n    \n    switch (e.key) {\n      case 'ArrowDown':\n      case 'ArrowRight':\n        e.preventDefault()\n        focusItem((currentIndex + 1) % items.length)\n        break\n        \n      case 'ArrowUp':\n      case 'ArrowLeft':\n        e.preventDefault()\n        focusItem((currentIndex - 1 + items.length) % items.length)\n        break\n        \n      case 'Home':\n        e.preventDefault()\n        focusItem(0)\n        break\n        \n      case 'End':\n        e.preventDefault()\n        focusItem(items.length - 1)\n        break\n    }\n  }\n  \n  // 绑定事件\n  onMounted(() => {\n    containerRef.value?.addEventListener('keydown', handleKeydown)\n  })\n  \n  onUnmounted(() => {\n    containerRef.value?.removeEventListener('keydown', handleKeydown)\n  })\n  \n  return {\n    focusIndex,\n    focusItem,\n  }\n}\n",[205],[259],{"type":24,"tag":105,"props":260,"children":261},{"__ignoreMap":7},[262],{"type":30,"value":256},{"type":24,"tag":94,"props":264,"children":266},{"id":265},"跳过导航链接",[267],{"type":30,"value":265},{"type":24,"tag":100,"props":269,"children":272},{"code":270,"language":135,"meta":7,"className":271},"\u003C!-- components/navigation/SkipLink.vue -->\n\u003Ctemplate>\n  \u003Ca href=\"#main-content\" class=\"skip-link\">\n    跳转到主要内容\n  \u003C/a>\n\u003C/template>\n\n\u003Cstyle scoped>\n.skip-link {\n  position: fixed;\n  top: -100%;\n  left: 50%;\n  transform: translateX(-50%);\n  padding: 12px 24px;\n  background: var(--color-primary);\n  color: white;\n  font-weight: 500;\n  text-decoration: none;\n  border-radius: var(--radius-md);\n  z-index: 9999;\n  transition: top 0.3s ease;\n}\n\n.skip-link:focus {\n  top: 16px;\n}\n\u003C/style>\n",[137],[273],{"type":24,"tag":105,"props":274,"children":275},{"__ignoreMap":7},[276],{"type":30,"value":270},{"type":24,"tag":25,"props":278,"children":280},{"id":279},"最佳实践总结",[281],{"type":30,"value":279},{"type":24,"tag":94,"props":283,"children":285},{"id":284},"导航设计清单",[286],{"type":30,"value":284},{"type":24,"tag":288,"props":289,"children":290},"table",{},[291,310],{"type":24,"tag":292,"props":293,"children":294},"thead",{},[295],{"type":24,"tag":296,"props":297,"children":298},"tr",{},[299,305],{"type":24,"tag":300,"props":301,"children":302},"th",{},[303],{"type":30,"value":304},"类别",{"type":24,"tag":300,"props":306,"children":307},{},[308],{"type":30,"value":309},"最佳实践",{"type":24,"tag":311,"props":312,"children":313},"tbody",{},[314,331,347,363,379,395,411],{"type":24,"tag":296,"props":315,"children":316},{},[317,326],{"type":24,"tag":318,"props":319,"children":320},"td",{},[321],{"type":24,"tag":46,"props":322,"children":323},{},[324],{"type":30,"value":325},"层级",{"type":24,"tag":318,"props":327,"children":328},{},[329],{"type":30,"value":330},"最多3层，扁平化结构",{"type":24,"tag":296,"props":332,"children":333},{},[334,342],{"type":24,"tag":318,"props":335,"children":336},{},[337],{"type":24,"tag":46,"props":338,"children":339},{},[340],{"type":30,"value":341},"标签",{"type":24,"tag":318,"props":343,"children":344},{},[345],{"type":30,"value":346},"简洁明确，2-4个字",{"type":24,"tag":296,"props":348,"children":349},{},[350,358],{"type":24,"tag":318,"props":351,"children":352},{},[353],{"type":24,"tag":46,"props":354,"children":355},{},[356],{"type":30,"value":357},"图标",{"type":24,"tag":318,"props":359,"children":360},{},[361],{"type":30,"value":362},"统一风格，辅助文字",{"type":24,"tag":296,"props":364,"children":365},{},[366,374],{"type":24,"tag":318,"props":367,"children":368},{},[369],{"type":24,"tag":46,"props":370,"children":371},{},[372],{"type":30,"value":373},"状态",{"type":24,"tag":318,"props":375,"children":376},{},[377],{"type":30,"value":378},"清晰的激活态反馈",{"type":24,"tag":296,"props":380,"children":381},{},[382,390],{"type":24,"tag":318,"props":383,"children":384},{},[385],{"type":24,"tag":46,"props":386,"children":387},{},[388],{"type":30,"value":389},"响应式",{"type":24,"tag":318,"props":391,"children":392},{},[393],{"type":30,"value":394},"移动端使用汉堡/底部导航",{"type":24,"tag":296,"props":396,"children":397},{},[398,406],{"type":24,"tag":318,"props":399,"children":400},{},[401],{"type":24,"tag":46,"props":402,"children":403},{},[404],{"type":30,"value":405},"可访问",{"type":24,"tag":318,"props":407,"children":408},{},[409],{"type":30,"value":410},"支持键盘导航，ARIA标签",{"type":24,"tag":296,"props":412,"children":413},{},[414,422],{"type":24,"tag":318,"props":415,"children":416},{},[417],{"type":24,"tag":46,"props":418,"children":419},{},[420],{"type":30,"value":421},"性能",{"type":24,"tag":318,"props":423,"children":424},{},[425],{"type":30,"value":426},"延迟加载子菜单",{"type":24,"tag":94,"props":428,"children":430},{"id":429},"组件设计原则",[431],{"type":30,"value":429},{"type":24,"tag":433,"props":434,"children":435},"ol",{},[436,446,456,466,476],{"type":24,"tag":42,"props":437,"children":438},{},[439,444],{"type":24,"tag":46,"props":440,"children":441},{},[442],{"type":30,"value":443},"一致性",{"type":30,"value":445}," - 全站统一的导航模式",{"type":24,"tag":42,"props":447,"children":448},{},[449,454],{"type":24,"tag":46,"props":450,"children":451},{},[452],{"type":30,"value":453},"可预测",{"type":30,"value":455}," - 用户能预判交互结果",{"type":24,"tag":42,"props":457,"children":458},{},[459,464],{"type":24,"tag":46,"props":460,"children":461},{},[462],{"type":30,"value":463},"反馈性",{"type":30,"value":465}," - 清晰的状态指示",{"type":24,"tag":42,"props":467,"children":468},{},[469,474],{"type":24,"tag":46,"props":470,"children":471},{},[472],{"type":30,"value":473},"灵活性",{"type":30,"value":475}," - 支持多种配置场景",{"type":24,"tag":42,"props":477,"children":478},{},[479,483],{"type":24,"tag":46,"props":480,"children":481},{},[482],{"type":30,"value":405},{"type":30,"value":484}," - 完整的键盘和屏幕阅读器支持",{"type":24,"tag":25,"props":486,"children":488},{"id":487},"总结",[489],{"type":30,"value":487},{"type":24,"tag":32,"props":491,"children":492},{},[493],{"type":30,"value":494},"优秀的导航系统需要：",{"type":24,"tag":38,"props":496,"children":497},{},[498,508,518,528],{"type":24,"tag":42,"props":499,"children":500},{},[501,506],{"type":24,"tag":46,"props":502,"children":503},{},[504],{"type":30,"value":505},"清晰的信息架构",{"type":30,"value":507}," - 合理的层级和分类",{"type":24,"tag":42,"props":509,"children":510},{},[511,516],{"type":24,"tag":46,"props":512,"children":513},{},[514],{"type":30,"value":515},"灵活的组件设计",{"type":30,"value":517}," - 适应不同场景需求",{"type":24,"tag":42,"props":519,"children":520},{},[521,526],{"type":24,"tag":46,"props":522,"children":523},{},[524],{"type":30,"value":525},"完善的响应式",{"type":30,"value":527}," - 桌面端和移动端无缝切换",{"type":24,"tag":42,"props":529,"children":530},{},[531,536],{"type":24,"tag":46,"props":532,"children":533},{},[534],{"type":30,"value":535},"良好的可访问性",{"type":30,"value":537}," - 所有用户都能顺畅使用",{"type":24,"tag":32,"props":539,"children":540},{},[541],{"type":30,"value":542},"通过本文介绍的组件和模式，你可以构建一套专业、统一、易用的导航系统，提升整体用户体验。",{"title":7,"searchDepth":544,"depth":544,"links":545},3,[546,548,552,556,559,563,566,567,571,575],{"id":27,"depth":547,"text":27},2,{"id":90,"depth":547,"text":90,"children":549},[550,551],{"id":96,"depth":544,"text":96},{"id":111,"depth":544,"text":111},{"id":124,"depth":547,"text":124,"children":553},[554,555],{"id":129,"depth":544,"text":129},{"id":145,"depth":544,"text":145},{"id":159,"depth":547,"text":159,"children":557},[558],{"id":164,"depth":544,"text":164},{"id":178,"depth":547,"text":178,"children":560},[561,562],{"id":183,"depth":544,"text":183},{"id":197,"depth":544,"text":197},{"id":213,"depth":547,"text":213,"children":564},[565],{"id":218,"depth":544,"text":218},{"id":232,"depth":547,"text":232},{"id":246,"depth":547,"text":246,"children":568},[569,570],{"id":251,"depth":544,"text":251},{"id":265,"depth":544,"text":265},{"id":279,"depth":547,"text":279,"children":572},[573,574],{"id":284,"depth":544,"text":284},{"id":429,"depth":544,"text":429},{"id":487,"depth":547,"text":487},"markdown","content:topics:design:navigation-component-guide.md","content","topics/design/navigation-component-guide.md","topics/design/navigation-component-guide","md",[583,937,1239],{"_path":584,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":585,"description":586,"keywords":587,"image":592,"author":11,"date":593,"readingTime":594,"topic":5,"body":595,"_type":576,"_id":934,"_source":578,"_file":935,"_stem":936,"_extension":581},"/topics/design/button-component-design","按钮组件设计详解","学习按钮样式、交互状态、无障碍性和最佳实践",[588,589,590,591,16],"按钮设计","Button Component","交互状态","UI 组件","/images/topics/button-design.jpg","2025-12-08",18,{"type":21,"children":596,"toc":916},[597,601,606,611,617,628,634,643,649,658,662,668,679,685,694,700,709,714,723,728,739,744,753,757,769,803,814,857,862],{"type":24,"tag":25,"props":598,"children":599},{"id":585},[600],{"type":30,"value":585},{"type":24,"tag":32,"props":602,"children":603},{},[604],{"type":30,"value":605},"按钮是 UI 中最重要的交互元素。优秀的按钮设计能够指导用户行为。",{"type":24,"tag":25,"props":607,"children":609},{"id":608},"按钮类型",[610],{"type":30,"value":608},{"type":24,"tag":94,"props":612,"children":614},{"id":613},"primary-button主按钮",[615],{"type":30,"value":616},"Primary Button（主按钮）",{"type":24,"tag":100,"props":618,"children":623},{"className":619,"code":621,"language":622,"meta":7},[620],"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",[624],{"type":24,"tag":105,"props":625,"children":626},{"__ignoreMap":7},[627],{"type":30,"value":621},{"type":24,"tag":94,"props":629,"children":631},{"id":630},"secondary-button次按钮",[632],{"type":30,"value":633},"Secondary Button（次按钮）",{"type":24,"tag":100,"props":635,"children":638},{"className":636,"code":637,"language":622,"meta":7},[620],".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",[639],{"type":24,"tag":105,"props":640,"children":641},{"__ignoreMap":7},[642],{"type":30,"value":637},{"type":24,"tag":94,"props":644,"children":646},{"id":645},"danger-button危险按钮",[647],{"type":30,"value":648},"Danger Button（危险按钮）",{"type":24,"tag":100,"props":650,"children":653},{"className":651,"code":652,"language":622,"meta":7},[620],".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",[654],{"type":24,"tag":105,"props":655,"children":656},{"__ignoreMap":7},[657],{"type":30,"value":652},{"type":24,"tag":25,"props":659,"children":660},{"id":590},[661],{"type":30,"value":590},{"type":24,"tag":94,"props":663,"children":665},{"id":664},"loading-状态",[666],{"type":30,"value":667},"Loading 状态",{"type":24,"tag":100,"props":669,"children":674},{"className":670,"code":672,"language":673,"meta":7},[671],"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",[675],{"type":24,"tag":105,"props":676,"children":677},{"__ignoreMap":7},[678],{"type":30,"value":672},{"type":24,"tag":94,"props":680,"children":682},{"id":681},"disabled-状态",[683],{"type":30,"value":684},"Disabled 状态",{"type":24,"tag":100,"props":686,"children":689},{"className":687,"code":688,"language":622,"meta":7},[620],".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",[690],{"type":24,"tag":105,"props":691,"children":692},{"__ignoreMap":7},[693],{"type":30,"value":688},{"type":24,"tag":94,"props":695,"children":697},{"id":696},"focus-状态",[698],{"type":30,"value":699},"Focus 状态",{"type":24,"tag":100,"props":701,"children":704},{"className":702,"code":703,"language":622,"meta":7},[620],".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",[705],{"type":24,"tag":105,"props":706,"children":707},{"__ignoreMap":7},[708],{"type":30,"value":703},{"type":24,"tag":25,"props":710,"children":712},{"id":711},"按钮大小",[713],{"type":30,"value":711},{"type":24,"tag":100,"props":715,"children":718},{"className":716,"code":717,"language":622,"meta":7},[620],"/* 小按钮 */\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",[719],{"type":24,"tag":105,"props":720,"children":721},{"__ignoreMap":7},[722],{"type":30,"value":717},{"type":24,"tag":25,"props":724,"children":726},{"id":725},"无障碍性",[727],{"type":30,"value":725},{"type":24,"tag":100,"props":729,"children":734},{"className":730,"code":732,"language":733,"meta":7},[731],"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",[735],{"type":24,"tag":105,"props":736,"children":737},{"__ignoreMap":7},[738],{"type":30,"value":732},{"type":24,"tag":25,"props":740,"children":742},{"id":741},"完整组件示例",[743],{"type":30,"value":741},{"type":24,"tag":100,"props":745,"children":748},{"className":746,"code":747,"language":673,"meta":7},[671],"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",[749],{"type":24,"tag":105,"props":750,"children":751},{"__ignoreMap":7},[752],{"type":30,"value":747},{"type":24,"tag":25,"props":754,"children":755},{"id":309},[756],{"type":30,"value":309},{"type":24,"tag":32,"props":758,"children":759},{},[760,762,767],{"type":30,"value":761},"✅ ",{"type":24,"tag":46,"props":763,"children":764},{},[765],{"type":30,"value":766},"应该做的事",{"type":30,"value":768},":",{"type":24,"tag":38,"props":770,"children":771},{},[772,777,782,793,798],{"type":24,"tag":42,"props":773,"children":774},{},[775],{"type":30,"value":776},"最小触摸目标 44x44px",{"type":24,"tag":42,"props":778,"children":779},{},[780],{"type":30,"value":781},"清晰的视觉反馈",{"type":24,"tag":42,"props":783,"children":784},{},[785,787],{"type":30,"value":786},"使用语义 HTML ",{"type":24,"tag":105,"props":788,"children":790},{"className":789},[],[791],{"type":30,"value":792},"\u003Cbutton>",{"type":24,"tag":42,"props":794,"children":795},{},[796],{"type":30,"value":797},"提供加载状态反馈",{"type":24,"tag":42,"props":799,"children":800},{},[801],{"type":30,"value":802},"支持键盘导航",{"type":24,"tag":32,"props":804,"children":805},{},[806,808,813],{"type":30,"value":807},"❌ ",{"type":24,"tag":46,"props":809,"children":810},{},[811],{"type":30,"value":812},"不应该做的事",{"type":30,"value":768},{"type":24,"tag":38,"props":815,"children":816},{},[817,830,835,840,845],{"type":24,"tag":42,"props":818,"children":819},{},[820,822,828],{"type":30,"value":821},"使用 ",{"type":24,"tag":105,"props":823,"children":825},{"className":824},[],[826],{"type":30,"value":827},"\u003Cdiv>",{"type":30,"value":829}," 模拟按钮",{"type":24,"tag":42,"props":831,"children":832},{},[833],{"type":30,"value":834},"隐藏焦点指示器",{"type":24,"tag":42,"props":836,"children":837},{},[838],{"type":30,"value":839},"过多的按钮样式",{"type":24,"tag":42,"props":841,"children":842},{},[843],{"type":30,"value":844},"忽视禁用状态",{"type":24,"tag":42,"props":846,"children":847},{},[848,849,855],{"type":30,"value":821},{"type":24,"tag":105,"props":850,"children":852},{"className":851},[],[853],{"type":30,"value":854},"\u003Ca>",{"type":30,"value":856}," 代替按钮",{"type":24,"tag":25,"props":858,"children":860},{"id":859},"测试清单",[861],{"type":30,"value":859},{"type":24,"tag":38,"props":863,"children":866},{"className":864},[865],"contains-task-list",[867,880,889,898,907],{"type":24,"tag":42,"props":868,"children":871},{"className":869},[870],"task-list-item",[872,878],{"type":24,"tag":873,"props":874,"children":877},"input",{"disabled":875,"type":876},true,"checkbox",[],{"type":30,"value":879}," 在各种浏览器中测试",{"type":24,"tag":42,"props":881,"children":883},{"className":882},[870],[884,887],{"type":24,"tag":873,"props":885,"children":886},{"disabled":875,"type":876},[],{"type":30,"value":888}," 验证键盘导航",{"type":24,"tag":42,"props":890,"children":892},{"className":891},[870],[893,896],{"type":24,"tag":873,"props":894,"children":895},{"disabled":875,"type":876},[],{"type":30,"value":897}," 检查色彩对比度",{"type":24,"tag":42,"props":899,"children":901},{"className":900},[870],[902,905],{"type":24,"tag":873,"props":903,"children":904},{"disabled":875,"type":876},[],{"type":30,"value":906}," 测试触摸设备",{"type":24,"tag":42,"props":908,"children":910},{"className":909},[870],[911,914],{"type":24,"tag":873,"props":912,"children":913},{"disabled":875,"type":876},[],{"type":30,"value":915}," 屏幕阅读器兼容性",{"title":7,"searchDepth":544,"depth":544,"links":917},[918,919,924,929,930,931,932,933],{"id":585,"depth":547,"text":585},{"id":608,"depth":547,"text":608,"children":920},[921,922,923],{"id":613,"depth":544,"text":616},{"id":630,"depth":544,"text":633},{"id":645,"depth":544,"text":648},{"id":590,"depth":547,"text":590,"children":925},[926,927,928],{"id":664,"depth":544,"text":667},{"id":681,"depth":544,"text":684},{"id":696,"depth":544,"text":699},{"id":711,"depth":547,"text":711},{"id":725,"depth":547,"text":725},{"id":741,"depth":547,"text":741},{"id":309,"depth":547,"text":309},{"id":859,"depth":547,"text":859},"content:topics:design:button-component-design.md","topics/design/button-component-design.md","topics/design/button-component-design",{"_path":938,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":939,"description":940,"keywords":941,"image":946,"author":11,"date":593,"readingTime":947,"topic":5,"body":948,"_type":576,"_id":1236,"_source":578,"_file":1237,"_stem":1238,"_extension":581},"/topics/design/dark-mode-design","暗黑模式设计完整方案","学习暗黑模式实现、色彩方案、对比度管理和最佳实践",[942,943,944,945,16],"暗黑模式","Dark Mode","色彩系统","CSS 变量","/images/topics/dark-mode-design.jpg",20,{"type":21,"children":949,"toc":1219},[950,954,959,964,970,979,985,994,999,1005,1014,1020,1031,1037,1046,1051,1060,1065,1074,1079,1088,1092,1101,1129,1138,1166,1170],{"type":24,"tag":25,"props":951,"children":952},{"id":939},[953],{"type":30,"value":939},{"type":24,"tag":32,"props":955,"children":956},{},[957],{"type":30,"value":958},"暗黑模式已成为现代应用的标准功能。它能够减少眼睛疲劳、节省电池、改善用户体验。",{"type":24,"tag":25,"props":960,"children":962},{"id":961},"核心色彩系统",[963],{"type":30,"value":961},{"type":24,"tag":94,"props":965,"children":967},{"id":966},"light-mode-配色",[968],{"type":30,"value":969},"Light Mode 配色",{"type":24,"tag":100,"props":971,"children":974},{"className":972,"code":973,"language":622,"meta":7},[620],":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",[975],{"type":24,"tag":105,"props":976,"children":977},{"__ignoreMap":7},[978],{"type":30,"value":973},{"type":24,"tag":94,"props":980,"children":982},{"id":981},"dark-mode-配色",[983],{"type":30,"value":984},"Dark Mode 配色",{"type":24,"tag":100,"props":986,"children":989},{"className":987,"code":988,"language":622,"meta":7},[620],"@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",[990],{"type":24,"tag":105,"props":991,"children":992},{"__ignoreMap":7},[993],{"type":30,"value":988},{"type":24,"tag":25,"props":995,"children":997},{"id":996},"实现方案",[998],{"type":30,"value":996},{"type":24,"tag":94,"props":1000,"children":1002},{"id":1001},"方案-1prefers-color-scheme",[1003],{"type":30,"value":1004},"方案 1：prefers-color-scheme",{"type":24,"tag":100,"props":1006,"children":1009},{"className":1007,"code":1008,"language":622,"meta":7},[620],"/* 自动跟随系统设置 */\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",[1010],{"type":24,"tag":105,"props":1011,"children":1012},{"__ignoreMap":7},[1013],{"type":30,"value":1008},{"type":24,"tag":94,"props":1015,"children":1017},{"id":1016},"方案-2javascript-切换",[1018],{"type":30,"value":1019},"方案 2：JavaScript 切换",{"type":24,"tag":100,"props":1021,"children":1026},{"className":1022,"code":1024,"language":1025,"meta":7},[1023],"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",[1027],{"type":24,"tag":105,"props":1028,"children":1029},{"__ignoreMap":7},[1030],{"type":30,"value":1024},{"type":24,"tag":94,"props":1032,"children":1034},{"id":1033},"方案-3css-variables-javascript",[1035],{"type":30,"value":1036},"方案 3：CSS Variables + JavaScript",{"type":24,"tag":100,"props":1038,"children":1041},{"className":1039,"code":1040,"language":1025,"meta":7},[1023],"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",[1042],{"type":24,"tag":105,"props":1043,"children":1044},{"__ignoreMap":7},[1045],{"type":30,"value":1040},{"type":24,"tag":25,"props":1047,"children":1049},{"id":1048},"对比度管理",[1050],{"type":30,"value":1048},{"type":24,"tag":100,"props":1052,"children":1055},{"className":1053,"code":1054,"language":622,"meta":7},[620],"/* 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",[1056],{"type":24,"tag":105,"props":1057,"children":1058},{"__ignoreMap":7},[1059],{"type":30,"value":1054},{"type":24,"tag":25,"props":1061,"children":1063},{"id":1062},"图片和图表处理",[1064],{"type":30,"value":1062},{"type":24,"tag":100,"props":1066,"children":1069},{"className":1067,"code":1068,"language":733,"meta":7},[731],"\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",[1070],{"type":24,"tag":105,"props":1071,"children":1072},{"__ignoreMap":7},[1073],{"type":30,"value":1068},{"type":24,"tag":25,"props":1075,"children":1077},{"id":1076},"完整示例",[1078],{"type":30,"value":1076},{"type":24,"tag":100,"props":1080,"children":1083},{"className":1081,"code":1082,"language":673,"meta":7},[671],"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",[1084],{"type":24,"tag":105,"props":1085,"children":1086},{"__ignoreMap":7},[1087],{"type":30,"value":1082},{"type":24,"tag":25,"props":1089,"children":1090},{"id":309},[1091],{"type":30,"value":309},{"type":24,"tag":32,"props":1093,"children":1094},{},[1095,1096,1100],{"type":30,"value":761},{"type":24,"tag":46,"props":1097,"children":1098},{},[1099],{"type":30,"value":766},{"type":30,"value":768},{"type":24,"tag":38,"props":1102,"children":1103},{},[1104,1109,1114,1119,1124],{"type":24,"tag":42,"props":1105,"children":1106},{},[1107],{"type":30,"value":1108},"支持系统偏好",{"type":24,"tag":42,"props":1110,"children":1111},{},[1112],{"type":30,"value":1113},"提供手动切换选项",{"type":24,"tag":42,"props":1115,"children":1116},{},[1117],{"type":30,"value":1118},"确保足够的对比度",{"type":24,"tag":42,"props":1120,"children":1121},{},[1122],{"type":30,"value":1123},"优化图片和图表",{"type":24,"tag":42,"props":1125,"children":1126},{},[1127],{"type":30,"value":1128},"防止加载闪烁",{"type":24,"tag":32,"props":1130,"children":1131},{},[1132,1133,1137],{"type":30,"value":807},{"type":24,"tag":46,"props":1134,"children":1135},{},[1136],{"type":30,"value":812},{"type":30,"value":768},{"type":24,"tag":38,"props":1139,"children":1140},{},[1141,1146,1151,1156,1161],{"type":24,"tag":42,"props":1142,"children":1143},{},[1144],{"type":30,"value":1145},"强制单一模式",{"type":24,"tag":42,"props":1147,"children":1148},{},[1149],{"type":30,"value":1150},"忽视性能影响",{"type":24,"tag":42,"props":1152,"children":1153},{},[1154],{"type":30,"value":1155},"使用相同的颜色",{"type":24,"tag":42,"props":1157,"children":1158},{},[1159],{"type":30,"value":1160},"忘记保存用户偏好",{"type":24,"tag":42,"props":1162,"children":1163},{},[1164],{"type":30,"value":1165},"过度使用深色背景",{"type":24,"tag":25,"props":1167,"children":1168},{"id":859},[1169],{"type":30,"value":859},{"type":24,"tag":38,"props":1171,"children":1173},{"className":1172},[865],[1174,1183,1192,1201,1210],{"type":24,"tag":42,"props":1175,"children":1177},{"className":1176},[870],[1178,1181],{"type":24,"tag":873,"props":1179,"children":1180},{"disabled":875,"type":876},[],{"type":30,"value":1182}," 在浅色和深色模式下测试所有页面",{"type":24,"tag":42,"props":1184,"children":1186},{"className":1185},[870],[1187,1190],{"type":24,"tag":873,"props":1188,"children":1189},{"disabled":875,"type":876},[],{"type":30,"value":1191}," 检查颜色对比度符合 WCAG 标准",{"type":24,"tag":42,"props":1193,"children":1195},{"className":1194},[870],[1196,1199],{"type":24,"tag":873,"props":1197,"children":1198},{"disabled":875,"type":876},[],{"type":30,"value":1200}," 验证图片和图表在两种模式下清晰",{"type":24,"tag":42,"props":1202,"children":1204},{"className":1203},[870],[1205,1208],{"type":24,"tag":873,"props":1206,"children":1207},{"disabled":875,"type":876},[],{"type":30,"value":1209}," 测试主题切换的平滑性",{"type":24,"tag":42,"props":1211,"children":1213},{"className":1212},[870],[1214,1217],{"type":24,"tag":873,"props":1215,"children":1216},{"disabled":875,"type":876},[],{"type":30,"value":1218}," 检查用户偏好是否被保存",{"title":7,"searchDepth":544,"depth":544,"links":1220},[1221,1222,1226,1231,1232,1233,1234,1235],{"id":939,"depth":547,"text":939},{"id":961,"depth":547,"text":961,"children":1223},[1224,1225],{"id":966,"depth":544,"text":969},{"id":981,"depth":544,"text":984},{"id":996,"depth":547,"text":996,"children":1227},[1228,1229,1230],{"id":1001,"depth":544,"text":1004},{"id":1016,"depth":544,"text":1019},{"id":1033,"depth":544,"text":1036},{"id":1048,"depth":547,"text":1048},{"id":1062,"depth":547,"text":1062},{"id":1076,"depth":547,"text":1076},{"id":309,"depth":547,"text":309},{"id":859,"depth":547,"text":859},"content:topics:design:dark-mode-design.md","topics/design/dark-mode-design.md","topics/design/dark-mode-design",{"_path":1240,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1241,"description":1242,"keywords":1243,"image":1248,"author":1249,"date":593,"readingTime":947,"topic":5,"body":1250,"_type":576,"_id":1515,"_source":578,"_file":1516,"_stem":1517,"_extension":581},"/topics/design/form-controls-design","表单控件设计规范","学习输入框、选择框、复选框等表单控件的设计和实现",[1244,1245,1246,1247,16],"表单设计","Form Controls","输入框","验证反馈","/images/topics/form-controls-design.jpg","AI Content Team",{"type":21,"children":1251,"toc":1501},[1252,1256,1261,1266,1271,1280,1285,1294,1298,1307,1312,1321,1326,1335,1340,1349,1354,1363,1367,1376,1402,1411,1439,1443],{"type":24,"tag":25,"props":1253,"children":1254},{"id":1241},[1255],{"type":30,"value":1241},{"type":24,"tag":32,"props":1257,"children":1258},{},[1259],{"type":30,"value":1260},"优秀的表单设计能够提高用户完成率和满意度。",{"type":24,"tag":25,"props":1262,"children":1264},{"id":1263},"输入框设计",[1265],{"type":30,"value":1263},{"type":24,"tag":94,"props":1267,"children":1269},{"id":1268},"基础文本输入",[1270],{"type":30,"value":1268},{"type":24,"tag":100,"props":1272,"children":1275},{"className":1273,"code":1274,"language":622,"meta":7},[620],".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",[1276],{"type":24,"tag":105,"props":1277,"children":1278},{"__ignoreMap":7},[1279],{"type":30,"value":1274},{"type":24,"tag":94,"props":1281,"children":1283},{"id":1282},"标签和提示",[1284],{"type":30,"value":1282},{"type":24,"tag":100,"props":1286,"children":1289},{"className":1287,"code":1288,"language":733,"meta":7},[731],"\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",[1290],{"type":24,"tag":105,"props":1291,"children":1292},{"__ignoreMap":7},[1293],{"type":30,"value":1288},{"type":24,"tag":25,"props":1295,"children":1296},{"id":1247},[1297],{"type":30,"value":1247},{"type":24,"tag":100,"props":1299,"children":1302},{"className":1300,"code":1301,"language":673,"meta":7},[671],"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",[1303],{"type":24,"tag":105,"props":1304,"children":1305},{"__ignoreMap":7},[1306],{"type":30,"value":1301},{"type":24,"tag":25,"props":1308,"children":1310},{"id":1309},"选择框设计",[1311],{"type":30,"value":1309},{"type":24,"tag":100,"props":1313,"children":1316},{"className":1314,"code":1315,"language":622,"meta":7},[620],".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",[1317],{"type":24,"tag":105,"props":1318,"children":1319},{"__ignoreMap":7},[1320],{"type":30,"value":1315},{"type":24,"tag":25,"props":1322,"children":1324},{"id":1323},"复选框和单选按钮",[1325],{"type":30,"value":1323},{"type":24,"tag":100,"props":1327,"children":1330},{"className":1328,"code":1329,"language":622,"meta":7},[620],".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",[1331],{"type":24,"tag":105,"props":1332,"children":1333},{"__ignoreMap":7},[1334],{"type":30,"value":1329},{"type":24,"tag":25,"props":1336,"children":1338},{"id":1337},"文本区域",[1339],{"type":30,"value":1337},{"type":24,"tag":100,"props":1341,"children":1344},{"className":1342,"code":1343,"language":622,"meta":7},[620],".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",[1345],{"type":24,"tag":105,"props":1346,"children":1347},{"__ignoreMap":7},[1348],{"type":30,"value":1343},{"type":24,"tag":25,"props":1350,"children":1352},{"id":1351},"完整表单示例",[1353],{"type":30,"value":1351},{"type":24,"tag":100,"props":1355,"children":1358},{"className":1356,"code":1357,"language":673,"meta":7},[671],"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",[1359],{"type":24,"tag":105,"props":1360,"children":1361},{"__ignoreMap":7},[1362],{"type":30,"value":1357},{"type":24,"tag":25,"props":1364,"children":1365},{"id":309},[1366],{"type":30,"value":309},{"type":24,"tag":32,"props":1368,"children":1369},{},[1370,1371,1375],{"type":30,"value":761},{"type":24,"tag":46,"props":1372,"children":1373},{},[1374],{"type":30,"value":766},{"type":30,"value":768},{"type":24,"tag":38,"props":1377,"children":1378},{},[1379,1384,1389,1394,1398],{"type":24,"tag":42,"props":1380,"children":1381},{},[1382],{"type":30,"value":1383},"使用正确的输入类型",{"type":24,"tag":42,"props":1385,"children":1386},{},[1387],{"type":30,"value":1388},"提供实时验证反馈",{"type":24,"tag":42,"props":1390,"children":1391},{},[1392],{"type":30,"value":1393},"清晰的标签和提示",{"type":24,"tag":42,"props":1395,"children":1396},{},[1397],{"type":30,"value":776},{"type":24,"tag":42,"props":1399,"children":1400},{},[1401],{"type":30,"value":802},{"type":24,"tag":32,"props":1403,"children":1404},{},[1405,1406,1410],{"type":30,"value":807},{"type":24,"tag":46,"props":1407,"children":1408},{},[1409],{"type":30,"value":812},{"type":30,"value":768},{"type":24,"tag":38,"props":1412,"children":1413},{},[1414,1419,1424,1429,1434],{"type":24,"tag":42,"props":1415,"children":1416},{},[1417],{"type":30,"value":1418},"隐藏标签",{"type":24,"tag":42,"props":1420,"children":1421},{},[1422],{"type":30,"value":1423},"过度使用占位符",{"type":24,"tag":42,"props":1425,"children":1426},{},[1427],{"type":30,"value":1428},"验证后立即提交",{"type":24,"tag":42,"props":1430,"children":1431},{},[1432],{"type":30,"value":1433},"忽视无障碍性",{"type":24,"tag":42,"props":1435,"children":1436},{},[1437],{"type":30,"value":1438},"复杂的验证规则",{"type":24,"tag":25,"props":1440,"children":1441},{"id":859},[1442],{"type":30,"value":859},{"type":24,"tag":38,"props":1444,"children":1446},{"className":1445},[865],[1447,1456,1465,1474,1483,1492],{"type":24,"tag":42,"props":1448,"children":1450},{"className":1449},[870],[1451,1454],{"type":24,"tag":873,"props":1452,"children":1453},{"disabled":875,"type":876},[],{"type":30,"value":1455}," 所有控件都可用键盘导航",{"type":24,"tag":42,"props":1457,"children":1459},{"className":1458},[870],[1460,1463],{"type":24,"tag":873,"props":1461,"children":1462},{"disabled":875,"type":876},[],{"type":30,"value":1464}," 标签与输入框关联",{"type":24,"tag":42,"props":1466,"children":1468},{"className":1467},[870],[1469,1472],{"type":24,"tag":873,"props":1470,"children":1471},{"disabled":875,"type":876},[],{"type":30,"value":1473}," 验证消息清晰",{"type":24,"tag":42,"props":1475,"children":1477},{"className":1476},[870],[1478,1481],{"type":24,"tag":873,"props":1479,"children":1480},{"disabled":875,"type":876},[],{"type":30,"value":1482}," 色彩对比度足够",{"type":24,"tag":42,"props":1484,"children":1486},{"className":1485},[870],[1487,1490],{"type":24,"tag":873,"props":1488,"children":1489},{"disabled":875,"type":876},[],{"type":30,"value":1491}," 屏幕阅读器兼容",{"type":24,"tag":42,"props":1493,"children":1495},{"className":1494},[870],[1496,1499],{"type":24,"tag":873,"props":1497,"children":1498},{"disabled":875,"type":876},[],{"type":30,"value":1500}," 移动设备测试",{"title":7,"searchDepth":544,"depth":544,"links":1502},[1503,1504,1508,1509,1510,1511,1512,1513,1514],{"id":1241,"depth":547,"text":1241},{"id":1263,"depth":547,"text":1263,"children":1505},[1506,1507],{"id":1268,"depth":544,"text":1268},{"id":1282,"depth":544,"text":1282},{"id":1247,"depth":547,"text":1247},{"id":1309,"depth":547,"text":1309},{"id":1323,"depth":547,"text":1323},{"id":1337,"depth":547,"text":1337},{"id":1351,"depth":547,"text":1351},{"id":309,"depth":547,"text":309},{"id":859,"depth":547,"text":859},"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":1519,"image":18,"featured":6,"readingTime":19,"body":1520,"_type":576,"_id":577,"_source":578,"_file":579,"_stem":580,"_extension":581},[13,14,15,16,17],{"type":21,"children":1521,"toc":1943},[1522,1526,1530,1565,1569,1573,1577,1584,1588,1595,1599,1603,1611,1615,1623,1627,1631,1639,1643,1647,1655,1659,1667,1671,1675,1683,1687,1695,1699,1703,1711,1715,1723,1727,1731,1849,1853,1896,1900,1904,1939],{"type":24,"tag":25,"props":1523,"children":1524},{"id":27},[1525],{"type":30,"value":27},{"type":24,"tag":32,"props":1527,"children":1528},{},[1529],{"type":30,"value":36},{"type":24,"tag":38,"props":1531,"children":1532},{},[1533,1541,1549,1557],{"type":24,"tag":42,"props":1534,"children":1535},{},[1536,1540],{"type":24,"tag":46,"props":1537,"children":1538},{},[1539],{"type":30,"value":50},{"type":30,"value":52},{"type":24,"tag":42,"props":1542,"children":1543},{},[1544,1548],{"type":24,"tag":46,"props":1545,"children":1546},{},[1547],{"type":30,"value":60},{"type":30,"value":62},{"type":24,"tag":42,"props":1550,"children":1551},{},[1552,1556],{"type":24,"tag":46,"props":1553,"children":1554},{},[1555],{"type":30,"value":70},{"type":30,"value":72},{"type":24,"tag":42,"props":1558,"children":1559},{},[1560,1564],{"type":24,"tag":46,"props":1561,"children":1562},{},[1563],{"type":30,"value":80},{"type":30,"value":82},{"type":24,"tag":32,"props":1566,"children":1567},{},[1568],{"type":30,"value":87},{"type":24,"tag":25,"props":1570,"children":1571},{"id":90},[1572],{"type":30,"value":90},{"type":24,"tag":94,"props":1574,"children":1575},{"id":96},[1576],{"type":30,"value":96},{"type":24,"tag":100,"props":1578,"children":1579},{"code":102},[1580],{"type":24,"tag":105,"props":1581,"children":1582},{"__ignoreMap":7},[1583],{"type":30,"value":102},{"type":24,"tag":94,"props":1585,"children":1586},{"id":111},[1587],{"type":30,"value":111},{"type":24,"tag":100,"props":1589,"children":1590},{"code":116},[1591],{"type":24,"tag":105,"props":1592,"children":1593},{"__ignoreMap":7},[1594],{"type":30,"value":116},{"type":24,"tag":25,"props":1596,"children":1597},{"id":124},[1598],{"type":30,"value":124},{"type":24,"tag":94,"props":1600,"children":1601},{"id":129},[1602],{"type":30,"value":129},{"type":24,"tag":100,"props":1604,"children":1606},{"code":134,"language":135,"meta":7,"className":1605},[137],[1607],{"type":24,"tag":105,"props":1608,"children":1609},{"__ignoreMap":7},[1610],{"type":30,"value":134},{"type":24,"tag":94,"props":1612,"children":1613},{"id":145},[1614],{"type":30,"value":145},{"type":24,"tag":100,"props":1616,"children":1618},{"code":150,"language":135,"meta":7,"className":1617},[137],[1619],{"type":24,"tag":105,"props":1620,"children":1621},{"__ignoreMap":7},[1622],{"type":30,"value":150},{"type":24,"tag":25,"props":1624,"children":1625},{"id":159},[1626],{"type":30,"value":159},{"type":24,"tag":94,"props":1628,"children":1629},{"id":164},[1630],{"type":30,"value":164},{"type":24,"tag":100,"props":1632,"children":1634},{"code":169,"language":135,"meta":7,"className":1633},[137],[1635],{"type":24,"tag":105,"props":1636,"children":1637},{"__ignoreMap":7},[1638],{"type":30,"value":169},{"type":24,"tag":25,"props":1640,"children":1641},{"id":178},[1642],{"type":30,"value":178},{"type":24,"tag":94,"props":1644,"children":1645},{"id":183},[1646],{"type":30,"value":183},{"type":24,"tag":100,"props":1648,"children":1650},{"code":188,"language":135,"meta":7,"className":1649},[137],[1651],{"type":24,"tag":105,"props":1652,"children":1653},{"__ignoreMap":7},[1654],{"type":30,"value":188},{"type":24,"tag":94,"props":1656,"children":1657},{"id":197},[1658],{"type":30,"value":197},{"type":24,"tag":100,"props":1660,"children":1662},{"code":202,"language":203,"meta":7,"className":1661},[205],[1663],{"type":24,"tag":105,"props":1664,"children":1665},{"__ignoreMap":7},[1666],{"type":30,"value":202},{"type":24,"tag":25,"props":1668,"children":1669},{"id":213},[1670],{"type":30,"value":213},{"type":24,"tag":94,"props":1672,"children":1673},{"id":218},[1674],{"type":30,"value":218},{"type":24,"tag":100,"props":1676,"children":1678},{"code":223,"language":135,"meta":7,"className":1677},[137],[1679],{"type":24,"tag":105,"props":1680,"children":1681},{"__ignoreMap":7},[1682],{"type":30,"value":223},{"type":24,"tag":25,"props":1684,"children":1685},{"id":232},[1686],{"type":30,"value":232},{"type":24,"tag":100,"props":1688,"children":1690},{"code":237,"language":135,"meta":7,"className":1689},[137],[1691],{"type":24,"tag":105,"props":1692,"children":1693},{"__ignoreMap":7},[1694],{"type":30,"value":237},{"type":24,"tag":25,"props":1696,"children":1697},{"id":246},[1698],{"type":30,"value":246},{"type":24,"tag":94,"props":1700,"children":1701},{"id":251},[1702],{"type":30,"value":251},{"type":24,"tag":100,"props":1704,"children":1706},{"code":256,"language":203,"meta":7,"className":1705},[205],[1707],{"type":24,"tag":105,"props":1708,"children":1709},{"__ignoreMap":7},[1710],{"type":30,"value":256},{"type":24,"tag":94,"props":1712,"children":1713},{"id":265},[1714],{"type":30,"value":265},{"type":24,"tag":100,"props":1716,"children":1718},{"code":270,"language":135,"meta":7,"className":1717},[137],[1719],{"type":24,"tag":105,"props":1720,"children":1721},{"__ignoreMap":7},[1722],{"type":30,"value":270},{"type":24,"tag":25,"props":1724,"children":1725},{"id":279},[1726],{"type":30,"value":279},{"type":24,"tag":94,"props":1728,"children":1729},{"id":284},[1730],{"type":30,"value":284},{"type":24,"tag":288,"props":1732,"children":1733},{},[1734,1748],{"type":24,"tag":292,"props":1735,"children":1736},{},[1737],{"type":24,"tag":296,"props":1738,"children":1739},{},[1740,1744],{"type":24,"tag":300,"props":1741,"children":1742},{},[1743],{"type":30,"value":304},{"type":24,"tag":300,"props":1745,"children":1746},{},[1747],{"type":30,"value":309},{"type":24,"tag":311,"props":1749,"children":1750},{},[1751,1765,1779,1793,1807,1821,1835],{"type":24,"tag":296,"props":1752,"children":1753},{},[1754,1761],{"type":24,"tag":318,"props":1755,"children":1756},{},[1757],{"type":24,"tag":46,"props":1758,"children":1759},{},[1760],{"type":30,"value":325},{"type":24,"tag":318,"props":1762,"children":1763},{},[1764],{"type":30,"value":330},{"type":24,"tag":296,"props":1766,"children":1767},{},[1768,1775],{"type":24,"tag":318,"props":1769,"children":1770},{},[1771],{"type":24,"tag":46,"props":1772,"children":1773},{},[1774],{"type":30,"value":341},{"type":24,"tag":318,"props":1776,"children":1777},{},[1778],{"type":30,"value":346},{"type":24,"tag":296,"props":1780,"children":1781},{},[1782,1789],{"type":24,"tag":318,"props":1783,"children":1784},{},[1785],{"type":24,"tag":46,"props":1786,"children":1787},{},[1788],{"type":30,"value":357},{"type":24,"tag":318,"props":1790,"children":1791},{},[1792],{"type":30,"value":362},{"type":24,"tag":296,"props":1794,"children":1795},{},[1796,1803],{"type":24,"tag":318,"props":1797,"children":1798},{},[1799],{"type":24,"tag":46,"props":1800,"children":1801},{},[1802],{"type":30,"value":373},{"type":24,"tag":318,"props":1804,"children":1805},{},[1806],{"type":30,"value":378},{"type":24,"tag":296,"props":1808,"children":1809},{},[1810,1817],{"type":24,"tag":318,"props":1811,"children":1812},{},[1813],{"type":24,"tag":46,"props":1814,"children":1815},{},[1816],{"type":30,"value":389},{"type":24,"tag":318,"props":1818,"children":1819},{},[1820],{"type":30,"value":394},{"type":24,"tag":296,"props":1822,"children":1823},{},[1824,1831],{"type":24,"tag":318,"props":1825,"children":1826},{},[1827],{"type":24,"tag":46,"props":1828,"children":1829},{},[1830],{"type":30,"value":405},{"type":24,"tag":318,"props":1832,"children":1833},{},[1834],{"type":30,"value":410},{"type":24,"tag":296,"props":1836,"children":1837},{},[1838,1845],{"type":24,"tag":318,"props":1839,"children":1840},{},[1841],{"type":24,"tag":46,"props":1842,"children":1843},{},[1844],{"type":30,"value":421},{"type":24,"tag":318,"props":1846,"children":1847},{},[1848],{"type":30,"value":426},{"type":24,"tag":94,"props":1850,"children":1851},{"id":429},[1852],{"type":30,"value":429},{"type":24,"tag":433,"props":1854,"children":1855},{},[1856,1864,1872,1880,1888],{"type":24,"tag":42,"props":1857,"children":1858},{},[1859,1863],{"type":24,"tag":46,"props":1860,"children":1861},{},[1862],{"type":30,"value":443},{"type":30,"value":445},{"type":24,"tag":42,"props":1865,"children":1866},{},[1867,1871],{"type":24,"tag":46,"props":1868,"children":1869},{},[1870],{"type":30,"value":453},{"type":30,"value":455},{"type":24,"tag":42,"props":1873,"children":1874},{},[1875,1879],{"type":24,"tag":46,"props":1876,"children":1877},{},[1878],{"type":30,"value":463},{"type":30,"value":465},{"type":24,"tag":42,"props":1881,"children":1882},{},[1883,1887],{"type":24,"tag":46,"props":1884,"children":1885},{},[1886],{"type":30,"value":473},{"type":30,"value":475},{"type":24,"tag":42,"props":1889,"children":1890},{},[1891,1895],{"type":24,"tag":46,"props":1892,"children":1893},{},[1894],{"type":30,"value":405},{"type":30,"value":484},{"type":24,"tag":25,"props":1897,"children":1898},{"id":487},[1899],{"type":30,"value":487},{"type":24,"tag":32,"props":1901,"children":1902},{},[1903],{"type":30,"value":494},{"type":24,"tag":38,"props":1905,"children":1906},{},[1907,1915,1923,1931],{"type":24,"tag":42,"props":1908,"children":1909},{},[1910,1914],{"type":24,"tag":46,"props":1911,"children":1912},{},[1913],{"type":30,"value":505},{"type":30,"value":507},{"type":24,"tag":42,"props":1916,"children":1917},{},[1918,1922],{"type":24,"tag":46,"props":1919,"children":1920},{},[1921],{"type":30,"value":515},{"type":30,"value":517},{"type":24,"tag":42,"props":1924,"children":1925},{},[1926,1930],{"type":24,"tag":46,"props":1927,"children":1928},{},[1929],{"type":30,"value":525},{"type":30,"value":527},{"type":24,"tag":42,"props":1932,"children":1933},{},[1934,1938],{"type":24,"tag":46,"props":1935,"children":1936},{},[1937],{"type":30,"value":535},{"type":30,"value":537},{"type":24,"tag":32,"props":1940,"children":1941},{},[1942],{"type":30,"value":542},{"title":7,"searchDepth":544,"depth":544,"links":1944},[1945,1946,1950,1954,1957,1961,1964,1965,1969,1973],{"id":27,"depth":547,"text":27},{"id":90,"depth":547,"text":90,"children":1947},[1948,1949],{"id":96,"depth":544,"text":96},{"id":111,"depth":544,"text":111},{"id":124,"depth":547,"text":124,"children":1951},[1952,1953],{"id":129,"depth":544,"text":129},{"id":145,"depth":544,"text":145},{"id":159,"depth":547,"text":159,"children":1955},[1956],{"id":164,"depth":544,"text":164},{"id":178,"depth":547,"text":178,"children":1958},[1959,1960],{"id":183,"depth":544,"text":183},{"id":197,"depth":544,"text":197},{"id":213,"depth":547,"text":213,"children":1962},[1963],{"id":218,"depth":544,"text":218},{"id":232,"depth":547,"text":232},{"id":246,"depth":547,"text":246,"children":1966},[1967,1968],{"id":251,"depth":544,"text":251},{"id":265,"depth":544,"text":265},{"id":279,"depth":547,"text":279,"children":1970},[1971,1972],{"id":284,"depth":544,"text":284},{"id":429,"depth":544,"text":429},{"id":487,"depth":547,"text":487},1778637715255]