197 lines
6.1 KiB
JavaScript
197 lines
6.1 KiB
JavaScript
/**
|
||
* 从 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)
|