Files
ycode/scripts/gen-icons.mjs
T

197 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 从 icons.json 生成 src/renderer/src/components/icons.tsx
*
* 用法:node scripts/gen-icons.mjs <iconsJson> <outTsx>
*/
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(/<svg /, `<svg width="__SIZE__" height="__SIZE__" `)
if (size) out = out.replace(/__SIZE__/g, String(size))
return out
}
const data = JSON.parse(await readFile(src, 'utf8'))
const parts = [
'/**',
' * 图标组件。',
' *',
' * ⚠️ 来源说明:本文件中的 SVG 路径取自本机安装的 WorkBuddy 运行时界面(CDP 采集,',
' * 见 scripts/wb-icons.mjs + scripts/gen-icons.mjs),用于当前阶段的 1:1 视觉还原。',
' * 这些矢量路径属于第三方产品的界面资产,**仅作临时占位**;',
' * 正式发布前必须替换为自有图标或开源图标集(如 Lucide / VS Code codicons,后者为 MIT)。',
' *',
' * 重新生成:node scripts/gen-icons.mjs <icons.json> <输出路径>',
' */',
'',
"import type { SVGProps } from 'react'",
'',
'type IconProps = SVGProps<SVGSVGElement>',
''
]
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(/^<svg /, `<svg {...props} `)
parts.push(`export function ${name}(props: IconProps) {`)
parts.push(' return (')
parts.push(' ' + cleaned)
parts.push(' )')
parts.push('}')
parts.push('')
}
/* ---- 第二轮:导航与账号区图标 ---- */
let navData = null
if (navSource) {
try {
navData = JSON.parse(await readFile(navSource, 'utf8'))
} catch {
navData = null
}
}
for (const [name, key] of Object.entries(NAV_MAP)) {
const svg = navData?.[key]
if (!svg) {
parts.push(`// ${name}: 导航采集数据缺失(${key})`)
parts.push('')
continue
}
const cleaned = sanitize(svg, NAV_SIZES[name])
.replace(/\sfill="currentColor"/g, ' fill="currentColor"')
.replace(/^<svg /, `<svg {...props} `)
parts.push(`export function ${name}(props: IconProps) {`)
parts.push(' return (')
parts.push(' ' + cleaned)
parts.push(' )')
parts.push('}')
parts.push('')
}
/* ---- 第三轮:标题栏左上操作区(收起侧边栏 / 搜索 / 筛选)---- */
const TOPBAR_MAP = {
IconCollapseSidebar: '收起侧边栏',
IconSearchTopbar: '搜索'
}
let topbarData = null
if (topbarSource) {
try {
topbarData = JSON.parse(await readFile(topbarSource, 'utf8'))
} catch {
topbarData = null
}
}
for (const [name, title] of Object.entries(TOPBAR_MAP)) {
const entry = Array.isArray(topbarData) ? topbarData.find((n) => 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(/^<svg /, `<svg {...props} `)
parts.push(`export function ${name}(props: IconProps) {`)
parts.push(' return (')
parts.push(' ' + cleaned)
parts.push(' )')
parts.push('}')
parts.push('')
}
await writeFile(out, parts.join('\n'), 'utf8')
console.log('已生成', out)
console.log('图标数:', Object.keys(MAP).length)