| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- #!/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'],
- // ---- 第二轮(高频色,2026-10-20 批量收割)----
- ['#94A3B8', '$app-text-subtle'],
- ['#94a3b8', '$app-text-subtle'],
- ['#1E293B', '$uni-text-color'],
- ['#1e293b', '$uni-text-color'],
- ['#64748B', '$uni-text-color-secondary'],
- ['#64748b', '$uni-text-color-secondary'],
- ['#E2E8F0', '$uni-border-color-light'],
- ['#e2e8f0', '$uni-border-color-light'],
- ['#F5F7FA', '$app-bg'],
- ['#f5f7fa', '$app-bg'],
- ['#667EEA', '$accent-indigo'],
- ['#667eea', '$accent-indigo'],
- ['#9CA3AF', '$facade-text-gray-400'],
- ['#9ca3af', '$facade-text-gray-400'],
- ['#FFF3E0', '$fortune-1-tint'],
- ['#fff3e0', '$fortune-1-tint'],
- ['#FED7AA', '$brand-primary-light-2'],
- ['#fed7aa', '$brand-primary-light-2'],
- ['#F1F5F9', '$facade-fill-slate'],
- ['#f1f5f9', '$facade-fill-slate'],
- ['#4CAF50', '$mat-green'],
- ['#4caf50', '$mat-green'],
- ['#E8F5E9', '$mat-green-tint'],
- ['#e8f5e9', '$mat-green-tint'],
- ['#F59E0B', '$dim-wealth'],
- ['#f59e0b', '$dim-wealth'],
- ['#F8F8F8', '$facade-fill-page'],
- ['#f8f8f8', '$facade-fill-page'],
- ['#FEF3C7', '$status-warning-soft'],
- ['#fef3c7', '$status-warning-soft'],
- ['#E65100', '$mat-orange-strong'],
- ['#e65100', '$mat-orange-strong'],
- ['#2E7D32', '$mat-green-strong'],
- ['#2e7d32', '$mat-green-strong'],
- ['#888888', '$facade-text-faint'],
- ['#888', '$facade-text-faint'],
- ['#F3F4F6', '$facade-fill-gray-100'],
- ['#f3f4f6', '$facade-fill-gray-100'],
- ['#E3F2FD', '$mat-blue-tint'],
- ['#e3f2fd', '$mat-blue-tint'],
- ['#EA580C', '$brand-primary-dark'],
- ['#ea580c', '$brand-primary-dark'],
- ['#E5E7EB', '$facade-gray-200'],
- ['#e5e7eb', '$facade-gray-200'],
- ['#E0E0E0', '$facade-hairline'],
- ['#e0e0e0', '$facade-hairline'],
- ['#1F2937', '$facade-text-gray-800'],
- ['#1f2937', '$facade-text-gray-800'],
- ['#6B7280', '$facade-text-slate'],
- ['#6b7280', '$facade-text-slate'],
- ['#F5FAFE', '$facade-fill-blue-ghost'],
- ['#f5fafe', '$facade-fill-blue-ghost'],
- ['#CBD5E1', '$uni-border-color'],
- ['#cbd5e1', '$uni-border-color'],
- ['#374151', '$facade-text-gray-700'],
- ['#374151', '$facade-text-gray-700'],
- ['#764BA2', '$accent-indigo-deep'],
- ['#764ba2', '$accent-indigo-deep'],
- ['#F8FAFC', '$facade-fill-slate-50'],
- ['#f8fafc', '$facade-fill-slate-50'],
- ['#FFD700', '$accent-gold'],
- ['#ffd700', '$accent-gold'],
- ['#1565C0', '$mat-blue-strong'],
- ['#1565c0', '$mat-blue-strong'],
- ['#DC2626', '$status-danger-deep'],
- ['#dc2626', '$status-danger-deep'],
- ['#475569', '$facade-text-slate-deep'],
- ['#475569', '$facade-text-slate-deep'],
- ['#8B5CF6', '$facade-violet'],
- ['#8b5cf6', '$facade-violet'],
- ['#334155', '$facade-text-slate-700'],
- ['#334155', '$facade-text-slate-700'],
- ['#FFB366', '$dim-body-light'],
- ['#ffb366', '$dim-body-light'],
- ['#FFEBEE', '$fortune-0-tint'],
- ['#ffebee', '$fortune-0-tint'],
- ['#34D399', '$status-success-light-3'],
- ['#34d399', '$status-success-light-3'],
- ['#1A1A2E', '$app-ink-night'],
- ['#1a1a2e', '$app-ink-night'],
- ['#E8E8E8', '$facade-fill-4'],
- ['#e8e8e8', '$facade-fill-4'],
- ['#16A34A', '$status-success-strong-2'],
- ['#16a34a', '$status-success-strong-2'],
- ['#FEE2E2', '$status-danger-soft'],
- ['#fee2e2', '$status-danger-soft'],
- ['#C2410C', '$brand-primary-deep'],
- ['#c2410c', '$brand-primary-deep'],
- ['#FFF7E6', '$brand-primary-tint-2'],
- ['#fff7e6', '$brand-primary-tint-2'],
- ['#22C55E', '$status-success-2'],
- ['#22c55e', '$status-success-2'],
- ['#3B82F6', '$status-info'],
- ['#3b82f6', '$status-info'],
- ['#2196F3', '$mat-blue-base'],
- ['#2196f3', '$mat-blue-base'],
- ['#D97706', '$status-warning-deep'],
- ['#d97706', '$status-warning-deep'],
- ['#0EA5E9', '$uni-info'],
- ['#0ea5e9', '$uni-info'],
- ['#DCFCE7', '$status-success-soft-2'],
- ['#dcfce7', '$status-success-soft-2'],
- ['#555555', '$facade-text-deep'],
- ['#555', '$facade-text-deep'],
- ['#818CF8', '$dim-wisdom-light'],
- ['#818cf8', '$dim-wisdom-light'],
- ['#FF9800', '$mat-orange-base'],
- ['#ff9800', '$mat-orange-base'],
- ['#F5F9FC', '$uni-bg-color'],
- ['#f5f9fc', '$uni-bg-color'],
- ['#F0FDF4', '$status-success-tint-2'],
- ['#f0fdf4', '$status-success-tint-2'],
- ['#D1FAE5', '$status-success-soft'],
- ['#d1fae5', '$status-success-soft'],
- ['#DBEAFE', '$status-info-soft'],
- ['#dbeafe', '$status-info-soft'],
- ['#92400E', '$status-warning-strong'],
- ['#92400e', '$status-warning-strong'],
- ].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 SECOND_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()
|