#!/usr/bin/env node /** * 批量替换硬编码色值为 SCSS 令牌(CSS style 块内,零视觉风险) * 用法: * node scripts/replace-hex-to-token.js --batch gray # 仅灰阶 * node scripts/replace-hex-to-token.js --batch brand # 仅品牌/状态色 * node scripts/replace-hex-to-token.js # 全部 */ const fs = require('fs') const path = require('path') const ROOT = path.resolve(__dirname, '..') const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'docs']) const ALL_RULES = [ // ---- 灰阶(neutral)---- ['#FFFFFF', '$app-surface'], ['#ffffff', '$app-surface'], ['#FFF', '$app-surface'], ['#fff', '$app-surface'], ['#333333', '$facade-text-strong'], ['#333', '$facade-text-strong'], ['#666666', '$facade-text-body'], ['#666', '$facade-text-body'], ['#999999', '$facade-text-muted'], ['#999', '$facade-text-muted'], ['#F5F5F5', '$facade-fill-2'], ['#f5f5f5', '$facade-fill-2'], ['#F0F0F0', '$facade-fill'], ['#f0f0f0', '$facade-fill'], ['#FAFAFA', '$facade-fill-ghost'], ['#fafafa', '$facade-fill-ghost'], ['#F9FAFB', '$facade-fill-gray-50'], ['#f9fafb', '$facade-fill-gray-50'], ['#BBBBBB', '$facade-text-ghost-light'], ['#bbbbbb', '$facade-text-ghost-light'], ['#BBB', '$facade-text-ghost-light'], ['#bbb', '$facade-text-ghost-light'], ['#CCCCCC', '$facade-text-ghost'], ['#cccccc', '$facade-text-ghost'], ['#CCC', '$facade-text-ghost'], ['#ccc', '$facade-text-ghost'], ['#DDDDDD', '$facade-border-soft'], ['#dddddd', '$facade-border-soft'], ['#DDD', '$facade-border-soft'], ['#ddd', '$facade-border-soft'], ['#EEEEEE', '$facade-hairline-2'], ['#eeeeee', '$facade-hairline-2'], ['#EEE', '$facade-hairline-2'], ['#eee', '$facade-hairline-2'], // ---- 品牌/状态色(brand/status)---- ['#F97316', '$brand-primary'], ['#f97316', '$brand-primary'], ['#FF8C42', '$dim-body'], ['#ff8c42', '$dim-body'], ['#FFF7ED', '$uni-primary-pale'], ['#fff7ed', '$uni-primary-pale'], ['#FB923C', '$brand-primary-light'], ['#fb923c', '$brand-primary-light'], ['#10B981', '$status-success'], ['#10b981', '$status-success'], ['#EF4444', '$status-danger'], ['#ef4444', '$status-danger'], ['#6366F1', '$dim-wisdom'], ['#6366f1', '$dim-wisdom'], ['#C62828', '$status-danger-strong-2'], ['#c62828', '$status-danger-strong-2'], ].sort((a, b) => b[0].length - a[0].length) const GRAY_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i < 30 }) const BRAND_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i >= 30 }) const batch = process.argv.find(a => a.startsWith('--batch='))?.split('=')[1] const rules = batch === 'brand' ? BRAND_RULES : batch === 'gray' ? GRAY_RULES : ALL_RULES const label = batch === 'brand' ? '品牌/状态色' : batch === 'gray' ? '灰阶' : '全部' console.log(`\n批次: ${label} 规则数: ${rules.length}\n`) 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 countReplacements(text, rules) { let n = 0 for (const [hex] of rules) { const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') n += (text.match(new RegExp(escaped + '(?![0-9a-fA-F])', 'g')) || []).length } return n } function applyRules(text, rules) { let result = text for (const [hex, token] of rules) { const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') result = result.replace(new RegExp(escaped + '(?![0-9a-fA-F])', 'g'), token) } return result } function processFile(filePath) { const src = fs.readFileSync(filePath, 'utf8') // 按 " const replCount = countReplacements(body, rules) if (replCount === 0) continue const newBody = applyRules(body, rules) totalRepl += replCount // 若