| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- #!/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')
- // 按 <style ...> 块拆分,只处理有目标 hex 的块
- const STYLE_RE = /(<style)([^>]*)>([\s\S]*?)(<\/style>)/gi
- let result = src
- let changed = false
- let totalRepl = 0
- let blocksModified = 0
- const matches = [...src.matchAll(STYLE_RE)]
- for (const match of matches) {
- const fullMatch = match[0]
- const tagOpen = match[1] // "<style"
- const attrs = match[2] // e.g. ' scoped lang="scss"'
- const body = match[3] // CSS content
- const tagClose = match[4] // "</style>"
- const replCount = countReplacements(body, rules)
- if (replCount === 0) continue
- const newBody = applyRules(body, rules)
- totalRepl += replCount
- // 若 <style> 没有 lang="scss",补充上(SCSS 是 CSS 超集,安全)
- let newAttrs = attrs
- if (!/lang\s*=\s*["']scss["']/i.test(attrs)) {
- newAttrs = attrs.trim() ? attrs.trim() + ' lang="scss"' : ' lang="scss"'
- }
- const newBlock = `${tagOpen}${newAttrs}>${newBody}${tagClose}`
- if (newBlock !== fullMatch) {
- result = result.split(fullMatch).join(newBlock)
- changed = true
- blocksModified++
- }
- }
- if (changed) {
- fs.writeFileSync(filePath, result, 'utf8')
- return { changed: true, replacements: totalRepl, blocks: blocksModified }
- }
- return { changed: false, replacements: 0, blocks: 0 }
- }
- function main() {
- const files = walk(ROOT)
- let totalChanged = 0
- let totalReplacements = 0
- let totalBlocks = 0
- const log = []
- for (const file of files) {
- const rel = path.relative(ROOT, file)
- const result = processFile(file)
- if (result.changed) {
- totalChanged++
- totalReplacements += result.replacements
- totalBlocks += result.blocks
- log.push(`${result.replacements.toString().padStart(4)} ${rel}`)
- }
- }
- console.log(`\n=== 替换完成 (${label}) ===`)
- console.log(`文件变更: ${totalChanged}`)
- console.log(`style 块修改: ${totalBlocks}`)
- console.log(`替换次数: ${totalReplacements}`)
- console.log('')
- log.sort((a, b) => parseInt(b.split(' ')[0]) - parseInt(a.split(' ')[0]))
- for (const line of log.slice(0, 50)) console.log(line)
- if (log.length > 50) console.log(`... 共 ${log.length} 个文件`)
- }
- main()
|