[{"data":1,"prerenderedAt":1685},["ShallowReactive",2],{"article-/topics/design/design-to-code-automation-guide":3,"related-design":432,"content-query-gUuq7BoR5W":1368},{"_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":426,"_id":427,"_source":428,"_file":429,"_stem":430,"_extension":431},"/topics/design/design-to-code-automation-guide","design",false,"","设计到代码的自动化流程：从 Figma 到前端组件的现代工作流","探索设计系统自动化的最佳实践，涵盖设计令牌同步、组件代码生成、资产导出等核心环节，实现设计与开发的无缝衔接。","2024-12-28","HTMLPAGE 团队",[13,14,15,16,17],"设计自动化","Figma API","Design Tokens","代码生成","DevOps","/images/topics/design-to-code.jpg",12,{"type":21,"children":22,"toc":394},"root",[23,31,37,42,47,57,62,69,74,85,91,96,107,113,122,128,139,144,150,159,165,174,179,185,194,200,209,214,220,229,235,246,252,261,266,272,280,286,294,300,309,315,324,329,334,380,385],{"type":24,"tag":25,"props":26,"children":28},"element","h2",{"id":27},"设计到代码的自动化流程从-figma-到前端组件的现代工作流",[29],{"type":30,"value":8},"text",{"type":24,"tag":32,"props":33,"children":34},"p",{},[35],{"type":30,"value":36},"设计稿更新了，开发要手动同步颜色值；图标改了，需要重新导出 SVG；组件调整了，代码要跟着改。这些重复劳动不仅消耗时间，还容易出错。",{"type":24,"tag":32,"props":38,"children":39},{},[40],{"type":30,"value":41},"设计到代码的自动化，就是用工具和流程解决这些问题，让设计变更自动同步到代码。",{"type":24,"tag":25,"props":43,"children":45},{"id":44},"自动化的核心环节",[46],{"type":30,"value":44},{"type":24,"tag":48,"props":49,"children":51},"pre",{"code":50},"设计工具 (Figma)\n    │\n    ├─→ 设计令牌 (Tokens)\n    │       │\n    │       └─→ CSS 变量 / Tailwind 配置 / SCSS 变量\n    │\n    ├─→ 图标资产 (Icons)\n    │       │\n    │       └─→ SVG 组件 / Icon Font / Sprite\n    │\n    ├─→ 图片资产 (Images)\n    │       │\n    │       └─→ 优化后的图片 / 多尺寸图片\n    │\n    └─→ 组件规格 (Specs)\n            │\n            └─→ 组件文档 / 类型定义\n",[52],{"type":24,"tag":53,"props":54,"children":55},"code",{"__ignoreMap":7},[56],{"type":30,"value":50},{"type":24,"tag":25,"props":58,"children":60},{"id":59},"设计令牌自动化",[61],{"type":30,"value":59},{"type":24,"tag":63,"props":64,"children":66},"h3",{"id":65},"使用-tokens-studio-原-figma-tokens",[67],{"type":30,"value":68},"使用 Tokens Studio (原 Figma Tokens)",{"type":24,"tag":32,"props":70,"children":71},{},[72],{"type":30,"value":73},"Tokens Studio 是 Figma 最流行的令牌管理插件。",{"type":24,"tag":48,"props":75,"children":80},{"code":76,"language":77,"meta":7,"className":78},"// tokens.json - Tokens Studio 导出格式\n{\n  \"global\": {\n    \"colors\": {\n      \"blue\": {\n        \"50\": { \"value\": \"#eff6ff\", \"type\": \"color\" },\n        \"500\": { \"value\": \"#3b82f6\", \"type\": \"color\" },\n        \"900\": { \"value\": \"#1e3a8a\", \"type\": \"color\" }\n      },\n      \"gray\": {\n        \"50\": { \"value\": \"#f9fafb\", \"type\": \"color\" },\n        \"500\": { \"value\": \"#6b7280\", \"type\": \"color\" },\n        \"900\": { \"value\": \"#111827\", \"type\": \"color\" }\n      }\n    },\n    \"spacing\": {\n      \"xs\": { \"value\": \"4px\", \"type\": \"spacing\" },\n      \"sm\": { \"value\": \"8px\", \"type\": \"spacing\" },\n      \"md\": { \"value\": \"16px\", \"type\": \"spacing\" },\n      \"lg\": { \"value\": \"24px\", \"type\": \"spacing\" },\n      \"xl\": { \"value\": \"32px\", \"type\": \"spacing\" }\n    },\n    \"borderRadius\": {\n      \"sm\": { \"value\": \"4px\", \"type\": \"borderRadius\" },\n      \"md\": { \"value\": \"8px\", \"type\": \"borderRadius\" },\n      \"lg\": { \"value\": \"12px\", \"type\": \"borderRadius\" },\n      \"full\": { \"value\": \"9999px\", \"type\": \"borderRadius\" }\n    }\n  },\n  \"semantic\": {\n    \"colors\": {\n      \"primary\": { \"value\": \"{global.colors.blue.500}\", \"type\": \"color\" },\n      \"text-primary\": { \"value\": \"{global.colors.gray.900}\", \"type\": \"color\" },\n      \"text-secondary\": { \"value\": \"{global.colors.gray.500}\", \"type\": \"color\" },\n      \"background\": { \"value\": \"{global.colors.gray.50}\", \"type\": \"color\" }\n    }\n  }\n}\n","json",[79],"language-json",[81],{"type":24,"tag":53,"props":82,"children":83},{"__ignoreMap":7},[84],{"type":30,"value":76},{"type":24,"tag":63,"props":86,"children":88},{"id":87},"style-dictionary-转换",[89],{"type":30,"value":90},"Style Dictionary 转换",{"type":24,"tag":32,"props":92,"children":93},{},[94],{"type":30,"value":95},"Style Dictionary 是 Amazon 开源的令牌转换工具。",{"type":24,"tag":48,"props":97,"children":102},{"code":98,"language":99,"meta":7,"className":100},"// config.js - Style Dictionary 配置\nmodule.exports = {\n  source: ['tokens/**/*.json'],\n  platforms: {\n    // CSS 变量\n    css: {\n      transformGroup: 'css',\n      buildPath: 'build/css/',\n      files: [{\n        destination: 'variables.css',\n        format: 'css/variables',\n        options: {\n          outputReferences: true,\n        },\n      }],\n    },\n    \n    // SCSS 变量\n    scss: {\n      transformGroup: 'scss',\n      buildPath: 'build/scss/',\n      files: [{\n        destination: '_variables.scss',\n        format: 'scss/variables',\n      }],\n    },\n    \n    // JavaScript/TypeScript\n    js: {\n      transformGroup: 'js',\n      buildPath: 'build/js/',\n      files: [{\n        destination: 'tokens.js',\n        format: 'javascript/es6',\n      }, {\n        destination: 'tokens.d.ts',\n        format: 'typescript/es6-declarations',\n      }],\n    },\n    \n    // Tailwind CSS\n    tailwind: {\n      transformGroup: 'js',\n      buildPath: 'build/',\n      files: [{\n        destination: 'tailwind.tokens.js',\n        format: 'javascript/tailwind',\n      }],\n    },\n  },\n};\n","javascript",[101],"language-javascript",[103],{"type":24,"tag":53,"props":104,"children":105},{"__ignoreMap":7},[106],{"type":30,"value":98},{"type":24,"tag":63,"props":108,"children":110},{"id":109},"自定义-tailwind-格式",[111],{"type":30,"value":112},"自定义 Tailwind 格式",{"type":24,"tag":48,"props":114,"children":117},{"code":115,"language":99,"meta":7,"className":116},"// formats/tailwind.js\nconst StyleDictionary = require('style-dictionary');\n\nStyleDictionary.registerFormat({\n  name: 'javascript/tailwind',\n  formatter: function({ dictionary }) {\n    const tokens = {\n      colors: {},\n      spacing: {},\n      borderRadius: {},\n      fontSize: {},\n      fontFamily: {},\n      boxShadow: {},\n    };\n    \n    dictionary.allProperties.forEach(prop => {\n      const category = prop.attributes.category;\n      const name = prop.name.replace(`${category}-`, '');\n      \n      if (tokens[category]) {\n        setNestedValue(tokens[category], name, prop.value);\n      }\n    });\n    \n    return `module.exports = ${JSON.stringify(tokens, null, 2)}`;\n  },\n});\n\nfunction setNestedValue(obj, path, value) {\n  const keys = path.split('-');\n  let current = obj;\n  \n  keys.forEach((key, index) => {\n    if (index === keys.length - 1) {\n      current[key] = value;\n    } else {\n      current[key] = current[key] || {};\n      current = current[key];\n    }\n  });\n}\n",[101],[118],{"type":24,"tag":53,"props":119,"children":120},{"__ignoreMap":7},[121],{"type":30,"value":115},{"type":24,"tag":63,"props":123,"children":125},{"id":124},"cicd-集成",[126],{"type":30,"value":127},"CI/CD 集成",{"type":24,"tag":48,"props":129,"children":134},{"code":130,"language":131,"meta":7,"className":132},"# .github/workflows/sync-tokens.yml\nname: Sync Design Tokens\n\non:\n  repository_dispatch:\n    types: [figma-tokens-updated]\n  workflow_dispatch:\n\njobs:\n  sync:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      \n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n      \n      - name: Install dependencies\n        run: npm ci\n      \n      - name: Pull tokens from Figma\n        run: npx token-transformer tokens.json build/tokens.json\n        env:\n          FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}\n      \n      - name: Build tokens\n        run: npx style-dictionary build\n      \n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v5\n        with:\n          title: 'chore: sync design tokens'\n          commit-message: 'chore: sync design tokens from Figma'\n          branch: sync-tokens\n          body: |\n            自动同步来自 Figma 的设计令牌更新。\n            \n            请检查以下变更：\n            - CSS 变量\n            - Tailwind 配置\n            - TypeScript 类型\n","yaml",[133],"language-yaml",[135],{"type":24,"tag":53,"props":136,"children":137},{"__ignoreMap":7},[138],{"type":30,"value":130},{"type":24,"tag":25,"props":140,"children":142},{"id":141},"图标资产自动化",[143],{"type":30,"value":141},{"type":24,"tag":63,"props":145,"children":147},{"id":146},"figma-api-导出图标",[148],{"type":30,"value":149},"Figma API 导出图标",{"type":24,"tag":48,"props":151,"children":154},{"code":152,"language":99,"meta":7,"className":153},"// scripts/export-icons.js\nconst Figma = require('figma-api');\nconst fs = require('fs');\nconst path = require('path');\nconst { optimize } = require('svgo');\n\nconst figma = new Figma.Api({\n  personalAccessToken: process.env.FIGMA_TOKEN,\n});\n\nasync function exportIcons() {\n  const fileId = process.env.FIGMA_FILE_ID;\n  const iconsPageName = 'Icons';\n  \n  // 获取文件结构\n  const file = await figma.getFile(fileId);\n  \n  // 找到图标页面\n  const iconsPage = file.document.children.find(\n    page => page.name === iconsPageName\n  );\n  \n  if (!iconsPage) {\n    throw new Error(`Page \"${iconsPageName}\" not found`);\n  }\n  \n  // 收集所有图标组件\n  const icons = [];\n  collectIcons(iconsPage, icons);\n  \n  console.log(`Found ${icons.length} icons`);\n  \n  // 批量获取 SVG\n  const iconIds = icons.map(i => i.id);\n  const images = await figma.getImage(fileId, {\n    ids: iconIds.join(','),\n    format: 'svg',\n  });\n  \n  // 下载并优化 SVG\n  for (const icon of icons) {\n    const svgUrl = images.images[icon.id];\n    if (!svgUrl) continue;\n    \n    const response = await fetch(svgUrl);\n    let svg = await response.text();\n    \n    // SVGO 优化\n    const optimized = optimize(svg, {\n      plugins: [\n        'removeDoctype',\n        'removeComments',\n        'removeMetadata',\n        'removeTitle',\n        'removeDesc',\n        'removeUselessDefs',\n        'removeEditorsNSData',\n        'removeEmptyAttrs',\n        'removeEmptyContainers',\n        'removeUnusedNS',\n        {\n          name: 'removeAttrs',\n          params: { attrs: ['fill', 'stroke'] },\n        },\n        {\n          name: 'addAttributesToSVGElement',\n          params: {\n            attributes: [\n              { fill: 'currentColor' },\n              { width: '1em' },\n              { height: '1em' },\n            ],\n          },\n        },\n      ],\n    });\n    \n    const outputPath = path.join('src/icons', `${icon.name}.svg`);\n    fs.mkdirSync(path.dirname(outputPath), { recursive: true });\n    fs.writeFileSync(outputPath, optimized.data);\n    \n    console.log(`Exported: ${icon.name}`);\n  }\n}\n\nfunction collectIcons(node, icons, prefix = '') {\n  if (node.type === 'COMPONENT') {\n    icons.push({\n      id: node.id,\n      name: prefix ? `${prefix}/${node.name}` : node.name,\n    });\n  }\n  \n  if (node.children) {\n    const newPrefix = node.type === 'FRAME' ? node.name : prefix;\n    node.children.forEach(child => collectIcons(child, icons, newPrefix));\n  }\n}\n\nexportIcons().catch(console.error);\n",[101],[155],{"type":24,"tag":53,"props":156,"children":157},{"__ignoreMap":7},[158],{"type":30,"value":152},{"type":24,"tag":63,"props":160,"children":162},{"id":161},"生成-vue-图标组件",[163],{"type":30,"value":164},"生成 Vue 图标组件",{"type":24,"tag":48,"props":166,"children":169},{"code":167,"language":99,"meta":7,"className":168},"// scripts/generate-icon-components.js\nconst fs = require('fs');\nconst path = require('path');\n\nconst iconsDir = 'src/icons';\nconst outputDir = 'src/components/icons';\n\nfunction generateIconComponent(name, svg) {\n  const componentName = toPascalCase(name) + 'Icon';\n  \n  // 移除 SVG 外层标签，保留内容\n  const svgContent = svg\n    .replace(/\u003Csvg[^>]*>/, '')\n    .replace(/\u003C\\/svg>/, '')\n    .trim();\n  \n  return `\u003Ctemplate>\n  \u003Csvg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    :width=\"size\"\n    :height=\"size\"\n    v-bind=\"$attrs\"\n  >\n    ${svgContent}\n  \u003C/svg>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nwithDefaults(defineProps\u003C{\n  size?: string | number;\n}>(), {\n  size: '1em',\n});\n\u003C/script>\n`;\n}\n\nfunction generateIndex(icons) {\n  const exports = icons.map(name => {\n    const componentName = toPascalCase(name) + 'Icon';\n    return `export { default as ${componentName} } from './${componentName}.vue';`;\n  });\n  \n  return exports.join('\\n');\n}\n\nfunction toPascalCase(str) {\n  return str\n    .split(/[-_/]/)\n    .map(part => part.charAt(0).toUpperCase() + part.slice(1))\n    .join('');\n}\n\n// 主逻辑\nconst icons = fs.readdirSync(iconsDir)\n  .filter(file => file.endsWith('.svg'))\n  .map(file => file.replace('.svg', ''));\n\nfs.mkdirSync(outputDir, { recursive: true });\n\nicons.forEach(name => {\n  const svg = fs.readFileSync(path.join(iconsDir, `${name}.svg`), 'utf-8');\n  const component = generateIconComponent(name, svg);\n  const componentName = toPascalCase(name) + 'Icon';\n  \n  fs.writeFileSync(\n    path.join(outputDir, `${componentName}.vue`),\n    component\n  );\n});\n\n// 生成 index.ts\nfs.writeFileSync(\n  path.join(outputDir, 'index.ts'),\n  generateIndex(icons)\n);\n\nconsole.log(`Generated ${icons.length} icon components`);\n",[101],[170],{"type":24,"tag":53,"props":171,"children":172},{"__ignoreMap":7},[173],{"type":30,"value":167},{"type":24,"tag":25,"props":175,"children":177},{"id":176},"组件文档自动生成",[178],{"type":30,"value":176},{"type":24,"tag":63,"props":180,"children":182},{"id":181},"从-figma-提取组件规格",[183],{"type":30,"value":184},"从 Figma 提取组件规格",{"type":24,"tag":48,"props":186,"children":189},{"code":187,"language":99,"meta":7,"className":188},"// scripts/extract-component-specs.js\nasync function extractComponentSpecs(fileId, componentName) {\n  const file = await figma.getFile(fileId);\n  \n  // 找到组件\n  const component = findComponent(file.document, componentName);\n  if (!component) return null;\n  \n  // 提取属性\n  const specs = {\n    name: component.name,\n    description: component.description,\n    variants: [],\n    properties: [],\n  };\n  \n  // 如果是组件集，提取变体\n  if (component.type === 'COMPONENT_SET') {\n    component.children.forEach(variant => {\n      specs.variants.push({\n        name: variant.name,\n        properties: parseVariantName(variant.name),\n      });\n    });\n  }\n  \n  // 提取组件属性\n  if (component.componentPropertyDefinitions) {\n    Object.entries(component.componentPropertyDefinitions).forEach(\n      ([key, def]) => {\n        specs.properties.push({\n          name: key,\n          type: def.type,\n          defaultValue: def.defaultValue,\n          options: def.variantOptions,\n        });\n      }\n    );\n  }\n  \n  return specs;\n}\n\nfunction parseVariantName(name) {\n  // 解析 \"Size=Large, Variant=Primary\" 格式\n  const props = {};\n  name.split(', ').forEach(part => {\n    const [key, value] = part.split('=');\n    props[key] = value;\n  });\n  return props;\n}\n",[101],[190],{"type":24,"tag":53,"props":191,"children":192},{"__ignoreMap":7},[193],{"type":30,"value":187},{"type":24,"tag":63,"props":195,"children":197},{"id":196},"生成-typescript-类型",[198],{"type":30,"value":199},"生成 TypeScript 类型",{"type":24,"tag":48,"props":201,"children":204},{"code":202,"language":99,"meta":7,"className":203},"// scripts/generate-types.js\nfunction generateComponentTypes(specs) {\n  const { name, properties } = specs;\n  \n  const propTypes = properties.map(prop => {\n    let type;\n    \n    switch (prop.type) {\n      case 'VARIANT':\n        type = prop.options.map(o => `'${o}'`).join(' | ');\n        break;\n      case 'BOOLEAN':\n        type = 'boolean';\n        break;\n      case 'TEXT':\n        type = 'string';\n        break;\n      case 'INSTANCE_SWAP':\n        type = 'React.ReactNode';\n        break;\n      default:\n        type = 'unknown';\n    }\n    \n    return `  ${toCamelCase(prop.name)}?: ${type};`;\n  });\n  \n  return `export interface ${name}Props {\n${propTypes.join('\\n')}\n}\n`;\n}\n",[101],[205],{"type":24,"tag":53,"props":206,"children":207},{"__ignoreMap":7},[208],{"type":30,"value":202},{"type":24,"tag":25,"props":210,"children":212},{"id":211},"完整工作流示例",[213],{"type":30,"value":211},{"type":24,"tag":63,"props":215,"children":217},{"id":216},"packagejson-脚本",[218],{"type":30,"value":219},"package.json 脚本",{"type":24,"tag":48,"props":221,"children":224},{"code":222,"language":77,"meta":7,"className":223},"{\n  \"scripts\": {\n    \"tokens:pull\": \"node scripts/pull-tokens.js\",\n    \"tokens:build\": \"style-dictionary build\",\n    \"tokens:sync\": \"npm run tokens:pull && npm run tokens:build\",\n    \n    \"icons:export\": \"node scripts/export-icons.js\",\n    \"icons:generate\": \"node scripts/generate-icon-components.js\",\n    \"icons:sync\": \"npm run icons:export && npm run icons:generate\",\n    \n    \"design:sync\": \"npm run tokens:sync && npm run icons:sync\",\n    \n    \"prepare\": \"husky install\"\n  }\n}\n",[79],[225],{"type":24,"tag":53,"props":226,"children":227},{"__ignoreMap":7},[228],{"type":30,"value":222},{"type":24,"tag":63,"props":230,"children":232},{"id":231},"git-hooks-集成",[233],{"type":30,"value":234},"Git Hooks 集成",{"type":24,"tag":48,"props":236,"children":241},{"code":237,"language":238,"meta":7,"className":239},"# .husky/pre-commit\n#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\n# 检查设计令牌是否有变更\nif git diff --cached --name-only | grep -q \"tokens/\"; then\n  echo \"Design tokens changed, rebuilding...\"\n  npm run tokens:build\n  git add build/\nfi\n","bash",[240],"language-bash",[242],{"type":24,"tag":53,"props":243,"children":244},{"__ignoreMap":7},[245],{"type":30,"value":237},{"type":24,"tag":63,"props":247,"children":249},{"id":248},"webhook-触发",[250],{"type":30,"value":251},"Webhook 触发",{"type":24,"tag":48,"props":253,"children":256},{"code":254,"language":99,"meta":7,"className":255},"// 在 Tokens Studio 中配置 Webhook\n// POST https://api.github.com/repos/{owner}/{repo}/dispatches\n\n// GitHub Actions 监听 repository_dispatch 事件\n// 自动创建 PR 同步令牌\n",[101],[257],{"type":24,"tag":53,"props":258,"children":259},{"__ignoreMap":7},[260],{"type":30,"value":254},{"type":24,"tag":25,"props":262,"children":264},{"id":263},"最佳实践",[265],{"type":30,"value":263},{"type":24,"tag":63,"props":267,"children":269},{"id":268},"_1-单一数据源",[270],{"type":30,"value":271},"1. 单一数据源",{"type":24,"tag":48,"props":273,"children":275},{"code":274},"Figma (设计真相源)\n    │\n    └─→ 代码 (生成产物)\n    \n永远不要手动修改生成的代码！\n",[276],{"type":24,"tag":53,"props":277,"children":278},{"__ignoreMap":7},[279],{"type":30,"value":274},{"type":24,"tag":63,"props":281,"children":283},{"id":282},"_2-版本控制",[284],{"type":30,"value":285},"2. 版本控制",{"type":24,"tag":48,"props":287,"children":289},{"code":288},"tokens/\n├── v1.0.0/\n│   └── tokens.json\n├── v1.1.0/\n│   └── tokens.json\n└── latest -> v1.1.0\n",[290],{"type":24,"tag":53,"props":291,"children":292},{"__ignoreMap":7},[293],{"type":30,"value":288},{"type":24,"tag":63,"props":295,"children":297},{"id":296},"_3-变更日志",[298],{"type":30,"value":299},"3. 变更日志",{"type":24,"tag":48,"props":301,"children":304},{"code":302,"language":99,"meta":7,"className":303},"// scripts/generate-changelog.js\nfunction compareTokens(oldTokens, newTokens) {\n  const changes = {\n    added: [],\n    removed: [],\n    modified: [],\n  };\n  \n  // 比较逻辑...\n  \n  return changes;\n}\n",[101],[305],{"type":24,"tag":53,"props":306,"children":307},{"__ignoreMap":7},[308],{"type":30,"value":302},{"type":24,"tag":63,"props":310,"children":312},{"id":311},"_4-验证检查",[313],{"type":30,"value":314},"4. 验证检查",{"type":24,"tag":48,"props":316,"children":319},{"code":317,"language":99,"meta":7,"className":318},"// scripts/validate-tokens.js\nfunction validateTokens(tokens) {\n  const errors = [];\n  \n  // 检查颜色对比度\n  // 检查命名规范\n  // 检查引用完整性\n  \n  if (errors.length > 0) {\n    throw new Error(`Token validation failed:\\n${errors.join('\\n')}`);\n  }\n}\n",[101],[320],{"type":24,"tag":53,"props":321,"children":322},{"__ignoreMap":7},[323],{"type":30,"value":317},{"type":24,"tag":25,"props":325,"children":327},{"id":326},"结语",[328],{"type":30,"value":326},{"type":24,"tag":32,"props":330,"children":331},{},[332],{"type":30,"value":333},"设计到代码的自动化不是一蹴而就的，它需要设计师和开发者共同建立流程和规范。但一旦建立起来，收益是巨大的：",{"type":24,"tag":335,"props":336,"children":337},"ol",{},[338,350,360,370],{"type":24,"tag":339,"props":340,"children":341},"li",{},[342,348],{"type":24,"tag":343,"props":344,"children":345},"strong",{},[346],{"type":30,"value":347},"消除重复工作",{"type":30,"value":349},"：手动同步变成自动同步",{"type":24,"tag":339,"props":351,"children":352},{},[353,358],{"type":24,"tag":343,"props":354,"children":355},{},[356],{"type":30,"value":357},"减少错误",{"type":30,"value":359},"：机器比人更可靠",{"type":24,"tag":339,"props":361,"children":362},{},[363,368],{"type":24,"tag":343,"props":364,"children":365},{},[366],{"type":30,"value":367},"加速迭代",{"type":30,"value":369},"：设计变更快速落地",{"type":24,"tag":339,"props":371,"children":372},{},[373,378],{"type":24,"tag":343,"props":374,"children":375},{},[376],{"type":30,"value":377},"保持一致",{"type":30,"value":379},"：单一数据源确保统一",{"type":24,"tag":32,"props":381,"children":382},{},[383],{"type":30,"value":384},"记住：自动化的目标不是取代人，而是让人专注于更有价值的工作。",{"type":24,"tag":386,"props":387,"children":388},"blockquote",{},[389],{"type":24,"tag":32,"props":390,"children":391},{},[392],{"type":30,"value":393},"\"Automate the boring stuff, so you can focus on the interesting stuff.\"",{"title":7,"searchDepth":395,"depth":395,"links":396},3,[397,399,400,406,410,414,419,425],{"id":27,"depth":398,"text":8},2,{"id":44,"depth":398,"text":44},{"id":59,"depth":398,"text":59,"children":401},[402,403,404,405],{"id":65,"depth":395,"text":68},{"id":87,"depth":395,"text":90},{"id":109,"depth":395,"text":112},{"id":124,"depth":395,"text":127},{"id":141,"depth":398,"text":141,"children":407},[408,409],{"id":146,"depth":395,"text":149},{"id":161,"depth":395,"text":164},{"id":176,"depth":398,"text":176,"children":411},[412,413],{"id":181,"depth":395,"text":184},{"id":196,"depth":395,"text":199},{"id":211,"depth":398,"text":211,"children":415},[416,417,418],{"id":216,"depth":395,"text":219},{"id":231,"depth":395,"text":234},{"id":248,"depth":395,"text":251},{"id":263,"depth":398,"text":263,"children":420},[421,422,423,424],{"id":268,"depth":395,"text":271},{"id":282,"depth":395,"text":285},{"id":296,"depth":395,"text":299},{"id":311,"depth":395,"text":314},{"id":326,"depth":398,"text":326},"markdown","content:topics:design:design-to-code-automation-guide.md","content","topics/design/design-to-code-automation-guide.md","topics/design/design-to-code-automation-guide","md",[433,789,1089],{"_path":434,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":435,"description":436,"keywords":437,"image":443,"author":11,"date":444,"readingTime":445,"topic":5,"body":446,"_type":426,"_id":786,"_source":428,"_file":787,"_stem":788,"_extension":431},"/topics/design/button-component-design","按钮组件设计详解","学习按钮样式、交互状态、无障碍性和最佳实践",[438,439,440,441,442],"按钮设计","Button Component","交互状态","UI 组件","用户体验","/images/topics/button-design.jpg","2025-12-08",18,{"type":21,"children":447,"toc":768},[448,452,457,462,468,479,485,494,500,509,513,519,530,536,545,551,560,565,574,579,590,595,604,608,620,655,666,709,714],{"type":24,"tag":25,"props":449,"children":450},{"id":435},[451],{"type":30,"value":435},{"type":24,"tag":32,"props":453,"children":454},{},[455],{"type":30,"value":456},"按钮是 UI 中最重要的交互元素。优秀的按钮设计能够指导用户行为。",{"type":24,"tag":25,"props":458,"children":460},{"id":459},"按钮类型",[461],{"type":30,"value":459},{"type":24,"tag":63,"props":463,"children":465},{"id":464},"primary-button主按钮",[466],{"type":30,"value":467},"Primary Button（主按钮）",{"type":24,"tag":48,"props":469,"children":474},{"className":470,"code":472,"language":473,"meta":7},[471],"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",[475],{"type":24,"tag":53,"props":476,"children":477},{"__ignoreMap":7},[478],{"type":30,"value":472},{"type":24,"tag":63,"props":480,"children":482},{"id":481},"secondary-button次按钮",[483],{"type":30,"value":484},"Secondary Button（次按钮）",{"type":24,"tag":48,"props":486,"children":489},{"className":487,"code":488,"language":473,"meta":7},[471],".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",[490],{"type":24,"tag":53,"props":491,"children":492},{"__ignoreMap":7},[493],{"type":30,"value":488},{"type":24,"tag":63,"props":495,"children":497},{"id":496},"danger-button危险按钮",[498],{"type":30,"value":499},"Danger Button（危险按钮）",{"type":24,"tag":48,"props":501,"children":504},{"className":502,"code":503,"language":473,"meta":7},[471],".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",[505],{"type":24,"tag":53,"props":506,"children":507},{"__ignoreMap":7},[508],{"type":30,"value":503},{"type":24,"tag":25,"props":510,"children":511},{"id":440},[512],{"type":30,"value":440},{"type":24,"tag":63,"props":514,"children":516},{"id":515},"loading-状态",[517],{"type":30,"value":518},"Loading 状态",{"type":24,"tag":48,"props":520,"children":525},{"className":521,"code":523,"language":524,"meta":7},[522],"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",[526],{"type":24,"tag":53,"props":527,"children":528},{"__ignoreMap":7},[529],{"type":30,"value":523},{"type":24,"tag":63,"props":531,"children":533},{"id":532},"disabled-状态",[534],{"type":30,"value":535},"Disabled 状态",{"type":24,"tag":48,"props":537,"children":540},{"className":538,"code":539,"language":473,"meta":7},[471],".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",[541],{"type":24,"tag":53,"props":542,"children":543},{"__ignoreMap":7},[544],{"type":30,"value":539},{"type":24,"tag":63,"props":546,"children":548},{"id":547},"focus-状态",[549],{"type":30,"value":550},"Focus 状态",{"type":24,"tag":48,"props":552,"children":555},{"className":553,"code":554,"language":473,"meta":7},[471],".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",[556],{"type":24,"tag":53,"props":557,"children":558},{"__ignoreMap":7},[559],{"type":30,"value":554},{"type":24,"tag":25,"props":561,"children":563},{"id":562},"按钮大小",[564],{"type":30,"value":562},{"type":24,"tag":48,"props":566,"children":569},{"className":567,"code":568,"language":473,"meta":7},[471],"/* 小按钮 */\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",[570],{"type":24,"tag":53,"props":571,"children":572},{"__ignoreMap":7},[573],{"type":30,"value":568},{"type":24,"tag":25,"props":575,"children":577},{"id":576},"无障碍性",[578],{"type":30,"value":576},{"type":24,"tag":48,"props":580,"children":585},{"className":581,"code":583,"language":584,"meta":7},[582],"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",[586],{"type":24,"tag":53,"props":587,"children":588},{"__ignoreMap":7},[589],{"type":30,"value":583},{"type":24,"tag":25,"props":591,"children":593},{"id":592},"完整组件示例",[594],{"type":30,"value":592},{"type":24,"tag":48,"props":596,"children":599},{"className":597,"code":598,"language":524,"meta":7},[522],"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",[600],{"type":24,"tag":53,"props":601,"children":602},{"__ignoreMap":7},[603],{"type":30,"value":598},{"type":24,"tag":25,"props":605,"children":606},{"id":263},[607],{"type":30,"value":263},{"type":24,"tag":32,"props":609,"children":610},{},[611,613,618],{"type":30,"value":612},"✅ ",{"type":24,"tag":343,"props":614,"children":615},{},[616],{"type":30,"value":617},"应该做的事",{"type":30,"value":619},":",{"type":24,"tag":621,"props":622,"children":623},"ul",{},[624,629,634,645,650],{"type":24,"tag":339,"props":625,"children":626},{},[627],{"type":30,"value":628},"最小触摸目标 44x44px",{"type":24,"tag":339,"props":630,"children":631},{},[632],{"type":30,"value":633},"清晰的视觉反馈",{"type":24,"tag":339,"props":635,"children":636},{},[637,639],{"type":30,"value":638},"使用语义 HTML ",{"type":24,"tag":53,"props":640,"children":642},{"className":641},[],[643],{"type":30,"value":644},"\u003Cbutton>",{"type":24,"tag":339,"props":646,"children":647},{},[648],{"type":30,"value":649},"提供加载状态反馈",{"type":24,"tag":339,"props":651,"children":652},{},[653],{"type":30,"value":654},"支持键盘导航",{"type":24,"tag":32,"props":656,"children":657},{},[658,660,665],{"type":30,"value":659},"❌ ",{"type":24,"tag":343,"props":661,"children":662},{},[663],{"type":30,"value":664},"不应该做的事",{"type":30,"value":619},{"type":24,"tag":621,"props":667,"children":668},{},[669,682,687,692,697],{"type":24,"tag":339,"props":670,"children":671},{},[672,674,680],{"type":30,"value":673},"使用 ",{"type":24,"tag":53,"props":675,"children":677},{"className":676},[],[678],{"type":30,"value":679},"\u003Cdiv>",{"type":30,"value":681}," 模拟按钮",{"type":24,"tag":339,"props":683,"children":684},{},[685],{"type":30,"value":686},"隐藏焦点指示器",{"type":24,"tag":339,"props":688,"children":689},{},[690],{"type":30,"value":691},"过多的按钮样式",{"type":24,"tag":339,"props":693,"children":694},{},[695],{"type":30,"value":696},"忽视禁用状态",{"type":24,"tag":339,"props":698,"children":699},{},[700,701,707],{"type":30,"value":673},{"type":24,"tag":53,"props":702,"children":704},{"className":703},[],[705],{"type":30,"value":706},"\u003Ca>",{"type":30,"value":708}," 代替按钮",{"type":24,"tag":25,"props":710,"children":712},{"id":711},"测试清单",[713],{"type":30,"value":711},{"type":24,"tag":621,"props":715,"children":718},{"className":716},[717],"contains-task-list",[719,732,741,750,759],{"type":24,"tag":339,"props":720,"children":723},{"className":721},[722],"task-list-item",[724,730],{"type":24,"tag":725,"props":726,"children":729},"input",{"disabled":727,"type":728},true,"checkbox",[],{"type":30,"value":731}," 在各种浏览器中测试",{"type":24,"tag":339,"props":733,"children":735},{"className":734},[722],[736,739],{"type":24,"tag":725,"props":737,"children":738},{"disabled":727,"type":728},[],{"type":30,"value":740}," 验证键盘导航",{"type":24,"tag":339,"props":742,"children":744},{"className":743},[722],[745,748],{"type":24,"tag":725,"props":746,"children":747},{"disabled":727,"type":728},[],{"type":30,"value":749}," 检查色彩对比度",{"type":24,"tag":339,"props":751,"children":753},{"className":752},[722],[754,757],{"type":24,"tag":725,"props":755,"children":756},{"disabled":727,"type":728},[],{"type":30,"value":758}," 测试触摸设备",{"type":24,"tag":339,"props":760,"children":762},{"className":761},[722],[763,766],{"type":24,"tag":725,"props":764,"children":765},{"disabled":727,"type":728},[],{"type":30,"value":767}," 屏幕阅读器兼容性",{"title":7,"searchDepth":395,"depth":395,"links":769},[770,771,776,781,782,783,784,785],{"id":435,"depth":398,"text":435},{"id":459,"depth":398,"text":459,"children":772},[773,774,775],{"id":464,"depth":395,"text":467},{"id":481,"depth":395,"text":484},{"id":496,"depth":395,"text":499},{"id":440,"depth":398,"text":440,"children":777},[778,779,780],{"id":515,"depth":395,"text":518},{"id":532,"depth":395,"text":535},{"id":547,"depth":395,"text":550},{"id":562,"depth":398,"text":562},{"id":576,"depth":398,"text":576},{"id":592,"depth":398,"text":592},{"id":263,"depth":398,"text":263},{"id":711,"depth":398,"text":711},"content:topics:design:button-component-design.md","topics/design/button-component-design.md","topics/design/button-component-design",{"_path":790,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":791,"description":792,"keywords":793,"image":798,"author":11,"date":444,"readingTime":799,"topic":5,"body":800,"_type":426,"_id":1086,"_source":428,"_file":1087,"_stem":1088,"_extension":431},"/topics/design/dark-mode-design","暗黑模式设计完整方案","学习暗黑模式实现、色彩方案、对比度管理和最佳实践",[794,795,796,797,442],"暗黑模式","Dark Mode","色彩系统","CSS 变量","/images/topics/dark-mode-design.jpg",20,{"type":21,"children":801,"toc":1069},[802,806,811,816,822,831,837,846,851,857,866,872,881,887,896,901,910,915,924,929,938,942,951,979,988,1016,1020],{"type":24,"tag":25,"props":803,"children":804},{"id":791},[805],{"type":30,"value":791},{"type":24,"tag":32,"props":807,"children":808},{},[809],{"type":30,"value":810},"暗黑模式已成为现代应用的标准功能。它能够减少眼睛疲劳、节省电池、改善用户体验。",{"type":24,"tag":25,"props":812,"children":814},{"id":813},"核心色彩系统",[815],{"type":30,"value":813},{"type":24,"tag":63,"props":817,"children":819},{"id":818},"light-mode-配色",[820],{"type":30,"value":821},"Light Mode 配色",{"type":24,"tag":48,"props":823,"children":826},{"className":824,"code":825,"language":473,"meta":7},[471],":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",[827],{"type":24,"tag":53,"props":828,"children":829},{"__ignoreMap":7},[830],{"type":30,"value":825},{"type":24,"tag":63,"props":832,"children":834},{"id":833},"dark-mode-配色",[835],{"type":30,"value":836},"Dark Mode 配色",{"type":24,"tag":48,"props":838,"children":841},{"className":839,"code":840,"language":473,"meta":7},[471],"@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",[842],{"type":24,"tag":53,"props":843,"children":844},{"__ignoreMap":7},[845],{"type":30,"value":840},{"type":24,"tag":25,"props":847,"children":849},{"id":848},"实现方案",[850],{"type":30,"value":848},{"type":24,"tag":63,"props":852,"children":854},{"id":853},"方案-1prefers-color-scheme",[855],{"type":30,"value":856},"方案 1：prefers-color-scheme",{"type":24,"tag":48,"props":858,"children":861},{"className":859,"code":860,"language":473,"meta":7},[471],"/* 自动跟随系统设置 */\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",[862],{"type":24,"tag":53,"props":863,"children":864},{"__ignoreMap":7},[865],{"type":30,"value":860},{"type":24,"tag":63,"props":867,"children":869},{"id":868},"方案-2javascript-切换",[870],{"type":30,"value":871},"方案 2：JavaScript 切换",{"type":24,"tag":48,"props":873,"children":876},{"className":874,"code":875,"language":99,"meta":7},[101],"// 检测和切换暗黑模式\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",[877],{"type":24,"tag":53,"props":878,"children":879},{"__ignoreMap":7},[880],{"type":30,"value":875},{"type":24,"tag":63,"props":882,"children":884},{"id":883},"方案-3css-variables-javascript",[885],{"type":30,"value":886},"方案 3：CSS Variables + JavaScript",{"type":24,"tag":48,"props":888,"children":891},{"className":889,"code":890,"language":99,"meta":7},[101],"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",[892],{"type":24,"tag":53,"props":893,"children":894},{"__ignoreMap":7},[895],{"type":30,"value":890},{"type":24,"tag":25,"props":897,"children":899},{"id":898},"对比度管理",[900],{"type":30,"value":898},{"type":24,"tag":48,"props":902,"children":905},{"className":903,"code":904,"language":473,"meta":7},[471],"/* 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",[906],{"type":24,"tag":53,"props":907,"children":908},{"__ignoreMap":7},[909],{"type":30,"value":904},{"type":24,"tag":25,"props":911,"children":913},{"id":912},"图片和图表处理",[914],{"type":30,"value":912},{"type":24,"tag":48,"props":916,"children":919},{"className":917,"code":918,"language":584,"meta":7},[582],"\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",[920],{"type":24,"tag":53,"props":921,"children":922},{"__ignoreMap":7},[923],{"type":30,"value":918},{"type":24,"tag":25,"props":925,"children":927},{"id":926},"完整示例",[928],{"type":30,"value":926},{"type":24,"tag":48,"props":930,"children":933},{"className":931,"code":932,"language":524,"meta":7},[522],"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",[934],{"type":24,"tag":53,"props":935,"children":936},{"__ignoreMap":7},[937],{"type":30,"value":932},{"type":24,"tag":25,"props":939,"children":940},{"id":263},[941],{"type":30,"value":263},{"type":24,"tag":32,"props":943,"children":944},{},[945,946,950],{"type":30,"value":612},{"type":24,"tag":343,"props":947,"children":948},{},[949],{"type":30,"value":617},{"type":30,"value":619},{"type":24,"tag":621,"props":952,"children":953},{},[954,959,964,969,974],{"type":24,"tag":339,"props":955,"children":956},{},[957],{"type":30,"value":958},"支持系统偏好",{"type":24,"tag":339,"props":960,"children":961},{},[962],{"type":30,"value":963},"提供手动切换选项",{"type":24,"tag":339,"props":965,"children":966},{},[967],{"type":30,"value":968},"确保足够的对比度",{"type":24,"tag":339,"props":970,"children":971},{},[972],{"type":30,"value":973},"优化图片和图表",{"type":24,"tag":339,"props":975,"children":976},{},[977],{"type":30,"value":978},"防止加载闪烁",{"type":24,"tag":32,"props":980,"children":981},{},[982,983,987],{"type":30,"value":659},{"type":24,"tag":343,"props":984,"children":985},{},[986],{"type":30,"value":664},{"type":30,"value":619},{"type":24,"tag":621,"props":989,"children":990},{},[991,996,1001,1006,1011],{"type":24,"tag":339,"props":992,"children":993},{},[994],{"type":30,"value":995},"强制单一模式",{"type":24,"tag":339,"props":997,"children":998},{},[999],{"type":30,"value":1000},"忽视性能影响",{"type":24,"tag":339,"props":1002,"children":1003},{},[1004],{"type":30,"value":1005},"使用相同的颜色",{"type":24,"tag":339,"props":1007,"children":1008},{},[1009],{"type":30,"value":1010},"忘记保存用户偏好",{"type":24,"tag":339,"props":1012,"children":1013},{},[1014],{"type":30,"value":1015},"过度使用深色背景",{"type":24,"tag":25,"props":1017,"children":1018},{"id":711},[1019],{"type":30,"value":711},{"type":24,"tag":621,"props":1021,"children":1023},{"className":1022},[717],[1024,1033,1042,1051,1060],{"type":24,"tag":339,"props":1025,"children":1027},{"className":1026},[722],[1028,1031],{"type":24,"tag":725,"props":1029,"children":1030},{"disabled":727,"type":728},[],{"type":30,"value":1032}," 在浅色和深色模式下测试所有页面",{"type":24,"tag":339,"props":1034,"children":1036},{"className":1035},[722],[1037,1040],{"type":24,"tag":725,"props":1038,"children":1039},{"disabled":727,"type":728},[],{"type":30,"value":1041}," 检查颜色对比度符合 WCAG 标准",{"type":24,"tag":339,"props":1043,"children":1045},{"className":1044},[722],[1046,1049],{"type":24,"tag":725,"props":1047,"children":1048},{"disabled":727,"type":728},[],{"type":30,"value":1050}," 验证图片和图表在两种模式下清晰",{"type":24,"tag":339,"props":1052,"children":1054},{"className":1053},[722],[1055,1058],{"type":24,"tag":725,"props":1056,"children":1057},{"disabled":727,"type":728},[],{"type":30,"value":1059}," 测试主题切换的平滑性",{"type":24,"tag":339,"props":1061,"children":1063},{"className":1062},[722],[1064,1067],{"type":24,"tag":725,"props":1065,"children":1066},{"disabled":727,"type":728},[],{"type":30,"value":1068}," 检查用户偏好是否被保存",{"title":7,"searchDepth":395,"depth":395,"links":1070},[1071,1072,1076,1081,1082,1083,1084,1085],{"id":791,"depth":398,"text":791},{"id":813,"depth":398,"text":813,"children":1073},[1074,1075],{"id":818,"depth":395,"text":821},{"id":833,"depth":395,"text":836},{"id":848,"depth":398,"text":848,"children":1077},[1078,1079,1080],{"id":853,"depth":395,"text":856},{"id":868,"depth":395,"text":871},{"id":883,"depth":395,"text":886},{"id":898,"depth":398,"text":898},{"id":912,"depth":398,"text":912},{"id":926,"depth":398,"text":926},{"id":263,"depth":398,"text":263},{"id":711,"depth":398,"text":711},"content:topics:design:dark-mode-design.md","topics/design/dark-mode-design.md","topics/design/dark-mode-design",{"_path":1090,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1091,"description":1092,"keywords":1093,"image":1098,"author":1099,"date":444,"readingTime":799,"topic":5,"body":1100,"_type":426,"_id":1365,"_source":428,"_file":1366,"_stem":1367,"_extension":431},"/topics/design/form-controls-design","表单控件设计规范","学习输入框、选择框、复选框等表单控件的设计和实现",[1094,1095,1096,1097,442],"表单设计","Form Controls","输入框","验证反馈","/images/topics/form-controls-design.jpg","AI Content Team",{"type":21,"children":1101,"toc":1351},[1102,1106,1111,1116,1121,1130,1135,1144,1148,1157,1162,1171,1176,1185,1190,1199,1204,1213,1217,1226,1252,1261,1289,1293],{"type":24,"tag":25,"props":1103,"children":1104},{"id":1091},[1105],{"type":30,"value":1091},{"type":24,"tag":32,"props":1107,"children":1108},{},[1109],{"type":30,"value":1110},"优秀的表单设计能够提高用户完成率和满意度。",{"type":24,"tag":25,"props":1112,"children":1114},{"id":1113},"输入框设计",[1115],{"type":30,"value":1113},{"type":24,"tag":63,"props":1117,"children":1119},{"id":1118},"基础文本输入",[1120],{"type":30,"value":1118},{"type":24,"tag":48,"props":1122,"children":1125},{"className":1123,"code":1124,"language":473,"meta":7},[471],".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",[1126],{"type":24,"tag":53,"props":1127,"children":1128},{"__ignoreMap":7},[1129],{"type":30,"value":1124},{"type":24,"tag":63,"props":1131,"children":1133},{"id":1132},"标签和提示",[1134],{"type":30,"value":1132},{"type":24,"tag":48,"props":1136,"children":1139},{"className":1137,"code":1138,"language":584,"meta":7},[582],"\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",[1140],{"type":24,"tag":53,"props":1141,"children":1142},{"__ignoreMap":7},[1143],{"type":30,"value":1138},{"type":24,"tag":25,"props":1145,"children":1146},{"id":1097},[1147],{"type":30,"value":1097},{"type":24,"tag":48,"props":1149,"children":1152},{"className":1150,"code":1151,"language":524,"meta":7},[522],"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",[1153],{"type":24,"tag":53,"props":1154,"children":1155},{"__ignoreMap":7},[1156],{"type":30,"value":1151},{"type":24,"tag":25,"props":1158,"children":1160},{"id":1159},"选择框设计",[1161],{"type":30,"value":1159},{"type":24,"tag":48,"props":1163,"children":1166},{"className":1164,"code":1165,"language":473,"meta":7},[471],".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",[1167],{"type":24,"tag":53,"props":1168,"children":1169},{"__ignoreMap":7},[1170],{"type":30,"value":1165},{"type":24,"tag":25,"props":1172,"children":1174},{"id":1173},"复选框和单选按钮",[1175],{"type":30,"value":1173},{"type":24,"tag":48,"props":1177,"children":1180},{"className":1178,"code":1179,"language":473,"meta":7},[471],".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",[1181],{"type":24,"tag":53,"props":1182,"children":1183},{"__ignoreMap":7},[1184],{"type":30,"value":1179},{"type":24,"tag":25,"props":1186,"children":1188},{"id":1187},"文本区域",[1189],{"type":30,"value":1187},{"type":24,"tag":48,"props":1191,"children":1194},{"className":1192,"code":1193,"language":473,"meta":7},[471],".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",[1195],{"type":24,"tag":53,"props":1196,"children":1197},{"__ignoreMap":7},[1198],{"type":30,"value":1193},{"type":24,"tag":25,"props":1200,"children":1202},{"id":1201},"完整表单示例",[1203],{"type":30,"value":1201},{"type":24,"tag":48,"props":1205,"children":1208},{"className":1206,"code":1207,"language":524,"meta":7},[522],"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",[1209],{"type":24,"tag":53,"props":1210,"children":1211},{"__ignoreMap":7},[1212],{"type":30,"value":1207},{"type":24,"tag":25,"props":1214,"children":1215},{"id":263},[1216],{"type":30,"value":263},{"type":24,"tag":32,"props":1218,"children":1219},{},[1220,1221,1225],{"type":30,"value":612},{"type":24,"tag":343,"props":1222,"children":1223},{},[1224],{"type":30,"value":617},{"type":30,"value":619},{"type":24,"tag":621,"props":1227,"children":1228},{},[1229,1234,1239,1244,1248],{"type":24,"tag":339,"props":1230,"children":1231},{},[1232],{"type":30,"value":1233},"使用正确的输入类型",{"type":24,"tag":339,"props":1235,"children":1236},{},[1237],{"type":30,"value":1238},"提供实时验证反馈",{"type":24,"tag":339,"props":1240,"children":1241},{},[1242],{"type":30,"value":1243},"清晰的标签和提示",{"type":24,"tag":339,"props":1245,"children":1246},{},[1247],{"type":30,"value":628},{"type":24,"tag":339,"props":1249,"children":1250},{},[1251],{"type":30,"value":654},{"type":24,"tag":32,"props":1253,"children":1254},{},[1255,1256,1260],{"type":30,"value":659},{"type":24,"tag":343,"props":1257,"children":1258},{},[1259],{"type":30,"value":664},{"type":30,"value":619},{"type":24,"tag":621,"props":1262,"children":1263},{},[1264,1269,1274,1279,1284],{"type":24,"tag":339,"props":1265,"children":1266},{},[1267],{"type":30,"value":1268},"隐藏标签",{"type":24,"tag":339,"props":1270,"children":1271},{},[1272],{"type":30,"value":1273},"过度使用占位符",{"type":24,"tag":339,"props":1275,"children":1276},{},[1277],{"type":30,"value":1278},"验证后立即提交",{"type":24,"tag":339,"props":1280,"children":1281},{},[1282],{"type":30,"value":1283},"忽视无障碍性",{"type":24,"tag":339,"props":1285,"children":1286},{},[1287],{"type":30,"value":1288},"复杂的验证规则",{"type":24,"tag":25,"props":1290,"children":1291},{"id":711},[1292],{"type":30,"value":711},{"type":24,"tag":621,"props":1294,"children":1296},{"className":1295},[717],[1297,1306,1315,1324,1333,1342],{"type":24,"tag":339,"props":1298,"children":1300},{"className":1299},[722],[1301,1304],{"type":24,"tag":725,"props":1302,"children":1303},{"disabled":727,"type":728},[],{"type":30,"value":1305}," 所有控件都可用键盘导航",{"type":24,"tag":339,"props":1307,"children":1309},{"className":1308},[722],[1310,1313],{"type":24,"tag":725,"props":1311,"children":1312},{"disabled":727,"type":728},[],{"type":30,"value":1314}," 标签与输入框关联",{"type":24,"tag":339,"props":1316,"children":1318},{"className":1317},[722],[1319,1322],{"type":24,"tag":725,"props":1320,"children":1321},{"disabled":727,"type":728},[],{"type":30,"value":1323}," 验证消息清晰",{"type":24,"tag":339,"props":1325,"children":1327},{"className":1326},[722],[1328,1331],{"type":24,"tag":725,"props":1329,"children":1330},{"disabled":727,"type":728},[],{"type":30,"value":1332}," 色彩对比度足够",{"type":24,"tag":339,"props":1334,"children":1336},{"className":1335},[722],[1337,1340],{"type":24,"tag":725,"props":1338,"children":1339},{"disabled":727,"type":728},[],{"type":30,"value":1341}," 屏幕阅读器兼容",{"type":24,"tag":339,"props":1343,"children":1345},{"className":1344},[722],[1346,1349],{"type":24,"tag":725,"props":1347,"children":1348},{"disabled":727,"type":728},[],{"type":30,"value":1350}," 移动设备测试",{"title":7,"searchDepth":395,"depth":395,"links":1352},[1353,1354,1358,1359,1360,1361,1362,1363,1364],{"id":1091,"depth":398,"text":1091},{"id":1113,"depth":398,"text":1113,"children":1355},[1356,1357],{"id":1118,"depth":395,"text":1118},{"id":1132,"depth":395,"text":1132},{"id":1097,"depth":398,"text":1097},{"id":1159,"depth":398,"text":1159},{"id":1173,"depth":398,"text":1173},{"id":1187,"depth":398,"text":1187},{"id":1201,"depth":398,"text":1201},{"id":263,"depth":398,"text":263},{"id":711,"depth":398,"text":711},"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":1369,"image":18,"featured":6,"readingTime":19,"body":1370,"_type":426,"_id":427,"_source":428,"_file":429,"_stem":430,"_extension":431},[13,14,15,16,17],{"type":21,"children":1371,"toc":1655},[1372,1376,1380,1384,1388,1395,1399,1403,1407,1415,1419,1423,1431,1435,1443,1447,1455,1459,1463,1471,1475,1483,1487,1491,1499,1503,1511,1515,1519,1527,1531,1539,1543,1551,1555,1559,1566,1570,1577,1581,1589,1593,1601,1605,1609,1644,1648],{"type":24,"tag":25,"props":1373,"children":1374},{"id":27},[1375],{"type":30,"value":8},{"type":24,"tag":32,"props":1377,"children":1378},{},[1379],{"type":30,"value":36},{"type":24,"tag":32,"props":1381,"children":1382},{},[1383],{"type":30,"value":41},{"type":24,"tag":25,"props":1385,"children":1386},{"id":44},[1387],{"type":30,"value":44},{"type":24,"tag":48,"props":1389,"children":1390},{"code":50},[1391],{"type":24,"tag":53,"props":1392,"children":1393},{"__ignoreMap":7},[1394],{"type":30,"value":50},{"type":24,"tag":25,"props":1396,"children":1397},{"id":59},[1398],{"type":30,"value":59},{"type":24,"tag":63,"props":1400,"children":1401},{"id":65},[1402],{"type":30,"value":68},{"type":24,"tag":32,"props":1404,"children":1405},{},[1406],{"type":30,"value":73},{"type":24,"tag":48,"props":1408,"children":1410},{"code":76,"language":77,"meta":7,"className":1409},[79],[1411],{"type":24,"tag":53,"props":1412,"children":1413},{"__ignoreMap":7},[1414],{"type":30,"value":76},{"type":24,"tag":63,"props":1416,"children":1417},{"id":87},[1418],{"type":30,"value":90},{"type":24,"tag":32,"props":1420,"children":1421},{},[1422],{"type":30,"value":95},{"type":24,"tag":48,"props":1424,"children":1426},{"code":98,"language":99,"meta":7,"className":1425},[101],[1427],{"type":24,"tag":53,"props":1428,"children":1429},{"__ignoreMap":7},[1430],{"type":30,"value":98},{"type":24,"tag":63,"props":1432,"children":1433},{"id":109},[1434],{"type":30,"value":112},{"type":24,"tag":48,"props":1436,"children":1438},{"code":115,"language":99,"meta":7,"className":1437},[101],[1439],{"type":24,"tag":53,"props":1440,"children":1441},{"__ignoreMap":7},[1442],{"type":30,"value":115},{"type":24,"tag":63,"props":1444,"children":1445},{"id":124},[1446],{"type":30,"value":127},{"type":24,"tag":48,"props":1448,"children":1450},{"code":130,"language":131,"meta":7,"className":1449},[133],[1451],{"type":24,"tag":53,"props":1452,"children":1453},{"__ignoreMap":7},[1454],{"type":30,"value":130},{"type":24,"tag":25,"props":1456,"children":1457},{"id":141},[1458],{"type":30,"value":141},{"type":24,"tag":63,"props":1460,"children":1461},{"id":146},[1462],{"type":30,"value":149},{"type":24,"tag":48,"props":1464,"children":1466},{"code":152,"language":99,"meta":7,"className":1465},[101],[1467],{"type":24,"tag":53,"props":1468,"children":1469},{"__ignoreMap":7},[1470],{"type":30,"value":152},{"type":24,"tag":63,"props":1472,"children":1473},{"id":161},[1474],{"type":30,"value":164},{"type":24,"tag":48,"props":1476,"children":1478},{"code":167,"language":99,"meta":7,"className":1477},[101],[1479],{"type":24,"tag":53,"props":1480,"children":1481},{"__ignoreMap":7},[1482],{"type":30,"value":167},{"type":24,"tag":25,"props":1484,"children":1485},{"id":176},[1486],{"type":30,"value":176},{"type":24,"tag":63,"props":1488,"children":1489},{"id":181},[1490],{"type":30,"value":184},{"type":24,"tag":48,"props":1492,"children":1494},{"code":187,"language":99,"meta":7,"className":1493},[101],[1495],{"type":24,"tag":53,"props":1496,"children":1497},{"__ignoreMap":7},[1498],{"type":30,"value":187},{"type":24,"tag":63,"props":1500,"children":1501},{"id":196},[1502],{"type":30,"value":199},{"type":24,"tag":48,"props":1504,"children":1506},{"code":202,"language":99,"meta":7,"className":1505},[101],[1507],{"type":24,"tag":53,"props":1508,"children":1509},{"__ignoreMap":7},[1510],{"type":30,"value":202},{"type":24,"tag":25,"props":1512,"children":1513},{"id":211},[1514],{"type":30,"value":211},{"type":24,"tag":63,"props":1516,"children":1517},{"id":216},[1518],{"type":30,"value":219},{"type":24,"tag":48,"props":1520,"children":1522},{"code":222,"language":77,"meta":7,"className":1521},[79],[1523],{"type":24,"tag":53,"props":1524,"children":1525},{"__ignoreMap":7},[1526],{"type":30,"value":222},{"type":24,"tag":63,"props":1528,"children":1529},{"id":231},[1530],{"type":30,"value":234},{"type":24,"tag":48,"props":1532,"children":1534},{"code":237,"language":238,"meta":7,"className":1533},[240],[1535],{"type":24,"tag":53,"props":1536,"children":1537},{"__ignoreMap":7},[1538],{"type":30,"value":237},{"type":24,"tag":63,"props":1540,"children":1541},{"id":248},[1542],{"type":30,"value":251},{"type":24,"tag":48,"props":1544,"children":1546},{"code":254,"language":99,"meta":7,"className":1545},[101],[1547],{"type":24,"tag":53,"props":1548,"children":1549},{"__ignoreMap":7},[1550],{"type":30,"value":254},{"type":24,"tag":25,"props":1552,"children":1553},{"id":263},[1554],{"type":30,"value":263},{"type":24,"tag":63,"props":1556,"children":1557},{"id":268},[1558],{"type":30,"value":271},{"type":24,"tag":48,"props":1560,"children":1561},{"code":274},[1562],{"type":24,"tag":53,"props":1563,"children":1564},{"__ignoreMap":7},[1565],{"type":30,"value":274},{"type":24,"tag":63,"props":1567,"children":1568},{"id":282},[1569],{"type":30,"value":285},{"type":24,"tag":48,"props":1571,"children":1572},{"code":288},[1573],{"type":24,"tag":53,"props":1574,"children":1575},{"__ignoreMap":7},[1576],{"type":30,"value":288},{"type":24,"tag":63,"props":1578,"children":1579},{"id":296},[1580],{"type":30,"value":299},{"type":24,"tag":48,"props":1582,"children":1584},{"code":302,"language":99,"meta":7,"className":1583},[101],[1585],{"type":24,"tag":53,"props":1586,"children":1587},{"__ignoreMap":7},[1588],{"type":30,"value":302},{"type":24,"tag":63,"props":1590,"children":1591},{"id":311},[1592],{"type":30,"value":314},{"type":24,"tag":48,"props":1594,"children":1596},{"code":317,"language":99,"meta":7,"className":1595},[101],[1597],{"type":24,"tag":53,"props":1598,"children":1599},{"__ignoreMap":7},[1600],{"type":30,"value":317},{"type":24,"tag":25,"props":1602,"children":1603},{"id":326},[1604],{"type":30,"value":326},{"type":24,"tag":32,"props":1606,"children":1607},{},[1608],{"type":30,"value":333},{"type":24,"tag":335,"props":1610,"children":1611},{},[1612,1620,1628,1636],{"type":24,"tag":339,"props":1613,"children":1614},{},[1615,1619],{"type":24,"tag":343,"props":1616,"children":1617},{},[1618],{"type":30,"value":347},{"type":30,"value":349},{"type":24,"tag":339,"props":1621,"children":1622},{},[1623,1627],{"type":24,"tag":343,"props":1624,"children":1625},{},[1626],{"type":30,"value":357},{"type":30,"value":359},{"type":24,"tag":339,"props":1629,"children":1630},{},[1631,1635],{"type":24,"tag":343,"props":1632,"children":1633},{},[1634],{"type":30,"value":367},{"type":30,"value":369},{"type":24,"tag":339,"props":1637,"children":1638},{},[1639,1643],{"type":24,"tag":343,"props":1640,"children":1641},{},[1642],{"type":30,"value":377},{"type":30,"value":379},{"type":24,"tag":32,"props":1645,"children":1646},{},[1647],{"type":30,"value":384},{"type":24,"tag":386,"props":1649,"children":1650},{},[1651],{"type":24,"tag":32,"props":1652,"children":1653},{},[1654],{"type":30,"value":393},{"title":7,"searchDepth":395,"depth":395,"links":1656},[1657,1658,1659,1665,1669,1673,1678,1684],{"id":27,"depth":398,"text":8},{"id":44,"depth":398,"text":44},{"id":59,"depth":398,"text":59,"children":1660},[1661,1662,1663,1664],{"id":65,"depth":395,"text":68},{"id":87,"depth":395,"text":90},{"id":109,"depth":395,"text":112},{"id":124,"depth":395,"text":127},{"id":141,"depth":398,"text":141,"children":1666},[1667,1668],{"id":146,"depth":395,"text":149},{"id":161,"depth":395,"text":164},{"id":176,"depth":398,"text":176,"children":1670},[1671,1672],{"id":181,"depth":395,"text":184},{"id":196,"depth":395,"text":199},{"id":211,"depth":398,"text":211,"children":1674},[1675,1676,1677],{"id":216,"depth":395,"text":219},{"id":231,"depth":395,"text":234},{"id":248,"depth":395,"text":251},{"id":263,"depth":398,"text":263,"children":1679},[1680,1681,1682,1683],{"id":268,"depth":395,"text":271},{"id":282,"depth":395,"text":285},{"id":296,"depth":395,"text":299},{"id":311,"depth":395,"text":314},{"id":326,"depth":398,"text":326},1778574590939]