refactor: 将 @iconify/vue 图标替换为 lucide-vue-next 本地组件
- 所有 lucide: 前缀的图标字符串替换为 lucide-vue-next 组件直接引用
- 动态图标绑定改为 <component :is> 渲染
- h(Icon, { icon: 'lucide:xxx' }) 改为 h(Xxx, { class: ... })
- app-card.vue 的 icon_url 改为 <img> 标签(实际是图片路径)
- 修复 Webhook 标识符冲突(重命名为 WebhookIcon)
- payment-channels 保留 @iconify/vue 用于品牌图标(支付宝、微信、Stripe、PayPal、USDT)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const SRC_DIR = 'D:\\Code\\verify\\frontend\\src'
|
||||
|
||||
function getAllFiles(dir, exts) {
|
||||
const results = []
|
||||
const entries = readdirSync(dir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...getAllFiles(fullPath, exts))
|
||||
} else if (exts.some(ext => entry.name.endsWith(ext))) {
|
||||
results.push(fullPath)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function lucideToPascal(kebab) {
|
||||
return kebab
|
||||
.split('-')
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('')
|
||||
}
|
||||
|
||||
function processFile(filePath) {
|
||||
let content = readFileSync(filePath, 'utf-8')
|
||||
const originalContent = content
|
||||
|
||||
if (!content.includes('@iconify/vue') && !content.includes('icon="lucide:') && !content.includes("icon='lucide:") && !content.includes('h(Icon')) {
|
||||
return null
|
||||
}
|
||||
|
||||
const usedIcons = new Set()
|
||||
|
||||
// Step 1: Replace <Icon icon="lucide:xxx" ... /> (single line)
|
||||
content = content.replace(/<Icon\s+icon="lucide:([a-z0-9-]+)"([^>]*)\/>/g, (match, iconName, restAttrs) => {
|
||||
const pascalName = lucideToPascal(iconName)
|
||||
usedIcons.add(pascalName)
|
||||
const trimmedAttrs = restAttrs.trim()
|
||||
if (trimmedAttrs) {
|
||||
return `<${pascalName} ${trimmedAttrs} />`
|
||||
}
|
||||
return `<${pascalName} />`
|
||||
})
|
||||
|
||||
// Step 2: Replace multiline <Icon\n icon="lucide:xxx"\n .../>
|
||||
content = content.replace(/<Icon\s+([^>]*?)icon="lucide:([a-z0-9-]+)"([^>]*?)\/>/gs, (match, before, iconName, after) => {
|
||||
const pascalName = lucideToPascal(iconName)
|
||||
usedIcons.add(pascalName)
|
||||
const newAttrs = (before + after).replace(/\s+/g, ' ').trim()
|
||||
if (newAttrs) {
|
||||
return `<${pascalName} ${newAttrs} />`
|
||||
}
|
||||
return `<${pascalName} />`
|
||||
})
|
||||
|
||||
// Step 3: Replace h(Icon, { icon: 'lucide:xxx', ... }) in columns.ts files
|
||||
content = content.replace(/h\(\s*Icon\s*,\s*\{\s*icon:\s*['"]lucide:([a-z0-9-]+)['"]\s*,?\s*([^}]*)\}\s*\)/g, (match, iconName, restProps) => {
|
||||
const pascalName = lucideToPascal(iconName)
|
||||
usedIcons.add(pascalName)
|
||||
const trimmedProps = restProps.trim().replace(/,\s*$/, '')
|
||||
if (trimmedProps) {
|
||||
return `h(${pascalName}, { ${trimmedProps} })`
|
||||
}
|
||||
return `h(${pascalName})`
|
||||
})
|
||||
|
||||
// Step 4: Replace import { Icon } from '@iconify/vue'
|
||||
content = content.replace(/import\s*\{\s*Icon\s*\}\s*from\s*['"]@iconify\/vue['"]\s*\n?/g, '')
|
||||
|
||||
// Step 5: Add lucide-vue-next import if we used any icons
|
||||
if (usedIcons.size > 0 && !content.includes("from 'lucide-vue-next'") && !content.includes('from "lucide-vue-next"')) {
|
||||
const sortedIcons = [...usedIcons].sort()
|
||||
const importLine = `import { ${sortedIcons.join(', ')} } from 'lucide-vue-next'\n`
|
||||
|
||||
const firstImportMatch = content.match(/^import\s/m)
|
||||
if (firstImportMatch) {
|
||||
const insertPos = content.indexOf(firstImportMatch[0])
|
||||
content = content.slice(0, insertPos) + importLine + content.slice(insertPos)
|
||||
} else {
|
||||
content = content.replace(/(<script[^>]*>)/, `$1\n${importLine}`)
|
||||
}
|
||||
} else if (usedIcons.size > 0) {
|
||||
const existingImportMatch = content.match(/import\s*\{([^}]*)\}\s*from\s*['"]lucide-vue-next['"]/s)
|
||||
if (existingImportMatch) {
|
||||
const existingIcons = existingImportMatch[1]
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const newIcons = [...usedIcons].filter(icon => !existingIcons.includes(icon))
|
||||
|
||||
if (newIcons.length > 0) {
|
||||
const allIcons = [...existingIcons, ...newIcons].sort()
|
||||
const newImport = `import { ${allIcons.join(', ')} } from 'lucide-vue-next'`
|
||||
content = content.replace(existingImportMatch[0], newImport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (content !== originalContent) {
|
||||
writeFileSync(filePath, content, 'utf-8')
|
||||
return { file: filePath, icons: [...usedIcons] }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const files = getAllFiles(SRC_DIR, ['.vue', '.ts'])
|
||||
const results = []
|
||||
|
||||
for (const file of files) {
|
||||
const result = processFile(file)
|
||||
if (result) {
|
||||
results.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nProcessed ${results.length} files:`)
|
||||
for (const r of results) {
|
||||
console.log(` ${r.file}: ${r.icons.join(', ')}`)
|
||||
}
|
||||
Reference in New Issue
Block a user