// 甄房换肤 · WEB 业务层替换器 // 用法: node _design-test/apply-vue-colors.js (预演,报告每条规则的命中数) // node _design-test/apply-vue-colors.js --write (真写) // // 为什么不能只做单色 1:1 替换: // 1) ECharts 的 10 色分类色板若逐色映射,会与已有的 #3b82f6 / #ef4444 撞色,序列失去区分度 // 2) 同一个渐变串 linear-gradient(135deg,#f08060,#d9481c) 同时用于「房源等级 A」和「客源热度 H」, // 但 A 现在要改成品牌金、H 仍是红 —— 必须按选择器区分 // 所以规则分两类:先按「选择器/整串」精确处理,再对剩余单色做通用映射。 const fs = require('fs'); const path = require('path'); const WRITE = process.argv.includes('--write'); // 两个前端:WEB(ruoyi-ui)与 H5(zhenfang-uni) const ROOTS = [ { root: 'e:/KF_7/ZhenOS-ZF/ruoyi-ui/src', skip: new Set(['assets/styles']) }, { root: 'e:/KF_7/ZhenOS-ZF/zhenfang-uni/src', skip: new Set() } ]; const EXT = new Set(['.vue', '.js', '.ts', '.html']); // ── 新的 10 色分类色板(Fiori 官方 Category/Accent 色 + 品牌金,色相拉开 + 明度错位)── const PALETTE10 = ['#0057D2', '#E76500', '#188918', '#DF1278', '#07838F', '#D27700', '#7858FF', '#D20A0A', '#5B738B', '#C6A15B']; const OLD10 = ['#ff7a1a', '#2e9e5b', '#ffb31a', '#f08060', '#0F766E', '#756a5e', '#7c3aed', '#14B8A6', '#ef4444', '#3b82f6']; const OLD6 = ['#ff7a1a', '#2e9e5b', '#ffb31a', '#f08060', '#0F766E', '#756a5e']; const arr = a => '[' + a.map(c => "'" + c + "'").join(', ') + ']'; // ── 规则(按顺序执行)── const RULES = [ // 1. 图表 10 色分类色板(4 处) ['图表色板 · 10 色', arr(OLD10).replace(/'/g, "'"), arr(PALETTE10), 'literal'], // 2. 图表 6 色(1 处) ['图表色板 · 6 色', arr(OLD6).replace(/'/g, "'"), arr(PALETTE10.slice(0, 6)), 'literal'], // 3. 房源等级 A → 品牌金 ['等级 A 渐变 → 品牌金', /(&\.level-A\s*\{[^}]*?linear-gradient\()135deg,\s*#f08060,\s*#d9481c/g, '$1135deg, #E3CB99, #C6A15B', 'regex'], // 4. 客源热度 H(热)→ 保留红系 ['热度 H 渐变 → 红', /(&\.level-H\s*\{[^}]*?linear-gradient\()135deg,\s*#f08060,\s*#d9481c/g, '$1135deg, #E06B6B, #AA0808', 'regex'], // 5. 等级 B → 警告橙 ['等级 B 渐变 → 橙', /(&\.level-B\s*\{[^}]*?linear-gradient\()135deg,\s*#FBBF24,\s*#ffb31a/g, '$1135deg, #D27700, #E76500', 'regex'], // 6. 等级 C → 中性 ['等级 C 渐变 → 中性', /(&\.level-C\s*\{[^}]*?linear-gradient\()135deg,\s*#9a8e80,\s*#756a5e/g, '$1135deg, #A9B4BE, #788FA6', 'regex'], // 7. 统计卡 warning 渐变 ['warning 渐变', /linear-gradient\((90|135)deg,\s*#ffb31a,\s*#FBBF24\)/g, 'linear-gradient($1deg, #E76500, #D27700)', 'regex'], // 8. 统计卡 danger 渐变 ['danger 渐变', /linear-gradient\((90|135)deg,\s*#f08060,\s*#ef4444\)/g, 'linear-gradient($1deg, #E76500, #D20A0A)', 'regex'], // 9. el-rate 星级(单一琥珀色 ×3) ['星级 · 单色', "['#ffb31a', '#ffb31a', '#ffb31a']", "['#D27700', '#D27700', '#D27700']", 'literal'], ['星级 · 同色系', "['#2e9e5b', '#2e9e5b', '#2e9e5b']", "['#188918', '#188918', '#188918']", 'literal'], // 10. el-rate 五档评价(差→极佳) ['评价五档', "['#d9481c', '#ffb31a', '#FBBF24', '#ff7a1a', '#2e9e5b']", "['#D20A0A', '#E76500', '#D27700', '#C6A15B', '#188918']", 'literal'], // 11. ThemePicker 预置色 ['主题预置色', "['#ff7a1a', '#ff8a3d', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]", "['#0070F2', '#4D9BF6', '#0C2340','#131E29','#188918', '#07838F', '#7858FF', '#D20A0A', ]", 'literal'], // 12. rgb()/rgba() 三元组(保留 rgb/rgba 与后面的 alpha) ['rgb 三元组 · 橙', /(rgba?)\(\s*255,\s*122,\s*26\b/g, '$1(0, 112, 242', 'regex'], ['rgb 三元组 · 浅橙', /(rgba?)\(\s*255,\s*138,\s*61\b/g, '$1(77, 155, 246', 'regex'], ['rgb 三元组 · 琥珀', /(rgba?)\(\s*255,\s*179,\s*26\b/g, '$1(231, 101, 0', 'regex'], ['rgb 三元组 · 亮琥珀', /(rgba?)\(\s*251,\s*191,\s*36\b/g, '$1(210, 119, 0', 'regex'], ['rgb 三元组 · 暖墨', /(rgba?)\(\s*42,\s*33,\s*24\b/g, '$1(19, 30, 41', 'regex'] ]; // ── 通用单色映射(处理剩余的所有字面量)── const GENERIC = [ ['#ff7a1a', '#0070F2'], ['#e86a0c', '#0064D9'], ['#d96408', '#0057D2'], ['#ff8a3d', '#4D9BF6'], ['#ffc48a', '#A6CFFF'], ['#ffe8d1', '#EBF8FF'], ['#fff6ec', '#F0F6FE'], ['#f6ead8', '#EAECEE'], ['#faf7f2', '#F5F6F7'], ['#fbf5ea', '#F5F6F7'], ['#e9d3b0', '#758CA4'], ['#efe7dc', '#E5E5E5'], ['#2a2118', '#131E29'], ['#3a322a', '#556B82'], ['#756a5e', '#5F7590'], ['#9a8e80', '#5F7590'], ['#8a7f73', '#758CA4'], ['#b8ac9e', '#A9B4BE'], ['#c9beb0', '#BBC1CA'], ['#ffb31a', '#E76500'], ['#fff6e0', '#FDF1E8'], ['#d9481c', '#AA0808'], ['#fdeee8', '#FBF0F0'], ['#2e9e5b', '#256F3A'], ['#ecf7f0', '#F1F7F3'], ['#1f8a5b', '#256F3A'], ['#ea580c', '#B44F00'], ['#f08060', '#D98A8A'], ['#fbbf24', '#D27700'], ['#2f2820', '#0A1D35'], ['#4a4036', '#1E2A36'], ['#d3dce6', '#223040'], ['#99a9bf', '#6B7F94'], // 图表/其它残留的 Tailwind 色 ['#ef4444', '#D20A0A'], ['#3b82f6', '#0057D2'], ['#0f766e', '#07838F'], ['#14b8a6', '#07838F'], ['#7c3aed', '#7858FF'], ['#f5222d', '#D20A0A'], // H5 专有 ['#36cfc9', '#4DA37A'], ['#ff9a3d', '#4D9BF6'], ['#10b981', '#188918'] ]; const files = []; for (const { root, skip } of ROOTS) { (function walk(d) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); const rel = path.relative(root, p).replace(/\\/g, '/'); if (e.isDirectory()) { if (!skip.has(rel)) walk(p); continue; } if (EXT.has(path.extname(e.name).toLowerCase())) files.push(p); } })(root); } const tally = new Map(); let changedFiles = 0; for (const f of files) { let txt = fs.readFileSync(f, 'utf8'); const orig = txt; for (const [label, pat, rep, kind] of RULES) { const n = kind === 'literal' ? (txt.split(pat).length - 1) : (txt.match(pat) || []).length; if (!n) continue; txt = kind === 'literal' ? txt.split(pat).join(rep) : txt.replace(pat, rep); tally.set(label, (tally.get(label) || 0) + n); } for (const [from, to] of GENERIC) { const n = (txt.match(new RegExp(from, 'gi')) || []).length; if (!n) continue; txt = txt.replace(new RegExp(from, 'gi'), to); const k = '单色 ' + from + ' -> ' + to; tally.set(k, (tally.get(k) || 0) + n); } if (txt !== orig) { changedFiles++; if (WRITE) fs.writeFileSync(f, txt); } } console.log('=== 规则命中统计 ==='); [...tally.entries()].sort((a, b) => b[1] - a[1]).forEach(([k, v]) => { console.log(' ' + String(v).padStart(5) + ' ' + k); }); const total = [...tally.values()].reduce((a, b) => a + b, 0); console.log('\n涉及文件 ' + changedFiles + ' 个 / 替换合计 ' + total + ' 处'); console.log(WRITE ? '已写入' : '预演,未写入(加 --write 生效)');