d31-script-hex-harvest.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. #!/usr/bin/env node
  2. /**
  3. * D31: script 段维度色批量收编 BrandColors
  4. * 只替换「整段字符串就是 hex」的写法('#10B981' / "#10B981"),避免破坏模板字符串/渐变拼接。
  5. * 自动按文件深度计算相对 import 路径;已 import 的文件不重复注入。
  6. *
  7. * 用法: node scripts/d31-script-hex-harvest.js [--dry]
  8. */
  9. const fs = require('fs');
  10. const path = require('path');
  11. const ROOT = path.resolve(__dirname, '..');
  12. const DRY = process.argv.includes('--dry');
  13. const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'scripts', '.tmp_equiv']);
  14. // hex -> BrandColors key(仅登记已存在于 utils/brand-colors.js 的键)
  15. const MAP = {
  16. '#10B981': 'success',
  17. '#F59E0B': 'wealth',
  18. '#FF6B9D': 'mind',
  19. '#FF8C42': 'body',
  20. '#F97316': 'primary',
  21. '#6366F1': 'wisdom',
  22. '#FFFFFF': 'white',
  23. '#3B82F6': 'infoBlue',
  24. '#EF4444': 'danger',
  25. '#94A3B8': 'appTextSubtle',
  26. '#8B5CF6': 'violet',
  27. '#EA580C': 'orangeDeep',
  28. '#0EA5E9': 'infoSky',
  29. '#EC4899': 'pink',
  30. '#6B7280': 'textSlate',
  31. '#4CAF50': 'matGreen',
  32. '#FF6B35': 'mindAccent',
  33. '#8D6E63': 'brownWarm',
  34. // ---- D40:跨文件重复的 script 色值(≥2 处)----
  35. '#A8A29E': 'stone400',
  36. '#64748B': 'slate500',
  37. '#444444': 'textDeep',
  38. '#444': 'textDeep',
  39. '#FFFBEB': 'amber50',
  40. '#1E293B': 'slate800',
  41. '#78716C': 'stone500',
  42. '#E7E5E4': 'stone200',
  43. '#999999': 'textMuted',
  44. '#999': 'textMuted',
  45. '#ECFDF5': 'emerald50',
  46. '#06B6D4': 'cyan500',
  47. '#1C1917': 'stone900',
  48. '#CBD5E1': 'slate300',
  49. '#475569': 'slate600',
  50. '#F56C6C': 'elementDanger',
  51. '#C0C0C0': 'silver',
  52. '#FFF0F5': 'mindTint',
  53. '#4F46E5': 'indigo600',
  54. '#EEF2FF': 'indigo50',
  55. '#DB2777': 'pink600',
  56. '#FDF2F8': 'pink50',
  57. '#F5F5F4': 'stone100',
  58. '#000000': 'black',
  59. '#4A5568': 'gray700',
  60. '#FFB74D': 'orange300',
  61. '#E2E8F0': 'slate200',
  62. '#EEF2F7': 'slateGhost',
  63. '#667EEA': 'indigoPeriwinkle',
  64. '#FFF': 'white',
  65. '#FFFFFF': 'white',
  66. '#F3E5F5': 'tintPurpleSoft',
  67. '#FFF3E0': 'tintOrangeSoft',
  68. '#E8F5E9': 'tintGreenSoft',
  69. '#FCE4EC': 'tintPinkSoft',
  70. };
  71. const bcSrc = fs.readFileSync(path.join(ROOT, 'utils/brand-colors.js'), 'utf8');
  72. for (const key of Object.values(MAP)) {
  73. if (!new RegExp('^\\s*' + key + ':', 'm').test(bcSrc)) {
  74. console.error('缺少 BrandColors 键: ' + key);
  75. process.exit(1);
  76. }
  77. }
  78. function walk(dir, out = []) {
  79. for (const name of fs.readdirSync(dir)) {
  80. if (EXCLUDE_DIRS.has(name)) continue;
  81. const p = path.join(dir, name);
  82. const st = fs.statSync(p);
  83. if (st.isDirectory()) walk(p, out);
  84. else if (name.endsWith('.vue')) out.push(p);
  85. }
  86. return out;
  87. }
  88. const files = walk(ROOT);
  89. let changedFiles = 0;
  90. let replacements = 0;
  91. const log = [];
  92. for (const abs of files) {
  93. const src = fs.readFileSync(abs, 'utf8');
  94. const styleIdx = src.search(/<style/);
  95. if (styleIdx === -1) continue;
  96. let head = src.slice(0, styleIdx);
  97. const tail = src.slice(styleIdx);
  98. let count = 0;
  99. for (const [hex, key] of Object.entries(MAP)) {
  100. const re = new RegExp("(['\"])" + hex + "\\1", 'g');
  101. head = head.replace(re, () => {
  102. count++;
  103. return 'BrandColors.' + key;
  104. });
  105. }
  106. if (count === 0) continue;
  107. // 追加 import:已有 brand-colors 具名导入则并入 BrandColors,否则新增一行
  108. if (!/\bBrandColors\b[^}]*\}\s*from\s*['"][^'"]*brand-colors/.test(head)) {
  109. const existing = head.match(/import\s*\{([^}]*)\}\s*from\s*(['"][^'"]*brand-colors(?:\.js)?['"])/);
  110. if (existing) {
  111. head = head.replace(existing[0], 'import {' + existing[1].replace(/\s*$/, '') + (existing[1].trim().endsWith(',') ? ' ' : ', ') + 'BrandColors } from ' + existing[2]);
  112. } else {
  113. const rel = path.relative(path.dirname(abs), path.join(ROOT, 'utils/brand-colors.js')).split(path.sep).join('/');
  114. const importLine = "import { BrandColors } from '" + (rel.startsWith('.') ? rel : './' + rel) + "'";
  115. const m = head.match(/<script[^>]*>\r?\n/);
  116. if (!m) {
  117. console.warn('SKIP(no script tag): ' + path.relative(ROOT, abs));
  118. continue;
  119. }
  120. head = head.replace(m[0], m[0] + importLine + (m[0].includes('\r\n') ? '\r\n' : '\n'));
  121. }
  122. }
  123. if (!DRY) fs.writeFileSync(abs, head + tail, 'utf8');
  124. changedFiles++;
  125. replacements += count;
  126. log.push(String(count).padStart(3) + ' ' + path.relative(ROOT, abs).split(path.sep).join('/'));
  127. }
  128. log.sort((a, b) => parseInt(b) - parseInt(a));
  129. console.log((DRY ? '[DRY] ' : '') + '文件: ' + changedFiles + ' 替换: ' + replacements);
  130. log.slice(0, 40).forEach(l => console.log(l));
  131. if (log.length > 40) console.log('... 共 ' + log.length + ' 个文件');