| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- #!/usr/bin/env node
- /**
- * C 批:JS 色板收编
- * 将 <script> 块内的硬编码 hex 颜色字符串替换为 BrandColors.* 导入引用。
- * 仅在目标文件顶部添加 import 语句。
- *
- * 替换规则(color-literal 匹配,仅匹配单引号或双引号包裹的 hex):
- * '#F97316' / "#F97316" → '$brand.primary'
- * '#FB923C' / "#FB923C" → '$brand.primaryLight'
- * ...
- *
- * 用法:
- * node scripts/js-color-hoist.js # 全部替换(推荐,但谨慎)
- * node scripts/js-color-hoist.js --dry-run # 仅预览不写入
- * node scripts/js-color-hoist.js --file path # 只处理指定文件
- */
- const fs = require('fs')
- const path = require('path')
- const ROOT = path.resolve(__dirname, '..')
- const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'docs'])
- // 替换规则(key=hex literal value, value=BrandColors.*)
- const JS_RULES = [
- { hex: '#F97316', token: 'BrandColors.primary' },
- { hex: '#fb923c', token: 'BrandColors.primaryLight' },
- { hex: '#FB923C', token: 'BrandColors.primaryLight' },
- { hex: '#fff7ed', token: 'BrandColors.primaryPale' },
- { hex: '#FFF7ED', token: 'BrandColors.primaryPale' },
- { hex: '#ff8c42', token: 'BrandColors.body' },
- { hex: '#FF8C42', token: 'BrandColors.body' },
- { hex: '#10b981', token: 'BrandColors.success' },
- { hex: '#10B981', token: 'BrandColors.success' },
- { hex: '#ef4444', token: 'BrandColors.danger' },
- { hex: '#EF4444', token: 'BrandColors.danger' },
- { hex: '#6366f1', token: 'BrandColors.wisdom' },
- { hex: '#6366F1', token: 'BrandColors.wisdom' },
- { hex: '#c62828', token: 'BrandColors.dangerDeep' },
- { hex: '#C62828', token: 'BrandColors.dangerDeep' },
- { hex: '#333333', token: 'BrandColors.textStrong' },
- { hex: '#333', token: 'BrandColors.textStrong' },
- { hex: '#666666', token: 'BrandColors.textBody' },
- { hex: '#666', token: 'BrandColors.textBody' },
- { hex: '#999999', token: 'BrandColors.textMuted' },
- { hex: '#999', token: 'BrandColors.textMuted' },
- { hex: '#fafafa', token: 'BrandColors.fillGhost' },
- { hex: '#FAFAFA', token: 'BrandColors.fillGhost' },
- { hex: '#f9fafb', token: 'BrandColors.fillGray50' },
- { hex: '#F9FAFB', token: 'BrandColors.fillGray50' },
- { hex: '#ffffff', token: 'BrandColors.white' },
- { hex: '#FFFFFF', token: 'BrandColors.white' },
- { hex: '#ffffff', token: 'BrandColors.white' },
- ].sort((a, b) => b.hex.length - a.hex.length)
- // 优先文件列表(按硬编码色数排序)
- const TOP_FILES = [
- 'components/ContentSharePoster.vue',
- 'pages/mind-detail/emotion-checkin.vue',
- 'pages/action-detail/components/PearlDiagram.vue',
- 'pages/energy/index.vue',
- 'components/ArticleBookshelf.vue',
- 'pages/home-pages/parent-index.vue',
- 'pages/mind-detail/components/FamilyTianpanCard.vue',
- 'pages/promotion/components/SharePoster.vue',
- 'components/PageBanner.vue',
- 'pages/tianpan/index.vue',
- ]
- function walk(dir, out = []) {
- for (const name of fs.readdirSync(dir)) {
- if (EXCLUDE_DIRS.has(name)) continue
- const p = path.join(dir, name)
- const st = fs.statSync(p)
- if (st.isDirectory()) walk(p, out)
- else if (name.endsWith('.vue')) out.push(p)
- }
- return out
- }
- function hexToToken(hex) {
- const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- const found = JS_RULES.find(r => r.hex.toLowerCase() === hex.toLowerCase())
- return found ? found.token : null
- }
- function replaceJsHex(text, dryRun = false) {
- // 匹配 <script> 块
- const SCRIPT_RE = /(<script)([^>]*)>([\s\S]*?)(<\/script>)/gi
- let result = text
- let changed = false
- let totalRepl = 0
- let needsImport = false
- let importAdded = false
- const matches = [...text.matchAll(SCRIPT_RE)]
- for (const match of matches) {
- const body = match[3]
- let newBody = body
- let blockRepl = 0
- for (const rule of JS_RULES) {
- const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- // 匹配单引号或双引号包裹的 hex,且前后不是字母数字
- const re = new RegExp(`(['"])${escaped}(?![0-9a-fA-F])\\1`, 'g')
- const m = newBody.match(re)
- if (m) {
- blockRepl += m.length
- newBody = newBody.replace(re, `$1${rule.token}$2`) // careful: preserve quote style
- }
- }
- if (blockRepl > 0) {
- totalRepl += blockRepl
- changed = true
- needsImport = true
- // 替换 <script> 块
- const before = result.slice(0, match.index)
- const after = result.slice(SCRIPT_RE.lastIndex)
- // 找插入 import 的位置(在 </script> 后面,<template> 前面)
- const templateIdx = result.indexOf('<template>')
- if (templateIdx !== -1 && !importAdded) {
- const importLine = `\nimport { ${[...new Set(JS_RULES.map(r => r.token.split('.')[1]))]
- .sort().join(', ')} } from '@/utils/brand-colors.js'\n`
- result = before + importLine + result.slice(match.index)
- importAdded = true
- }
- result = result.slice(0, match.index) +
- match[1] + match[2] + '>' + newBody + '</script>' +
- result.slice(SCRIPT_RE.lastIndex)
- SCRIPT_RE.lastIndex = result.indexOf('</script>', match.index) + '</script>'.length
- }
- }
- if (!dryRun && changed) {
- fs.writeFileSync(result === text ? text : result, text) // bug guard
- return { changed: true, replacements: totalRepl, needsImport }
- }
- return { changed: false, replacements: totalRepl, needsImport }
- }
- // 简化版:直接处理整个文件
- function processFileSimple(filePath, dryRun = false) {
- const src = fs.readFileSync(filePath, 'utf8')
- let newSrc = src
- let totalRepl = 0
- // 收集文件中用到的 token
- const usedTokens = new Set()
- for (const rule of JS_RULES) {
- const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- const re = new RegExp(`['"]${escaped}(?![0-9a-fA-F])['"]`, 'g')
- const m = src.match(re)
- if (m) {
- totalRepl += m.length
- usedTokens.add(rule.token.split('.')[1])
- }
- }
- if (totalRepl === 0) return { changed: false, replacements: 0, needsImport: false }
- // 替换 script 块内的 hex
- const SCRIPT_RE = /(<script)([^>]*)>([\s\S]*?)(<\/script>)/gi
- newSrc = src.replace(SCRIPT_RE, (fullMatch, tagOpen, attrs, body, tagClose) => {
- let newBody = body
- for (const rule of JS_RULES) {
- const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- const re = new RegExp(`['"]${escaped}(?![0-9a-fA-F])['"]`, 'g')
- const m = newBody.match(re)
- if (m) {
- newBody = newBody.replace(re, `'${rule.token}'`)
- }
- }
- return tagOpen + attrs + '>' + newBody + tagClose
- })
- // 在 </script> 后添加 import(如果还没加过)
- if (usedTokens.size > 0 && !src.includes('brand-colors')) {
- const importLine = `\nimport { ${[...usedTokens].sort().join(', ')} } from '@/utils/brand-colors.js'`
- newSrc = newSrc.replace(/(<\/script>)/, `$1${importLine}`)
- }
- if (!dryRun && newSrc !== src) {
- fs.writeFileSync(filePath, newSrc, 'utf8')
- }
- return { changed: newSrc !== src, replacements: totalRepl, needsImport: true }
- }
- function main() {
- const dryRun = process.argv.includes('--dry-run')
- const fileArg = process.argv.find(a => a.startsWith('--file='))?.split('=')[1]
- const targets = fileArg
- ? [path.join(ROOT, fileArg)]
- : TOP_FILES.map(f => path.join(ROOT, f))
- // 如果没指定文件,跑全量扫描(所有 .vue)
- if (!fileArg) {
- const allFiles = walk(ROOT)
- // 过滤出已处理的 TOP 文件,加上其余全部
- const topSet = new Set(TOP_FILES.map(f => path.resolve(ROOT, f)))
- const remaining = allFiles.filter(f => !topSet.has(f))
- targets.push(...remaining)
- }
- let grandTotal = 0
- const log = []
- for (const filePath of targets) {
- if (!fs.existsSync(filePath)) {
- console.warn(`跳过不存在: ${filePath}`)
- continue
- }
- const rel = path.relative(ROOT, filePath)
- const result = processFileSimple(filePath, dryRun)
- if (result.changed || result.replacements > 0) {
- grandTotal += result.replacements
- log.push(`${result.replacements.toString().padStart(4)} ${rel}`)
- if (!dryRun) console.log(`✓ ${rel} (${result.replacements} 处)`)
- } else {
- console.log(`- ${rel} (0 处,跳过)`)
- }
- }
- if (!dryRun) {
- console.log(`\n=== JS 色板收编完成 ===`)
- console.log(`替换总次数: ${grandTotal}`)
- log.sort((a, b) => parseInt(b.split(' ')[0]) - parseInt(a.split(' ')[0]))
- console.log('')
- for (const line of log) console.log(line)
- } else {
- console.log('\n[dry-run] 预览替换量:', grandTotal)
- }
- }
- main()
|