/** * 从 icons.json 生成 src/renderer/src/components/icons.tsx * * 用法:node scripts/gen-icons.mjs */ import { readFile, writeFile } from 'node:fs/promises' const src = process.argv[2] ?? 'C:/Users/Administrator/.ycode/captures/icons.json' const out = process.argv[3] ?? 'src/renderer/src/components/icons.tsx' /** 第二轮采集的导航/账号图标(可选) */ const navSource = process.argv[4] ?? 'C:/Users/Administrator/.ycode/captures/nav-icons.json' /** 第三轮:标题栏左上操作区图标(可选) */ const topbarSource = process.argv[5] ?? 'C:/Users/Administrator/.ycode/captures/topbar-icons.json' /** 图标名 → [target, svgIndex, 覆盖的渲染尺寸] */ const MAP = { IconNewTask: ['newTask', 0, 16], IconChevronDown: ['templates', 1, 14], IconFilter: ['topbarFilter', 0, 16], IconCompact: ['sectionCompact', 0, 16], IconSectionAdd: ['sectionAdd', 0, 16], IconPlus: ['inputToolbarLeft', 0, 16], IconShield: ['inputToolbarLeft', 1, 16], IconChevronSmall: ['inputToolbarRight', 2, 12], IconMic: ['inputToolbarRight', 3, 16], IconStop: ['sendButton', 0, 32], IconTab: ['tabIcon', 0, 13], IconClose: ['tabClose', 0, 14], IconChevronRight: ['artifactActions', 0, 8], IconPanelCollapse: ['panelActions', 0, 16], IconPanelRight: ['panelActions', 1, 16] } /** * 第二轮:从 nav-icons.json 采集的导航与账号区图标。 * 键为采集时的标签文案 / 选择器。 */ const NAV_MAP = { IconNavAssistant: '助理Beta', IconNavExperts: '专家·技能·连接器', IconNavScheduled: '定时任务', IconNavLibrary: '资料库', IconNavMore: '更多', IconBell: '.msg-center-bell-btn', IconMiniprogram: '.user-menu-trigger-miniprogram', IconDiscover: '.industry-template-switcher__host button' } const NAV_SIZES = { IconNavAssistant: 16, IconNavExperts: 16, IconNavScheduled: 16, IconNavLibrary: 16, IconNavMore: 16, IconBell: 16, IconMiniprogram: 16, IconDiscover: 14 } /** HTML 连字符属性 → JSX 驼峰属性 */ const JSX_ATTRS = { 'fill-rule': 'fillRule', 'clip-rule': 'clipRule', 'stroke-width': 'strokeWidth', 'stroke-linecap': 'strokeLinecap', 'stroke-linejoin': 'strokeLinejoin', 'stroke-dasharray': 'strokeDasharray', 'stroke-dashoffset': 'strokeDashoffset', 'stroke-miterlimit': 'strokeMiterlimit', 'clip-path': 'clipPath', 'fill-opacity': 'fillOpacity', 'stroke-opacity': 'strokeOpacity' } /** 只保留几何与绘制属性,去掉 class / aria 等宿主细节 */ function sanitize(svg, size) { let out = svg out = out.replace(/\sclass="[^"]*"/g, '') out = out.replace(/\sstyle="[^"]*"/g, '') out = out.replace(/\saria-hidden="[^"]*"/g, '') out = out.replace(/\sxmlns="[^"]*"/g, '') // 统一由外层控制尺寸 out = out.replace(/\swidth="[^"]*"/, '') out = out.replace(/\sheight="[^"]*"/, '') for (const [html, jsx] of Object.entries(JSX_ATTRS)) { out = out.replace(new RegExp(`\\s${html}=`, 'g'), ` ${jsx}=`) } out = out.replace(/ <输出路径>', ' */', '', "import type { SVGProps } from 'react'", '', 'type IconProps = SVGProps', '' ] for (const [name, [target, index, size]] of Object.entries(MAP)) { const entry = data[target] const svg = entry?.svgs?.[index] if (!svg) { parts.push(`// ${name}: 采集数据缺失(${target}[${index}])`) parts.push('') continue } const cleaned = sanitize(svg, size) .replace(/\sfill="currentColor"/g, ' fill="currentColor"') .replace(/^ n.title === title) : null if (!entry?.svg) { parts.push(`// ${name}: 标题栏采集数据缺失(${title})`) parts.push('') continue } const cleaned = sanitize(entry.svg, 16) .replace(/\sfill="currentColor"/g, ' fill="currentColor"') .replace(/^