d41-template-hex-bind.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env node
  2. /**
  3. * D41: 模板区剩余硬编码色收编
  4. *
  5. * 覆盖三类模板硬编码:
  6. * A) 组件属性色值(RadarChart gridColor/labelColor、progress backgroundColor、switch color)
  7. * → 绑 data/computed(BrandColors)
  8. * B) 模板表达式字符串拼接里的 hex(';color:#fff'、conic-gradient(#F97316 …))
  9. * → 拼接 BrandColors 值(data 属性)
  10. * C) placeholder-style / 兜底色字面量 → 绑定 data 字符串
  11. *
  12. * 用法: node scripts/d41-template-hex-bind.js [--dry]
  13. */
  14. const fs = require('fs');
  15. const path = require('path');
  16. const ROOT = path.resolve(__dirname, '..');
  17. const DRY = process.argv.includes('--dry');
  18. const IMPORT_LINE = "import { BrandColors } from '@/utils/brand-colors.js'";
  19. /**
  20. * data: { 属性名: BrandColors 键 } —— 值以 BrandColors.<键> 注入
  21. * raw: { 属性名: JS 表达式字符串 } —— 原样注入(用于拼接字符串)
  22. * replace: [[旧文本, 新文本], ...] —— 模板区精确替换(全局)
  23. */
  24. const JOBS = [
  25. {
  26. file: 'pages/action-detail/index.vue',
  27. data: { gridColor: 'emerald100', labelColor: 'emerald800' },
  28. replace: [
  29. ['gridColor="#D1FAE5"', ':gridColor="gridColor"'],
  30. ['labelColor="#065F46"', ':labelColor="labelColor"'],
  31. ],
  32. },
  33. {
  34. file: 'pages/body-detail/index.vue',
  35. data: { gridColor: 'orange200', labelColor: 'orange800' },
  36. replace: [
  37. ['gridColor="#FED7AA"', ':gridColor="gridColor"'],
  38. ['labelColor="#9A3412"', ':labelColor="labelColor"'],
  39. ],
  40. },
  41. {
  42. file: 'pages/member-detail/components/member-detail-sections/body-section.vue',
  43. data: { gridColor: 'orange200', labelColor: 'orange800' },
  44. replace: [
  45. ['gridColor="#FED7AA"', ':gridColor="gridColor"'],
  46. ['labelColor="#9A3412"', ':labelColor="labelColor"'],
  47. ],
  48. },
  49. {
  50. file: 'pages/mind-detail/index.vue',
  51. data: { gridColor: 'pink100', labelColor: 'pink800' },
  52. replace: [
  53. ['gridColor="#FCE7F3"', ':gridColor="gridColor"'],
  54. ['labelColor="#9D174D"', ':labelColor="labelColor"'],
  55. ],
  56. },
  57. {
  58. file: 'pages/wisdom-detail/index.vue',
  59. data: { gridColor: 'indigo100', labelColor: 'indigo800' },
  60. replace: [
  61. ['gridColor="#E0E7FF"', ':gridColor="gridColor"'],
  62. ['labelColor="#3730A3"', ':labelColor="labelColor"'],
  63. ],
  64. },
  65. {
  66. file: 'pages/article-center/article-edit.vue',
  67. data: { whiteColor: 'white' },
  68. replace: [["';color:#fff;border-color:'", "';color:' + whiteColor + ';border-color:'"]],
  69. },
  70. {
  71. file: 'pages/article-center/index.vue',
  72. data: { whiteColor: 'white' },
  73. replace: [["';color:#fff;border-color:'", "';color:' + whiteColor + ';border-color:'"]],
  74. },
  75. {
  76. file: 'pages/mind-detail/articles.vue',
  77. data: { whiteColor: 'white' },
  78. replace: [["';color:#fff'", "';color:' + whiteColor"]],
  79. },
  80. {
  81. file: 'pages/growth/challenge-progress.vue',
  82. data: { ringColor: 'primary', ringTrack: 'fill' },
  83. replace: [
  84. ["'conic-gradient(#F97316 ' + progressPercent + '%, #f0f0f0 ' + progressPercent + '%)'",
  85. "'conic-gradient(' + ringColor + ' ' + progressPercent + '%, ' + ringTrack + ' ' + progressPercent + '%)'"],
  86. ],
  87. },
  88. {
  89. file: 'pages/activity/create.vue',
  90. data: { paleColor: 'primaryPale' },
  91. replace: [["tpl.lightColor || '#FFF7ED'", 'tpl.lightColor || paleColor']],
  92. },
  93. {
  94. file: 'pages/child/rewards.vue',
  95. data: { trackColor: 'hairline2' },
  96. replace: [['backgroundColor="#eee"', ':backgroundColor="trackColor"']],
  97. },
  98. {
  99. file: 'pages/rewards/rewards.vue',
  100. data: { trackColor: 'hairline2' },
  101. replace: [['backgroundColor="#eee"', ':backgroundColor="trackColor"']],
  102. },
  103. {
  104. file: 'pages/shop/consignee-edit/consignee-edit.vue',
  105. data: { switchColor: 'indigoPeriwinkle' },
  106. replace: [['color="#667eea"', ':color="switchColor"']],
  107. },
  108. {
  109. file: 'pages/shop/address-edit/address-edit.vue',
  110. raw: { placeholderStyle: "'color:' + BrandColors.textGhost + '; font-size: 28rpx;'" },
  111. replace: [['placeholder-style="color: #ccc; font-size: 28rpx;"', ':placeholder-style="placeholderStyle"']],
  112. },
  113. {
  114. file: 'pages/shop/checkout/checkout.vue',
  115. raw: { placeholderStyle: "'color:' + BrandColors.textGhost + '; font-size: 26rpx;'" },
  116. replace: [['placeholder-style="color: #ccc; font-size: 26rpx;"', ':placeholder-style="placeholderStyle"']],
  117. },
  118. {
  119. file: 'pages/diet/index.vue',
  120. raw: { placeholderStyle: "'color:' + BrandColors.textHint" },
  121. replace: [['placeholder-style="color:#bbb"', ':placeholder-style="placeholderStyle"']],
  122. },
  123. ];
  124. const stats = [];
  125. for (const job of JOBS) {
  126. const abs = path.join(ROOT, job.file);
  127. if (!fs.existsSync(abs)) { console.log('SKIP(not found): ' + job.file); continue; }
  128. let src = fs.readFileSync(abs, 'utf8');
  129. const eol = src.includes('\r\n') ? '\r\n' : '\n';
  130. const scriptStart = src.search(/^<script/m);
  131. if (scriptStart === -1) { console.log('SKIP(no script): ' + job.file); continue; }
  132. let head = src.slice(0, scriptStart);
  133. let body = src.slice(scriptStart);
  134. let replaced = 0;
  135. for (const [from, to] of job.replace) {
  136. if (!head.includes(from)) { console.log(' !! 未命中: ' + job.file + ' ' + from.slice(0, 60)); continue; }
  137. head = head.split(from).join(to);
  138. replaced += (head.split(to).length - 1) > 0 ? 1 : 1;
  139. }
  140. // import
  141. if (!/brand-colors/.test(body)) {
  142. body = body.replace(/^<script[^>]*>\r?\n/, m => m + IMPORT_LINE + eol);
  143. }
  144. // data 注入
  145. const entries = [];
  146. for (const [name, key] of Object.entries(job.data || {})) entries.push(name + ': BrandColors.' + key + ',');
  147. for (const [name, expr] of Object.entries(job.raw || {})) entries.push(name + ': ' + expr + ',');
  148. const dataPatterns = [
  149. /^([ \t]*)data\s*(?:\([^)]*\)|:\s*function\s*\([^)]*\))\s*\{\r?\n([ \t]*)return\s*\{\r?\n/m,
  150. /^([ \t]*)data\s*:\s*\(\)\s*=>\s*\(\{\r?\n([ \t]*)/m,
  151. ];
  152. let injected = false;
  153. for (const re of dataPatterns) {
  154. const m = body.match(re);
  155. if (!m) continue;
  156. const idx = body.indexOf(m[0]) + m[0].length;
  157. const indent = m[2] + ' ';
  158. const block = entries.map(e => indent + e).join(eol) + eol;
  159. body = body.slice(0, idx) + block + body.slice(idx);
  160. injected = true;
  161. break;
  162. }
  163. if (!injected) { console.log('FAIL(no data): ' + job.file); continue; }
  164. if (!DRY) fs.writeFileSync(abs, head + body, 'utf8');
  165. stats.push(job.file);
  166. console.log((DRY ? 'DRY ' : 'OK ') + job.file + ' 属性=' + entries.length);
  167. }
  168. console.log('处理文件: ' + stats.length);