|
|
@@ -0,0 +1,233 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+/**
|
|
|
+ * C 批:JS 色板收编
|
|
|
+ * 将 <script> 块内的硬编码 hex 颜色字符串替换为 BrandColors.* 导入引用。
|
|
|
+ * 仅在目标文件顶部添加 import 语句。
|
|
|
+ *
|
|
|
+ * 替换规则(color-literal 匹配,仅匹配单引号或双引号包裹的 hex):
|
|
|
+ * '#F97316' / "#F97316" → '$brand.primary'
|
|
|
+ * '#FB923C' / "#FB923C" → '$brand.primaryLight'
|
|
|
+ * ...
|
|
|
+ *
|
|
|
+ * 用法:
|
|
|
+ * node scripts/js-color-hoist.js # 全部替换(推荐,但谨慎)
|
|
|
+ * node scripts/js-color-hoist.js --dry-run # 仅预览不写入
|
|
|
+ * node scripts/js-color-hoist.js --file path # 只处理指定文件
|
|
|
+ */
|
|
|
+const fs = require('fs')
|
|
|
+const path = require('path')
|
|
|
+
|
|
|
+const ROOT = path.resolve(__dirname, '..')
|
|
|
+const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'docs'])
|
|
|
+
|
|
|
+// 替换规则(key=hex literal value, value=BrandColors.*)
|
|
|
+const JS_RULES = [
|
|
|
+ { hex: '#F97316', token: 'BrandColors.primary' },
|
|
|
+ { hex: '#fb923c', token: 'BrandColors.primaryLight' },
|
|
|
+ { hex: '#FB923C', token: 'BrandColors.primaryLight' },
|
|
|
+ { hex: '#fff7ed', token: 'BrandColors.primaryPale' },
|
|
|
+ { hex: '#FFF7ED', token: 'BrandColors.primaryPale' },
|
|
|
+ { hex: '#ff8c42', token: 'BrandColors.body' },
|
|
|
+ { hex: '#FF8C42', token: 'BrandColors.body' },
|
|
|
+ { hex: '#10b981', token: 'BrandColors.success' },
|
|
|
+ { hex: '#10B981', token: 'BrandColors.success' },
|
|
|
+ { hex: '#ef4444', token: 'BrandColors.danger' },
|
|
|
+ { hex: '#EF4444', token: 'BrandColors.danger' },
|
|
|
+ { hex: '#6366f1', token: 'BrandColors.wisdom' },
|
|
|
+ { hex: '#6366F1', token: 'BrandColors.wisdom' },
|
|
|
+ { hex: '#c62828', token: 'BrandColors.dangerDeep' },
|
|
|
+ { hex: '#C62828', token: 'BrandColors.dangerDeep' },
|
|
|
+ { hex: '#333333', token: 'BrandColors.textStrong' },
|
|
|
+ { hex: '#333', token: 'BrandColors.textStrong' },
|
|
|
+ { hex: '#666666', token: 'BrandColors.textBody' },
|
|
|
+ { hex: '#666', token: 'BrandColors.textBody' },
|
|
|
+ { hex: '#999999', token: 'BrandColors.textMuted' },
|
|
|
+ { hex: '#999', token: 'BrandColors.textMuted' },
|
|
|
+ { hex: '#fafafa', token: 'BrandColors.fillGhost' },
|
|
|
+ { hex: '#FAFAFA', token: 'BrandColors.fillGhost' },
|
|
|
+ { hex: '#f9fafb', token: 'BrandColors.fillGray50' },
|
|
|
+ { hex: '#F9FAFB', token: 'BrandColors.fillGray50' },
|
|
|
+ { hex: '#ffffff', token: 'BrandColors.white' },
|
|
|
+ { hex: '#FFFFFF', token: 'BrandColors.white' },
|
|
|
+ { hex: '#ffffff', token: 'BrandColors.white' },
|
|
|
+].sort((a, b) => b.hex.length - a.hex.length)
|
|
|
+
|
|
|
+// 优先文件列表(按硬编码色数排序)
|
|
|
+const TOP_FILES = [
|
|
|
+ 'components/ContentSharePoster.vue',
|
|
|
+ 'pages/mind-detail/emotion-checkin.vue',
|
|
|
+ 'pages/action-detail/components/PearlDiagram.vue',
|
|
|
+ 'pages/energy/index.vue',
|
|
|
+ 'components/ArticleBookshelf.vue',
|
|
|
+ 'pages/home-pages/parent-index.vue',
|
|
|
+ 'pages/mind-detail/components/FamilyTianpanCard.vue',
|
|
|
+ 'pages/promotion/components/SharePoster.vue',
|
|
|
+ 'components/PageBanner.vue',
|
|
|
+ 'pages/tianpan/index.vue',
|
|
|
+]
|
|
|
+
|
|
|
+function walk(dir, out = []) {
|
|
|
+ for (const name of fs.readdirSync(dir)) {
|
|
|
+ if (EXCLUDE_DIRS.has(name)) continue
|
|
|
+ const p = path.join(dir, name)
|
|
|
+ const st = fs.statSync(p)
|
|
|
+ if (st.isDirectory()) walk(p, out)
|
|
|
+ else if (name.endsWith('.vue')) out.push(p)
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+function hexToToken(hex) {
|
|
|
+ const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
+ const found = JS_RULES.find(r => r.hex.toLowerCase() === hex.toLowerCase())
|
|
|
+ return found ? found.token : null
|
|
|
+}
|
|
|
+
|
|
|
+function replaceJsHex(text, dryRun = false) {
|
|
|
+ // 匹配 <script> 块
|
|
|
+ const SCRIPT_RE = /(<script)([^>]*)>([\s\S]*?)(<\/script>)/gi
|
|
|
+ let result = text
|
|
|
+ let changed = false
|
|
|
+ let totalRepl = 0
|
|
|
+ let needsImport = false
|
|
|
+ let importAdded = false
|
|
|
+
|
|
|
+ const matches = [...text.matchAll(SCRIPT_RE)]
|
|
|
+ for (const match of matches) {
|
|
|
+ const body = match[3]
|
|
|
+ let newBody = body
|
|
|
+ let blockRepl = 0
|
|
|
+ for (const rule of JS_RULES) {
|
|
|
+ const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
+ // 匹配单引号或双引号包裹的 hex,且前后不是字母数字
|
|
|
+ const re = new RegExp(`(['"])${escaped}(?![0-9a-fA-F])\\1`, 'g')
|
|
|
+ const m = newBody.match(re)
|
|
|
+ if (m) {
|
|
|
+ blockRepl += m.length
|
|
|
+ newBody = newBody.replace(re, `$1${rule.token}$2`) // careful: preserve quote style
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (blockRepl > 0) {
|
|
|
+ totalRepl += blockRepl
|
|
|
+ changed = true
|
|
|
+ needsImport = true
|
|
|
+ // 替换 <script> 块
|
|
|
+ const before = result.slice(0, match.index)
|
|
|
+ const after = result.slice(SCRIPT_RE.lastIndex)
|
|
|
+ // 找插入 import 的位置(在 </script> 后面,<template> 前面)
|
|
|
+ const templateIdx = result.indexOf('<template>')
|
|
|
+ if (templateIdx !== -1 && !importAdded) {
|
|
|
+ const importLine = `\nimport { ${[...new Set(JS_RULES.map(r => r.token.split('.')[1]))]
|
|
|
+ .sort().join(', ')} } from '@/utils/brand-colors.js'\n`
|
|
|
+ result = before + importLine + result.slice(match.index)
|
|
|
+ importAdded = true
|
|
|
+ }
|
|
|
+ result = result.slice(0, match.index) +
|
|
|
+ match[1] + match[2] + '>' + newBody + '</script>' +
|
|
|
+ result.slice(SCRIPT_RE.lastIndex)
|
|
|
+ SCRIPT_RE.lastIndex = result.indexOf('</script>', match.index) + '</script>'.length
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!dryRun && changed) {
|
|
|
+ fs.writeFileSync(result === text ? text : result, text) // bug guard
|
|
|
+ return { changed: true, replacements: totalRepl, needsImport }
|
|
|
+ }
|
|
|
+ return { changed: false, replacements: totalRepl, needsImport }
|
|
|
+}
|
|
|
+
|
|
|
+// 简化版:直接处理整个文件
|
|
|
+function processFileSimple(filePath, dryRun = false) {
|
|
|
+ const src = fs.readFileSync(filePath, 'utf8')
|
|
|
+ let newSrc = src
|
|
|
+ let totalRepl = 0
|
|
|
+
|
|
|
+ // 收集文件中用到的 token
|
|
|
+ const usedTokens = new Set()
|
|
|
+ for (const rule of JS_RULES) {
|
|
|
+ const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
+ const re = new RegExp(`['"]${escaped}(?![0-9a-fA-F])['"]`, 'g')
|
|
|
+ const m = src.match(re)
|
|
|
+ if (m) {
|
|
|
+ totalRepl += m.length
|
|
|
+ usedTokens.add(rule.token.split('.')[1])
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (totalRepl === 0) return { changed: false, replacements: 0, needsImport: false }
|
|
|
+
|
|
|
+ // 替换 script 块内的 hex
|
|
|
+ const SCRIPT_RE = /(<script)([^>]*)>([\s\S]*?)(<\/script>)/gi
|
|
|
+ newSrc = src.replace(SCRIPT_RE, (fullMatch, tagOpen, attrs, body, tagClose) => {
|
|
|
+ let newBody = body
|
|
|
+ for (const rule of JS_RULES) {
|
|
|
+ const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
+ const re = new RegExp(`['"]${escaped}(?![0-9a-fA-F])['"]`, 'g')
|
|
|
+ const m = newBody.match(re)
|
|
|
+ if (m) {
|
|
|
+ newBody = newBody.replace(re, `'${rule.token}'`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return tagOpen + attrs + '>' + newBody + tagClose
|
|
|
+ })
|
|
|
+
|
|
|
+ // 在 </script> 后添加 import(如果还没加过)
|
|
|
+ if (usedTokens.size > 0 && !src.includes('brand-colors')) {
|
|
|
+ const importLine = `\nimport { ${[...usedTokens].sort().join(', ')} } from '@/utils/brand-colors.js'`
|
|
|
+ newSrc = newSrc.replace(/(<\/script>)/, `$1${importLine}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!dryRun && newSrc !== src) {
|
|
|
+ fs.writeFileSync(filePath, newSrc, 'utf8')
|
|
|
+ }
|
|
|
+ return { changed: newSrc !== src, replacements: totalRepl, needsImport: true }
|
|
|
+}
|
|
|
+
|
|
|
+function main() {
|
|
|
+ const dryRun = process.argv.includes('--dry-run')
|
|
|
+ const fileArg = process.argv.find(a => a.startsWith('--file='))?.split('=')[1]
|
|
|
+
|
|
|
+ const targets = fileArg
|
|
|
+ ? [path.join(ROOT, fileArg)]
|
|
|
+ : TOP_FILES.map(f => path.join(ROOT, f))
|
|
|
+
|
|
|
+ // 如果没指定文件,跑全量扫描(所有 .vue)
|
|
|
+ if (!fileArg) {
|
|
|
+ const allFiles = walk(ROOT)
|
|
|
+ // 过滤出已处理的 TOP 文件,加上其余全部
|
|
|
+ const topSet = new Set(TOP_FILES.map(f => path.resolve(ROOT, f)))
|
|
|
+ const remaining = allFiles.filter(f => !topSet.has(f))
|
|
|
+ targets.push(...remaining)
|
|
|
+ }
|
|
|
+
|
|
|
+ let grandTotal = 0
|
|
|
+ const log = []
|
|
|
+
|
|
|
+ for (const filePath of targets) {
|
|
|
+ if (!fs.existsSync(filePath)) {
|
|
|
+ console.warn(`跳过不存在: ${filePath}`)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const rel = path.relative(ROOT, filePath)
|
|
|
+ const result = processFileSimple(filePath, dryRun)
|
|
|
+ if (result.changed || result.replacements > 0) {
|
|
|
+ grandTotal += result.replacements
|
|
|
+ log.push(`${result.replacements.toString().padStart(4)} ${rel}`)
|
|
|
+ if (!dryRun) console.log(`✓ ${rel} (${result.replacements} 处)`)
|
|
|
+ } else {
|
|
|
+ console.log(`- ${rel} (0 处,跳过)`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!dryRun) {
|
|
|
+ console.log(`\n=== JS 色板收编完成 ===`)
|
|
|
+ console.log(`替换总次数: ${grandTotal}`)
|
|
|
+ log.sort((a, b) => parseInt(b.split(' ')[0]) - parseInt(a.split(' ')[0]))
|
|
|
+ console.log('')
|
|
|
+ for (const line of log) console.log(line)
|
|
|
+ } else {
|
|
|
+ console.log('\n[dry-run] 预览替换量:', grandTotal)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+main()
|