replace-hex-to-token.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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. // ---- 第五轮(现有令牌补登记,2026-10-20)----
  217. ['#059669', '$status-success-deep'],
  218. ['#059669', '$status-success-deep'],
  219. ['#FF6B35', '$mind-accent'],
  220. ['#ff6b35', '$mind-accent'],
  221. ['#D1D5DB', '$app-border-strong'],
  222. ['#d1d5db', '$app-border-strong'],
  223. ['#FFEDD5', '$facade-brand-tint-2'],
  224. ['#ffedd5', '$facade-brand-tint-2'],
  225. ['#B45309', '$status-warning-strong-2'],
  226. ['#b45309', '$status-warning-strong-2'],
  227. ['#16213E', '$app-ink-night-2'],
  228. ['#16213e', '$app-ink-night-2'],
  229. ['#ECFDF5', '$status-success-tint'],
  230. ['#ecfdf5', '$status-success-tint'],
  231. ['#FFFBEB', '$dim-wealth-tint'],
  232. ['#fffbeb', '$dim-wealth-tint'],
  233. ['#9A3412', '$mat-orange-deep'],
  234. ['#9a3412', '$mat-orange-deep'],
  235. ['#FDE68A', '$status-warning-light'],
  236. ['#fde68a', '$status-warning-light'],
  237. ['#F0F4FF', '$dim-wisdom-tint-3'],
  238. ['#f0f4ff', '$dim-wisdom-tint-3'],
  239. ['#2563EB', '$status-info-deep'],
  240. ['#2563eb', '$status-info-deep'],
  241. ['#FFF8E1', '$mat-amber-tint'],
  242. ['#fff8e1', '$mat-amber-tint'],
  243. ['#0284C7', '$status-info-deep-2'],
  244. ['#0284c7', '$status-info-deep-2'],
  245. ['#1890FF', '$status-info-3'],
  246. ['#1890ff', '$status-info-3'],
  247. ['#EEF2FF', '$dim-wisdom-tint-2'],
  248. ['#eef2ff', '$dim-wisdom-tint-2'],
  249. ['#F9F9F9', '$facade-fill-ghost-2'],
  250. ['#f9f9f9', '$facade-fill-ghost-2'],
  251. ['#EC4899', '$accent-pink'],
  252. ['#ec4899', '$accent-pink'],
  253. // ---- 第六轮(新增全局令牌,2026-10-20)----
  254. ['#FFF0E0', '$warm-peach'],
  255. ['#fff0e0', '$warm-peach'],
  256. ['#8A94A6', '$text-steel'],
  257. ['#8a94a6', '$text-steel'],
  258. ['#2A2A4A', '$game-ink'],
  259. ['#2a2a4a', '$game-ink'],
  260. ['#1F2D3D', '$ink-steel'],
  261. ['#1f2d3d', '$ink-steel'],
  262. ['#EDE9FE', '$violet-tint-100'],
  263. ['#ede9fe', '$violet-tint-100'],
  264. ['#8D6E63', '$brown-warm'],
  265. ['#8d6e63', '$brown-warm'],
  266. ['#F56C6C', '$el-danger'],
  267. ['#f56c6c', '$el-danger'],
  268. ['#F44336', '$mat-red'],
  269. ['#f44336', '$mat-red'],
  270. ['#52C41A', '$status-success-3'],
  271. ['#52c41a', '$status-success-3'],
  272. ['#1A1A1A', '$text-charcoal'],
  273. ['#1a1a1a', '$text-charcoal'],
  274. ['#AAA', '$text-ghost-soft'],
  275. ['#aaa', '$text-ghost-soft'],
  276. ].sort((a, b) => b[0].length - a[0].length)
  277. const GRAY_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i < 30 })
  278. const BRAND_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i >= 30 })
  279. const SECOND_RULES = ALL_RULES.filter(r => { const i = ALL_RULES.indexOf(r); return i >= 30 })
  280. const batch = process.argv.find(a => a.startsWith('--batch='))?.split('=')[1]
  281. const rules = batch === 'brand' ? BRAND_RULES : batch === 'gray' ? GRAY_RULES : ALL_RULES
  282. const label = batch === 'brand' ? '品牌/状态色' : batch === 'gray' ? '灰阶' : '全部'
  283. console.log(`\n批次: ${label} 规则数: ${rules.length}\n`)
  284. function walk(dir, out = []) {
  285. for (const name of fs.readdirSync(dir)) {
  286. if (EXCLUDE_DIRS.has(name)) continue
  287. const p = path.join(dir, name)
  288. const st = fs.statSync(p)
  289. if (st.isDirectory()) walk(p, out)
  290. else if (name.endsWith('.vue')) out.push(p)
  291. }
  292. return out
  293. }
  294. function countReplacements(text, rules) {
  295. let n = 0
  296. for (const [hex] of rules) {
  297. const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  298. n += (text.match(new RegExp(escaped + '(?![0-9a-fA-F])', 'g')) || []).length
  299. }
  300. return n
  301. }
  302. function applyRules(text, rules) {
  303. let result = text
  304. for (const [hex, token] of rules) {
  305. const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  306. result = result.replace(new RegExp(escaped + '(?![0-9a-fA-F])', 'g'), token)
  307. }
  308. return result
  309. }
  310. function processFile(filePath) {
  311. const src = fs.readFileSync(filePath, 'utf8')
  312. // 按 <style ...> 块拆分,只处理有目标 hex 的块
  313. const STYLE_RE = /(<style)([^>]*)>([\s\S]*?)(<\/style>)/gi
  314. let result = src
  315. let changed = false
  316. let totalRepl = 0
  317. let blocksModified = 0
  318. const matches = [...src.matchAll(STYLE_RE)]
  319. for (const match of matches) {
  320. const fullMatch = match[0]
  321. const tagOpen = match[1] // "<style"
  322. const attrs = match[2] // e.g. ' scoped lang="scss"'
  323. const body = match[3] // CSS content
  324. const tagClose = match[4] // "</style>"
  325. const replCount = countReplacements(body, rules)
  326. if (replCount === 0) continue
  327. const newBody = applyRules(body, rules)
  328. totalRepl += replCount
  329. // 若 <style> 没有 lang="scss",补充上(SCSS 是 CSS 超集,安全)
  330. let newAttrs = attrs
  331. if (!/lang\s*=\s*["']scss["']/i.test(attrs)) {
  332. newAttrs = attrs.trim() ? attrs.trim() + ' lang="scss"' : ' lang="scss"'
  333. }
  334. const newBlock = `${tagOpen}${newAttrs}>${newBody}${tagClose}`
  335. if (newBlock !== fullMatch) {
  336. result = result.split(fullMatch).join(newBlock)
  337. changed = true
  338. blocksModified++
  339. }
  340. }
  341. if (changed) {
  342. fs.writeFileSync(filePath, result, 'utf8')
  343. return { changed: true, replacements: totalRepl, blocks: blocksModified }
  344. }
  345. return { changed: false, replacements: 0, blocks: 0 }
  346. }
  347. function main() {
  348. const files = walk(ROOT)
  349. let totalChanged = 0
  350. let totalReplacements = 0
  351. let totalBlocks = 0
  352. const log = []
  353. for (const file of files) {
  354. const rel = path.relative(ROOT, file)
  355. const result = processFile(file)
  356. if (result.changed) {
  357. totalChanged++
  358. totalReplacements += result.replacements
  359. totalBlocks += result.blocks
  360. log.push(`${result.replacements.toString().padStart(4)} ${rel}`)
  361. }
  362. }
  363. console.log(`\n=== 替换完成 (${label}) ===`)
  364. console.log(`文件变更: ${totalChanged}`)
  365. console.log(`style 块修改: ${totalBlocks}`)
  366. console.log(`替换次数: ${totalReplacements}`)
  367. console.log('')
  368. log.sort((a, b) => parseInt(b.split(' ')[0]) - parseInt(a.split(' ')[0]))
  369. for (const line of log.slice(0, 50)) console.log(line)
  370. if (log.length > 50) console.log(`... 共 ${log.length} 个文件`)
  371. }
  372. main()