migrate-list-table.mjs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /**
  2. * 将带 class="list-table" 的 <el-table> 批量替换为 <ListTable table-key="...">。
  3. * 同文件多表自动追加 -2/-3;不会误匹配 el-table-column。
  4. *
  5. * Usage: node scripts/migrate-list-table.mjs
  6. */
  7. import fs from 'node:fs'
  8. import path from 'node:path'
  9. import { fileURLToPath } from 'node:url'
  10. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  11. const srcRoot = path.resolve(__dirname, '../src')
  12. const viewsRoot = path.resolve(srcRoot, 'views')
  13. const componentsRoot = path.resolve(srcRoot, 'components')
  14. /** 匹配真正的 el-table 开标签,排除 el-table-column / el-table-column-group 等 */
  15. const OPEN_RE = /<el-table(?=[\s/>])/g
  16. const CLOSE_TAG = '</el-table>'
  17. function walk(dir, out = []) {
  18. if (!fs.existsSync(dir)) return out
  19. for (const name of fs.readdirSync(dir)) {
  20. const full = path.join(dir, name)
  21. const st = fs.statSync(full)
  22. if (st.isDirectory()) {
  23. // 跳过 ListTable 自身,避免把内部 el-table 替换掉
  24. if (full.endsWith(`${path.sep}list-table`)) continue
  25. walk(full, out)
  26. } else if (name.endsWith('.vue')) out.push(full)
  27. }
  28. return out
  29. }
  30. function toKebab(s) {
  31. return String(s)
  32. .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
  33. .replace(/[_\s]+/g, '-')
  34. .replace(/-+/g, '-')
  35. .toLowerCase()
  36. }
  37. function baseKeyFromFile(file) {
  38. let rel
  39. if (file.startsWith(viewsRoot + path.sep) || file.startsWith(viewsRoot + '/')) {
  40. rel = path.relative(viewsRoot, file)
  41. } else {
  42. rel = path.relative(componentsRoot, file)
  43. }
  44. const parts = rel.split(path.sep)
  45. const stem = path.basename(file, '.vue')
  46. const dirs = parts.slice(0, -1).map(toKebab)
  47. if (!['index', 'Index'].includes(stem)) dirs.push(toKebab(stem))
  48. return dirs.filter(Boolean).join('-') || toKebab(stem)
  49. }
  50. function hasListTableClass(attrText) {
  51. return /class\s*=\s*(['"`])([^'"`]*\blist-table\b[^'"`]*)\1/.test(attrText)
  52. }
  53. function findOpenTagEnd(content, nameEnd) {
  54. let j = nameEnd
  55. let quote = null
  56. while (j < content.length) {
  57. const ch = content[j]
  58. if (quote) {
  59. if (ch === quote) quote = null
  60. } else if (ch === '"' || ch === "'" || ch === '`') {
  61. quote = ch
  62. } else if (ch === '>') {
  63. return j
  64. }
  65. j++
  66. }
  67. return -1
  68. }
  69. function transform(content, file) {
  70. const baseKey = baseKeyFromFile(file)
  71. let tableIndex = 0
  72. let i = 0
  73. let out = ''
  74. while (i < content.length) {
  75. OPEN_RE.lastIndex = i
  76. const m = OPEN_RE.exec(content)
  77. if (!m) {
  78. out += content.slice(i)
  79. break
  80. }
  81. out += content.slice(i, m.index)
  82. const start = m.index
  83. const nameEnd = start + '<el-table'.length
  84. const openEnd = findOpenTagEnd(content, nameEnd)
  85. if (openEnd < 0) {
  86. out += content.slice(start)
  87. break
  88. }
  89. const attrText = content.slice(nameEnd, openEnd)
  90. const selfClosing = content[openEnd - 1] === '/'
  91. const isList = hasListTableClass(attrText)
  92. if (!isList) {
  93. out += content.slice(start, openEnd + 1)
  94. i = openEnd + 1
  95. continue
  96. }
  97. tableIndex += 1
  98. const key = tableIndex === 1 ? baseKey : `${baseKey}-${tableIndex}`
  99. let newAttrs = attrText
  100. if (!/\btable-key\s*=/.test(newAttrs) && !/\btableKey\s*=/.test(newAttrs)) {
  101. newAttrs = ` table-key="${key}"` + newAttrs
  102. }
  103. if (selfClosing) {
  104. out += `<ListTable${newAttrs.slice(0, -1)} />`
  105. i = openEnd + 1
  106. continue
  107. }
  108. out += `<ListTable${newAttrs}>`
  109. i = openEnd + 1
  110. let depth = 1
  111. while (i < content.length && depth > 0) {
  112. OPEN_RE.lastIndex = i
  113. const nextOpen = OPEN_RE.exec(content)
  114. const nextCloseIdx = content.indexOf(CLOSE_TAG, i)
  115. if (nextCloseIdx < 0) {
  116. out += content.slice(i)
  117. i = content.length
  118. depth = 0
  119. break
  120. }
  121. const openIdx = nextOpen ? nextOpen.index : -1
  122. if (openIdx >= 0 && openIdx < nextCloseIdx) {
  123. // 嵌套 el-table:原样拷贝开标签,depth++
  124. out += content.slice(i, openIdx)
  125. const nestedNameEnd = openIdx + '<el-table'.length
  126. const nestedOpenEnd = findOpenTagEnd(content, nestedNameEnd)
  127. if (nestedOpenEnd < 0) {
  128. out += content.slice(openIdx)
  129. i = content.length
  130. depth = 0
  131. break
  132. }
  133. out += content.slice(openIdx, nestedOpenEnd + 1)
  134. i = nestedOpenEnd + 1
  135. if (content[nestedOpenEnd - 1] !== '/') depth += 1
  136. continue
  137. }
  138. // 关闭标签
  139. out += content.slice(i, nextCloseIdx)
  140. depth -= 1
  141. if (depth === 0) {
  142. out += '</ListTable>'
  143. } else {
  144. out += CLOSE_TAG
  145. }
  146. i = nextCloseIdx + CLOSE_TAG.length
  147. }
  148. }
  149. return { content: out, count: tableIndex }
  150. }
  151. const files = [...walk(viewsRoot), ...walk(path.join(componentsRoot, 'business'))]
  152. let total = 0
  153. const changed = []
  154. for (const file of files) {
  155. const raw = fs.readFileSync(file, 'utf8')
  156. if (!raw.includes('list-table') || !raw.includes('<el-table')) continue
  157. const { content, count } = transform(raw, file)
  158. if (count > 0 && content !== raw) {
  159. fs.writeFileSync(file, content)
  160. total += count
  161. changed.push(`${path.relative(path.resolve(__dirname, '..'), file)} (${count})`)
  162. }
  163. }
  164. console.log(`Migrated ${total} tables in ${changed.length} files:`)
  165. for (const line of changed) console.log(' -', line)