replace-hex-to-token.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. #!/usr/bin/env node
  2. /**
  3. * 批量替换硬编码色值为 SCSS 令牌(CSS style 块内,零视觉风险)
  4. * 用法:
  5. * node scripts/replace-hex-to-token.js --batch gray # 仅灰阶
  6. * node scripts/replace-hex-to-token.js --batch brand # 仅品牌/状态色
  7. * node scripts/replace-hex-to-token.js # 全部
  8. */
  9. const fs = require('fs')
  10. const path = require('path')
  11. const ROOT = path.resolve(__dirname, '..')
  12. const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'docs'])
  13. const ALL_RULES = [
  14. // ---- 灰阶(neutral)----
  15. ['#FFFFFF', '$app-surface'],
  16. ['#ffffff', '$app-surface'],
  17. ['#FFF', '$app-surface'],
  18. ['#fff', '$app-surface'],
  19. ['#333333', '$facade-text-strong'],
  20. ['#333', '$facade-text-strong'],
  21. ['#666666', '$facade-text-body'],
  22. ['#666', '$facade-text-body'],
  23. ['#999999', '$facade-text-muted'],
  24. ['#999', '$facade-text-muted'],
  25. ['#F5F5F5', '$facade-fill-2'],
  26. ['#f5f5f5', '$facade-fill-2'],
  27. ['#F0F0F0', '$facade-fill'],
  28. ['#f0f0f0', '$facade-fill'],
  29. ['#FAFAFA', '$facade-fill-ghost'],
  30. ['#fafafa', '$facade-fill-ghost'],
  31. ['#F9FAFB', '$facade-fill-gray-50'],
  32. ['#f9fafb', '$facade-fill-gray-50'],
  33. ['#BBBBBB', '$facade-text-ghost-light'],
  34. ['#bbbbbb', '$facade-text-ghost-light'],
  35. ['#BBB', '$facade-text-ghost-light'],
  36. ['#bbb', '$facade-text-ghost-light'],
  37. ['#CCCCCC', '$facade-text-ghost'],
  38. ['#cccccc', '$facade-text-ghost'],
  39. ['#CCC', '$facade-text-ghost'],
  40. ['#ccc', '$facade-text-ghost'],
  41. ['#DDDDDD', '$facade-border-soft'],
  42. ['#dddddd', '$facade-border-soft'],
  43. ['#DDD', '$facade-border-soft'],
  44. ['#ddd', '$facade-border-soft'],
  45. ['#EEEEEE', '$facade-hairline-2'],
  46. ['#eeeeee', '$facade-hairline-2'],
  47. ['#EEE', '$facade-hairline-2'],
  48. ['#eee', '$facade-hairline-2'],
  49. // ---- 品牌/状态色(brand/status)----
  50. ['#F97316', '$brand-primary'],
  51. ['#f97316', '$brand-primary'],
  52. ['#FF8C42', '$dim-body'],
  53. ['#ff8c42', '$dim-body'],
  54. ['#FFF7ED', '$uni-primary-pale'],
  55. ['#fff7ed', '$uni-primary-pale'],
  56. ['#FB923C', '$brand-primary-light'],
  57. ['#fb923c', '$brand-primary-light'],
  58. ['#10B981', '$status-success'],
  59. ['#10b981', '$status-success'],
  60. ['#EF4444', '$status-danger'],
  61. ['#ef4444', '$status-danger'],
  62. ['#6366F1', '$dim-wisdom'],
  63. ['#6366f1', '$dim-wisdom'],
  64. ['#C62828', '$status-danger-strong-2'],
  65. ['#c62828', '$status-danger-strong-2'],
  66. // ---- 第二轮(高频色,2026-10-20 批量收割)----
  67. ['#94A3B8', '$app-text-subtle'],
  68. ['#94a3b8', '$app-text-subtle'],
  69. ['#1E293B', '$uni-text-color'],
  70. ['#1e293b', '$uni-text-color'],
  71. ['#64748B', '$uni-text-color-secondary'],
  72. ['#64748b', '$uni-text-color-secondary'],
  73. ['#E2E8F0', '$uni-border-color-light'],
  74. ['#e2e8f0', '$uni-border-color-light'],
  75. ['#F5F7FA', '$app-bg'],
  76. ['#f5f7fa', '$app-bg'],
  77. ['#667EEA', '$accent-indigo'],
  78. ['#667eea', '$accent-indigo'],
  79. ['#9CA3AF', '$facade-text-gray-400'],
  80. ['#9ca3af', '$facade-text-gray-400'],
  81. ['#FFF3E0', '$fortune-1-tint'],
  82. ['#fff3e0', '$fortune-1-tint'],
  83. ['#FED7AA', '$brand-primary-light-2'],
  84. ['#fed7aa', '$brand-primary-light-2'],
  85. ['#F1F5F9', '$facade-fill-slate'],
  86. ['#f1f5f9', '$facade-fill-slate'],
  87. ['#4CAF50', '$mat-green'],
  88. ['#4caf50', '$mat-green'],
  89. ['#E8F5E9', '$mat-green-tint'],
  90. ['#e8f5e9', '$mat-green-tint'],
  91. ['#F59E0B', '$dim-wealth'],
  92. ['#f59e0b', '$dim-wealth'],
  93. ['#F8F8F8', '$facade-fill-page'],
  94. ['#f8f8f8', '$facade-fill-page'],
  95. ['#FEF3C7', '$status-warning-soft'],
  96. ['#fef3c7', '$status-warning-soft'],
  97. ['#E65100', '$mat-orange-strong'],
  98. ['#e65100', '$mat-orange-strong'],
  99. ['#2E7D32', '$mat-green-strong'],
  100. ['#2e7d32', '$mat-green-strong'],
  101. ['#888888', '$facade-text-faint'],
  102. ['#888', '$facade-text-faint'],
  103. ['#F3F4F6', '$facade-fill-gray-100'],
  104. ['#f3f4f6', '$facade-fill-gray-100'],
  105. ['#E3F2FD', '$mat-blue-tint'],
  106. ['#e3f2fd', '$mat-blue-tint'],
  107. ['#EA580C', '$brand-primary-dark'],
  108. ['#ea580c', '$brand-primary-dark'],
  109. ['#E5E7EB', '$facade-gray-200'],
  110. ['#e5e7eb', '$facade-gray-200'],
  111. ['#E0E0E0', '$facade-hairline'],
  112. ['#e0e0e0', '$facade-hairline'],
  113. ['#1F2937', '$facade-text-gray-800'],
  114. ['#1f2937', '$facade-text-gray-800'],
  115. ['#6B7280', '$facade-text-slate'],
  116. ['#6b7280', '$facade-text-slate'],
  117. ['#F5FAFE', '$facade-fill-blue-ghost'],
  118. ['#f5fafe', '$facade-fill-blue-ghost'],
  119. ['#CBD5E1', '$uni-border-color'],
  120. ['#cbd5e1', '$uni-border-color'],
  121. ['#374151', '$facade-text-gray-700'],
  122. ['#374151', '$facade-text-gray-700'],
  123. ['#764BA2', '$accent-indigo-deep'],
  124. ['#764ba2', '$accent-indigo-deep'],
  125. ['#F8FAFC', '$facade-fill-slate-50'],
  126. ['#f8fafc', '$facade-fill-slate-50'],
  127. ['#FFD700', '$accent-gold'],
  128. ['#ffd700', '$accent-gold'],
  129. ['#1565C0', '$mat-blue-strong'],
  130. ['#1565c0', '$mat-blue-strong'],
  131. ['#DC2626', '$status-danger-deep'],
  132. ['#dc2626', '$status-danger-deep'],
  133. ['#475569', '$facade-text-slate-deep'],
  134. ['#475569', '$facade-text-slate-deep'],
  135. ['#8B5CF6', '$facade-violet'],
  136. ['#8b5cf6', '$facade-violet'],
  137. ['#334155', '$facade-text-slate-700'],
  138. ['#334155', '$facade-text-slate-700'],
  139. ['#FFB366', '$dim-body-light'],
  140. ['#ffb366', '$dim-body-light'],
  141. ['#FFEBEE', '$fortune-0-tint'],
  142. ['#ffebee', '$fortune-0-tint'],
  143. ['#34D399', '$status-success-light-3'],
  144. ['#34d399', '$status-success-light-3'],
  145. ['#1A1A2E', '$app-ink-night'],
  146. ['#1a1a2e', '$app-ink-night'],
  147. ['#E8E8E8', '$facade-fill-4'],
  148. ['#e8e8e8', '$facade-fill-4'],
  149. ['#16A34A', '$status-success-strong-2'],
  150. ['#16a34a', '$status-success-strong-2'],
  151. ['#FEE2E2', '$status-danger-soft'],
  152. ['#fee2e2', '$status-danger-soft'],
  153. ['#C2410C', '$brand-primary-deep'],
  154. ['#c2410c', '$brand-primary-deep'],
  155. ['#FFF7E6', '$brand-primary-tint-2'],
  156. ['#fff7e6', '$brand-primary-tint-2'],
  157. ['#22C55E', '$status-success-2'],
  158. ['#22c55e', '$status-success-2'],
  159. ['#3B82F6', '$status-info'],
  160. ['#3b82f6', '$status-info'],
  161. ['#2196F3', '$mat-blue-base'],
  162. ['#2196f3', '$mat-blue-base'],
  163. ['#D97706', '$status-warning-deep'],
  164. ['#d97706', '$status-warning-deep'],
  165. ['#0EA5E9', '$uni-info'],
  166. ['#0ea5e9', '$uni-info'],
  167. ['#DCFCE7', '$status-success-soft-2'],
  168. ['#dcfce7', '$status-success-soft-2'],
  169. ['#555555', '$facade-text-deep'],
  170. ['#555', '$facade-text-deep'],
  171. ['#818CF8', '$dim-wisdom-light'],
  172. ['#818cf8', '$dim-wisdom-light'],
  173. ['#FF9800', '$mat-orange-base'],
  174. ['#ff9800', '$mat-orange-base'],
  175. ['#F5F9FC', '$uni-bg-color'],
  176. ['#f5f9fc', '$uni-bg-color'],
  177. ['#F0FDF4', '$status-success-tint-2'],
  178. ['#f0fdf4', '$status-success-tint-2'],
  179. ['#D1FAE5', '$status-success-soft'],
  180. ['#d1fae5', '$status-success-soft'],
  181. ['#DBEAFE', '$status-info-soft'],
  182. ['#dbeafe', '$status-info-soft'],
  183. ['#92400E', '$status-warning-strong'],
  184. ['#92400e', '$status-warning-strong'],
  185. ].sort((a, b) => b[0].length - a[0].length)
  186. const GRAY_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i < 30 })
  187. const BRAND_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i >= 30 })
  188. const SECOND_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i >= 30 })
  189. const batch = process.argv.find(a => a.startsWith('--batch='))?.split('=')[1]
  190. const rules = batch === 'brand' ? BRAND_RULES : batch === 'gray' ? GRAY_RULES : ALL_RULES
  191. const label = batch === 'brand' ? '品牌/状态色' : batch === 'gray' ? '灰阶' : '全部'
  192. console.log(`\n批次: ${label} 规则数: ${rules.length}\n`)
  193. function walk(dir, out = []) {
  194. for (const name of fs.readdirSync(dir)) {
  195. if (EXCLUDE_DIRS.has(name)) continue
  196. const p = path.join(dir, name)
  197. const st = fs.statSync(p)
  198. if (st.isDirectory()) walk(p, out)
  199. else if (name.endsWith('.vue')) out.push(p)
  200. }
  201. return out
  202. }
  203. function countReplacements(text, rules) {
  204. let n = 0
  205. for (const [hex] of rules) {
  206. const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  207. n += (text.match(new RegExp(escaped + '(?![0-9a-fA-F])', 'g')) || []).length
  208. }
  209. return n
  210. }
  211. function applyRules(text, rules) {
  212. let result = text
  213. for (const [hex, token] of rules) {
  214. const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  215. result = result.replace(new RegExp(escaped + '(?![0-9a-fA-F])', 'g'), token)
  216. }
  217. return result
  218. }
  219. function processFile(filePath) {
  220. const src = fs.readFileSync(filePath, 'utf8')
  221. // 按 <style ...> 块拆分,只处理有目标 hex 的块
  222. const STYLE_RE = /(<style)([^>]*)>([\s\S]*?)(<\/style>)/gi
  223. let result = src
  224. let changed = false
  225. let totalRepl = 0
  226. let blocksModified = 0
  227. const matches = [...src.matchAll(STYLE_RE)]
  228. for (const match of matches) {
  229. const fullMatch = match[0]
  230. const tagOpen = match[1] // "<style"
  231. const attrs = match[2] // e.g. ' scoped lang="scss"'
  232. const body = match[3] // CSS content
  233. const tagClose = match[4] // "</style>"
  234. const replCount = countReplacements(body, rules)
  235. if (replCount === 0) continue
  236. const newBody = applyRules(body, rules)
  237. totalRepl += replCount
  238. // 若 <style> 没有 lang="scss",补充上(SCSS 是 CSS 超集,安全)
  239. let newAttrs = attrs
  240. if (!/lang\s*=\s*["']scss["']/i.test(attrs)) {
  241. newAttrs = attrs.trim() ? attrs.trim() + ' lang="scss"' : ' lang="scss"'
  242. }
  243. const newBlock = `${tagOpen}${newAttrs}>${newBody}${tagClose}`
  244. if (newBlock !== fullMatch) {
  245. result = result.split(fullMatch).join(newBlock)
  246. changed = true
  247. blocksModified++
  248. }
  249. }
  250. if (changed) {
  251. fs.writeFileSync(filePath, result, 'utf8')
  252. return { changed: true, replacements: totalRepl, blocks: blocksModified }
  253. }
  254. return { changed: false, replacements: 0, blocks: 0 }
  255. }
  256. function main() {
  257. const files = walk(ROOT)
  258. let totalChanged = 0
  259. let totalReplacements = 0
  260. let totalBlocks = 0
  261. const log = []
  262. for (const file of files) {
  263. const rel = path.relative(ROOT, file)
  264. const result = processFile(file)
  265. if (result.changed) {
  266. totalChanged++
  267. totalReplacements += result.replacements
  268. totalBlocks += result.blocks
  269. log.push(`${result.replacements.toString().padStart(4)} ${rel}`)
  270. }
  271. }
  272. console.log(`\n=== 替换完成 (${label}) ===`)
  273. console.log(`文件变更: ${totalChanged}`)
  274. console.log(`style 块修改: ${totalBlocks}`)
  275. console.log(`替换次数: ${totalReplacements}`)
  276. console.log('')
  277. log.sort((a, b) => parseInt(b.split(' ')[0]) - parseInt(a.split(' ')[0]))
  278. for (const line of log.slice(0, 50)) console.log(line)
  279. if (log.length > 50) console.log(`... 共 ${log.length} 个文件`)
  280. }
  281. main()