d37-template-color-bind.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #!/usr/bin/env node
  2. /**
  3. * D37: 模板区组件属性色值(switch color / slider activeColor / 图表 fillColor 等)
  4. * 绑定到 BrandColors,消除模板里编译期不可达的硬编码 hex。
  5. *
  6. * 做法:
  7. * 1) 模板属性 `attr=#HEX` → `:attr="<key>Color"`
  8. * 2) script 内补 `import { BrandColors } from '@/utils/brand-colors.js'`(已存在则跳过)
  9. * 3) 在 data() 的 return 对象首部(无 data 则 computed,再否则新建 computed)注入
  10. * `<key>Color: BrandColors.<key>,`
  11. *
  12. * 用法: node scripts/d37-template-color-bind.js [--dry]
  13. */
  14. const fs = require('fs');
  15. const HEXT_TO_KEY = {
  16. '#FF8C42': 'body',
  17. '#F97316': 'primary',
  18. '#10B981': 'success',
  19. '#6366F1': 'wisdom',
  20. '#F59E0B': 'wealth',
  21. '#FF6B9D': 'mind',
  22. };
  23. const FILES = [
  24. 'pages/action-detail/index.vue',
  25. 'pages/body-detail/index.vue',
  26. 'pages/child/rewards.vue',
  27. 'pages/discover/create-circle.vue',
  28. 'pages/discover-detail/product-detail/product-detail.vue',
  29. 'pages/family/add-member.vue',
  30. 'pages/health/components/ComprehensiveSurvey.vue',
  31. 'pages/health/daily-checkin.vue',
  32. 'pages/health/sleep-index.vue',
  33. 'pages/health-main/index.vue',
  34. 'pages/member-detail/components/member-detail-sections/body-section.vue',
  35. 'pages/mind-detail/index.vue',
  36. 'pages/profile/components/ProfileMenu.vue',
  37. 'pages/promotion/index.vue',
  38. 'pages/rewards/rewards.vue',
  39. 'pages/wealth/components/WealthEnergyRing.vue',
  40. 'pages/wealth/index.vue',
  41. 'pages/wisdom-detail/index.vue',
  42. ];
  43. const DRY = process.argv.includes('--dry');
  44. const IMPORT_LINE = "import { BrandColors } from '@/utils/brand-colors.js'";
  45. let totalBind = 0;
  46. for (const f of FILES) {
  47. let src = fs.readFileSync(f, 'utf8');
  48. const eol = src.includes('\r\n') ? '\r\n' : '\n';
  49. // 只在模板区(第一个行首 <script 之前)替换属性色值
  50. const scriptStart = src.search(/^<script/m);
  51. if (scriptStart === -1) { console.log('SKIP(no script): ' + f); continue; }
  52. let head = src.slice(0, scriptStart);
  53. const tail = src.slice(scriptStart);
  54. const keys = new Set();
  55. let changes = 0;
  56. head = head.replace(/([A-Za-z-]+)=(#[0-9a-fA-F]{3,8})\b/g, (m, attr, hex) => {
  57. const key = HEXT_TO_KEY[hex.toUpperCase()];
  58. if (!key) return m;
  59. changes++;
  60. // SVG 内部属性(<stop stop-color>):小程序对内联 SVG 支持有限,
  61. // 不做响应式绑定,仅补引号保持合法字面量
  62. if (attr === 'stop-color') return attr + '="' + hex + '"';
  63. keys.add(key);
  64. totalBind++;
  65. return ':' + attr + '="' + key + 'Color"';
  66. });
  67. if (!changes) { console.log('SKIP(no binding): ' + f); continue; }
  68. let body = tail;
  69. // 1) import(仅有响应式绑定才需要引入)
  70. if (keys.size && !/brand-colors/.test(body)) {
  71. body = body.replace(/^<script[^>]*>\r?\n/, m => m + IMPORT_LINE + eol);
  72. }
  73. // 2) 注入 data/computed 属性
  74. let injected = false;
  75. const dataPatterns = [
  76. /^([ \t]*)data\s*(?:\([^)]*\)|:\s*function\s*\([^)]*\))\s*\{\r?\n([ \t]*)return\s*\{\r?\n/m,
  77. /^([ \t]*)data\s*:\s*\(\)\s*=>\s*\(\{\r?\n([ \t]*)/m,
  78. ];
  79. for (const re of dataPatterns) {
  80. const m = body.match(re);
  81. if (!m) continue;
  82. const idx = body.indexOf(m[0]) + m[0].length;
  83. const indent = m[2] + ' ';
  84. const block = [...keys].map(k => indent + k + 'Color: BrandColors.' + k + ',').join(eol) + eol;
  85. body = body.slice(0, idx) + block + body.slice(idx);
  86. injected = true;
  87. break;
  88. }
  89. if (!injected && keys.size) {
  90. const computedMatch = body.match(/^([ \t]*)computed\s*:\s*\{\r?\n([ \t]*)/m);
  91. if (computedMatch) {
  92. const idx = body.indexOf(computedMatch[0]) + computedMatch[0].length;
  93. const indent = computedMatch[2] || (computedMatch[1] + ' ');
  94. const block = [...keys].map(k => indent + k + 'Color() { return BrandColors.' + k + ' },').join(eol) + eol;
  95. // computedMatch 已吞掉下一行缩进,需补回,否则首个既有属性会丢缩进
  96. body = body.slice(0, idx) + block + indent + body.slice(idx);
  97. injected = true;
  98. }
  99. }
  100. if (!injected && keys.size) {
  101. const exp = body.match(/^([ \t]*)export\s+default\s*\{\r?\n/m);
  102. if (exp) {
  103. const idx = body.indexOf(exp[0]) + exp[0].length;
  104. const indent = exp[1] + ' ';
  105. const block = indent + 'computed: {' + eol +
  106. [...keys].map(k => indent + ' ' + k + 'Color() { return BrandColors.' + k + ' },').join(eol) + eol +
  107. indent + '},' + eol;
  108. body = body.slice(0, idx) + block + body.slice(idx);
  109. injected = true;
  110. }
  111. }
  112. if (!injected && keys.size) { console.log('FAIL(inject): ' + f); continue; }
  113. const out = head + body;
  114. if (!DRY) fs.writeFileSync(f, out, 'utf8');
  115. console.log((DRY ? 'DRY ' : 'OK ') + f + ' bindings=' + keys.size);
  116. }
  117. console.log('模板属性绑定总数: ' + totalBind);