replace-hex-to-token.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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. // ---- 第四轮(flat 设计主题,2026-10-20 收编为 $flat-* 全局令牌)----
  186. ['#4ECDC4', '$flat-teal'],
  187. ['#4ecdc4', '$flat-teal'],
  188. ['#44B09E', '$flat-teal-deep'],
  189. ['#44b09e', '$flat-teal-deep'],
  190. ['#A8E063', '$flat-green'],
  191. ['#a8e063', '$flat-green'],
  192. ['#56AB2F', '$flat-green-deep'],
  193. ['#56ab2f', '$flat-green-deep'],
  194. ['#FFF9F0', '$flat-bg'],
  195. ['#fff9f0', '$flat-bg'],
  196. ['#FFF5EB', '$flat-card'],
  197. ['#fff5eb', '$flat-card'],
  198. ['#FFF0E6', '$flat-warm'],
  199. ['#fff0e6', '$flat-warm'],
  200. ['#F5F0EB', '$flat-line'],
  201. ['#f5f0eb', '$flat-line'],
  202. ['#2D3436', '$flat-ink'],
  203. ['#2d3436', '$flat-ink'],
  204. ['#B2BEC3', '$flat-muted'],
  205. ['#b2bec3', '$flat-muted'],
  206. ['#636E72', '$flat-muted-2'],
  207. ['#636e72', '$flat-muted-2'],
  208. ['#FFE0E0', '$flat-danger-line'],
  209. ['#ffe0e0', '$flat-danger-line'],
  210. ['#E17055', '$flat-orange-text'],
  211. ['#e17055', '$flat-orange-text'],
  212. ['#FFB088', '$flat-orange-soft'],
  213. ['#ffb088', '$flat-orange-soft'],
  214. ['#FFD93D', '$flat-bar'],
  215. ['#ffd93d', '$flat-bar'],
  216. ].sort((a, b) => b[0].length - a[0].length)
  217. const GRAY_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i < 30 })
  218. const BRAND_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i >= 30 })
  219. const SECOND_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i >= 30 })
  220. const batch = process.argv.find(a => a.startsWith('--batch='))?.split('=')[1]
  221. const rules = batch === 'brand' ? BRAND_RULES : batch === 'gray' ? GRAY_RULES : ALL_RULES
  222. const label = batch === 'brand' ? '品牌/状态色' : batch === 'gray' ? '灰阶' : '全部'
  223. console.log(`\n批次: ${label} 规则数: ${rules.length}\n`)
  224. function walk(dir, out = []) {
  225. for (const name of fs.readdirSync(dir)) {
  226. if (EXCLUDE_DIRS.has(name)) continue
  227. const p = path.join(dir, name)
  228. const st = fs.statSync(p)
  229. if (st.isDirectory()) walk(p, out)
  230. else if (name.endsWith('.vue')) out.push(p)
  231. }
  232. return out
  233. }
  234. function countReplacements(text, rules) {
  235. let n = 0
  236. for (const [hex] of rules) {
  237. const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  238. n += (text.match(new RegExp(escaped + '(?![0-9a-fA-F])', 'g')) || []).length
  239. }
  240. return n
  241. }
  242. function applyRules(text, rules) {
  243. let result = text
  244. for (const [hex, token] of rules) {
  245. const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  246. result = result.replace(new RegExp(escaped + '(?![0-9a-fA-F])', 'g'), token)
  247. }
  248. return result
  249. }
  250. function processFile(filePath) {
  251. const src = fs.readFileSync(filePath, 'utf8')
  252. // 按 <style ...> 块拆分,只处理有目标 hex 的块
  253. const STYLE_RE = /(<style)([^>]*)>([\s\S]*?)(<\/style>)/gi
  254. let result = src
  255. let changed = false
  256. let totalRepl = 0
  257. let blocksModified = 0
  258. const matches = [...src.matchAll(STYLE_RE)]
  259. for (const match of matches) {
  260. const fullMatch = match[0]
  261. const tagOpen = match[1] // "<style"
  262. const attrs = match[2] // e.g. ' scoped lang="scss"'
  263. const body = match[3] // CSS content
  264. const tagClose = match[4] // "</style>"
  265. const replCount = countReplacements(body, rules)
  266. if (replCount === 0) continue
  267. const newBody = applyRules(body, rules)
  268. totalRepl += replCount
  269. // 若 <style> 没有 lang="scss",补充上(SCSS 是 CSS 超集,安全)
  270. let newAttrs = attrs
  271. if (!/lang\s*=\s*["']scss["']/i.test(attrs)) {
  272. newAttrs = attrs.trim() ? attrs.trim() + ' lang="scss"' : ' lang="scss"'
  273. }
  274. const newBlock = `${tagOpen}${newAttrs}>${newBody}${tagClose}`
  275. if (newBlock !== fullMatch) {
  276. result = result.split(fullMatch).join(newBlock)
  277. changed = true
  278. blocksModified++
  279. }
  280. }
  281. if (changed) {
  282. fs.writeFileSync(filePath, result, 'utf8')
  283. return { changed: true, replacements: totalRepl, blocks: blocksModified }
  284. }
  285. return { changed: false, replacements: 0, blocks: 0 }
  286. }
  287. function main() {
  288. const files = walk(ROOT)
  289. let totalChanged = 0
  290. let totalReplacements = 0
  291. let totalBlocks = 0
  292. const log = []
  293. for (const file of files) {
  294. const rel = path.relative(ROOT, file)
  295. const result = processFile(file)
  296. if (result.changed) {
  297. totalChanged++
  298. totalReplacements += result.replacements
  299. totalBlocks += result.blocks
  300. log.push(`${result.replacements.toString().padStart(4)} ${rel}`)
  301. }
  302. }
  303. console.log(`\n=== 替换完成 (${label}) ===`)
  304. console.log(`文件变更: ${totalChanged}`)
  305. console.log(`style 块修改: ${totalBlocks}`)
  306. console.log(`替换次数: ${totalReplacements}`)
  307. console.log('')
  308. log.sort((a, b) => parseInt(b.split(' ')[0]) - parseInt(a.split(' ')[0]))
  309. for (const line of log.slice(0, 50)) console.log(line)
  310. if (log.length > 50) console.log(`... 共 ${log.length} 个文件`)
  311. }
  312. main()