apply-list-page-layout.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /**
  2. * 批量为列表页应用统一布局 class,并将搜索/操作按钮移入查询表单。
  3. * Usage:
  4. * node scripts/apply-list-page-layout.js
  5. * node scripts/apply-list-page-layout.js ../saas-companyui/src/views
  6. */
  7. const fs = require('fs')
  8. const path = require('path')
  9. const defaultViewsDir = path.join(__dirname, '../src/views')
  10. const viewsDir = process.argv[2] ? path.resolve(process.argv[2]) : defaultViewsDir
  11. const SKIP_FILES = new Set([
  12. 'hisStore/storeOrder/index.vue',
  13. 'system/user/index.vue',
  14. 'company/companyUser/index.vue',
  15. 'company/companyUser/indexOld.vue',
  16. 'company/companyUser/companyUserList.vue',
  17. 'addressBook/all.vue'
  18. ])
  19. const WIDTH_STYLE_RE = /\sstyle="width:\s*\d+(?:\.\d+)?px"/gi
  20. function walk(dir, files = []) {
  21. for (const name of fs.readdirSync(dir)) {
  22. const full = path.join(dir, name)
  23. if (fs.statSync(full).isDirectory()) {
  24. walk(full, files)
  25. } else if (name.endsWith('.vue')) {
  26. files.push(full)
  27. }
  28. }
  29. return files
  30. }
  31. function rel(file) {
  32. return path.relative(viewsDir, file).replace(/\\/g, '/')
  33. }
  34. function addClassToQueryForm(content) {
  35. return content.replace(
  36. /(<el-form\b(?=[^>]*ref="queryForm")[^>]*)(>)/,
  37. (match, start, end) => {
  38. if (/class="[^"]*list-search-form/.test(start)) {
  39. return match
  40. }
  41. if (/class="/.test(start)) {
  42. return start.replace(/class="([^"]*)"/, 'class="$1 list-search-form"') + end
  43. }
  44. return `${start} class="list-search-form"${end}`
  45. }
  46. )
  47. }
  48. function addListPageAlertClass(content) {
  49. const idx = content.indexOf('app-container')
  50. if (idx < 0) return content
  51. const slice = content.slice(idx)
  52. const re = /<el-alert\b(?![^>]*list-page-alert)([^>]*?)(\/?>)/i
  53. const match = slice.match(re)
  54. if (!match) return content
  55. const start = idx + slice.indexOf(match[0])
  56. const end = start + match[0].length
  57. let tagStart = `<el-alert${match[1]}`
  58. if (/class="/.test(tagStart)) {
  59. tagStart = tagStart.replace(/class="([^"]*)"/, 'class="$1 list-page-alert"')
  60. } else {
  61. tagStart += ' class="list-page-alert"'
  62. }
  63. const updated = `${tagStart}${match[2]}`
  64. return content.slice(0, start) + updated + content.slice(end)
  65. }
  66. function addListToolbarClass(content) {
  67. return content.replace(
  68. /(<el-row\b[^>]*class=")([^"]*mb8[^"]*)(")/g,
  69. (match, p1, classes, p3) => {
  70. if (classes.includes('list-toolbar') || classes.includes('store-order-toolbar') || classes.includes('list-stats-row')) {
  71. return match
  72. }
  73. return `${p1}${classes} list-toolbar${p3}`
  74. }
  75. )
  76. }
  77. function normalizeStatsRows(content) {
  78. return content.replace(
  79. /<el-row\b([^>]*)>([\s\S]*?)<\/el-row>/g,
  80. (row, attrs, inner) => {
  81. if (!/stat-card|stat-value|stat-label/.test(inner) || !/<el-card/.test(inner)) {
  82. return row
  83. }
  84. let nextAttrs = attrs
  85. .replace(/\s:gutter="[^"]*"/g, '')
  86. .replace(/\sclass="([^"]*)"/, (m, classes) => {
  87. const parts = classes.split(/\s+/).filter(Boolean)
  88. const filtered = parts.filter(c => c !== 'list-toolbar')
  89. if (!filtered.includes('list-stats-row')) filtered.push('list-stats-row')
  90. if (!filtered.includes('mb8')) filtered.push('mb8')
  91. return ` class="${filtered.join(' ')}"`
  92. })
  93. if (!/\sclass="/.test(nextAttrs)) {
  94. nextAttrs += ' class="mb8 list-stats-row"'
  95. }
  96. const nextInner = inner.replace(/<el-col\b[^>]*>/g, '<el-col>')
  97. return `<el-row${nextAttrs}>${nextInner}</el-row>`
  98. }
  99. )
  100. }
  101. function extractQueryToolbarRow(content) {
  102. const rowRe = /<el-row\b[^>]*>[\s\S]*?<\/el-row>/g
  103. let match
  104. while ((match = rowRe.exec(content)) !== null) {
  105. const row = match[0]
  106. if (/handleQuery|resetQuery/.test(row) && !/stat-card/.test(row)) {
  107. return row
  108. }
  109. }
  110. return null
  111. }
  112. function extractToolbarButtons(row) {
  113. const parts = []
  114. const colRe = /<el-col[^>]*>([\s\S]*?)<\/el-col>/g
  115. let match
  116. while ((match = colRe.exec(row)) !== null) {
  117. const inner = match[1].trim()
  118. if (inner) parts.push(inner)
  119. }
  120. const rightToolbar = row.match(/<right-toolbar[\s\S]*?\/>|<right-toolbar[\s\S]*?<\/right-toolbar>/)
  121. if (rightToolbar && /:columns=/.test(rightToolbar[0])) {
  122. parts.push(rightToolbar[0].trim())
  123. }
  124. return parts.join('\n ')
  125. }
  126. function buildSearchActionsItem(buttonsHtml) {
  127. if (!buttonsHtml) return ''
  128. return ` <el-form-item class="list-search-actions">\n ${buttonsHtml}\n </el-form-item>`
  129. }
  130. function insertSearchActionsIntoForm(formHtml, actionsItem) {
  131. if (!actionsItem || formHtml.includes('list-search-actions')) {
  132. return formHtml
  133. }
  134. const patterns = [
  135. /<el-form-item[^>]*label="[^"]*时间[^"]*"[^>]*>[\s\S]*?<\/el-form-item>/,
  136. /<el-form-item[^>]*prop="dateRange"[^>]*>[\s\S]*?<\/el-form-item>/,
  137. /<el-form-item[^>]*>[\s\S]*?type="daterange"[\s\S]*?<\/el-form-item>/,
  138. /<el-form-item[^>]*>[\s\S]*?type="datetimerange"[\s\S]*?<\/el-form-item>/
  139. ]
  140. for (const re of patterns) {
  141. const match = formHtml.match(re)
  142. if (match) {
  143. return formHtml.replace(match[0], `${match[0]}\n${actionsItem}`)
  144. }
  145. }
  146. return formHtml.replace(/<\/el-form>\s*$/, `\n${actionsItem}\n </el-form>`)
  147. }
  148. function moveSearchIntoForm(content, isCompanyUi) {
  149. const toolbarRow = extractQueryToolbarRow(content)
  150. if (!toolbarRow) return content
  151. const buttonsHtml = extractToolbarButtons(toolbarRow)
  152. if (!buttonsHtml) return content
  153. let normalizedButtons = buttonsHtml
  154. if (isCompanyUi) {
  155. normalizedButtons = normalizedButtons.replace(
  156. /<el-button([^>]*@click="handleQuery"[^>]*)>/g,
  157. (match, attrs) => {
  158. const cleaned = attrs.replace(/\stype="[^"]+"/g, '')
  159. return `<el-button type="cyan"${cleaned}>`
  160. }
  161. )
  162. }
  163. const actionsItem = buildSearchActionsItem(normalizedButtons)
  164. const formStart = content.indexOf('ref="queryForm"')
  165. if (formStart < 0) return content
  166. const formOpen = content.lastIndexOf('<el-form', formStart)
  167. const formClose = content.indexOf('</el-form>', formStart)
  168. if (formOpen < 0 || formClose < 0) return content
  169. const before = content.slice(0, formOpen)
  170. let form = content.slice(formOpen, formClose + '</el-form>'.length)
  171. const after = content.slice(formClose + '</el-form>'.length)
  172. form = insertSearchActionsIntoForm(form, actionsItem)
  173. let next = before + form + after
  174. next = next.replace(toolbarRow, '')
  175. next = next.replace(/\n\s*\n\s*\n/g, '\n\n')
  176. return next
  177. }
  178. function stripQueryControlWidths(content) {
  179. const formStart = content.indexOf('ref="queryForm"')
  180. if (formStart < 0) return content
  181. const formOpen = content.lastIndexOf('<el-form', formStart)
  182. const formClose = content.indexOf('</el-form>', formStart)
  183. if (formOpen < 0 || formClose < 0) return content
  184. const head = content.slice(0, formOpen)
  185. const form = content.slice(formOpen, formClose + '</el-form>'.length)
  186. const tail = content.slice(formClose + '</el-form>'.length)
  187. return head + form.replace(WIDTH_STYLE_RE, '') + tail
  188. }
  189. function wrapListFooter(content) {
  190. if (content.includes('list-footer') || content.includes('store-order-footer')) {
  191. return content
  192. }
  193. const re = /(\n(\s*))<div(?![^>]*class="[^"]*(?:sort-info|dialog-footer|remark-block|list-page-remark))([^>]*)>([\s\S]*?)<\/div>\s*\n\s*(<pagination[\s\S]*?\/>)/
  194. return content.replace(re, (full, nl, indent, attrs, body, paginationTag) => {
  195. const trimmed = body.trim()
  196. if (!trimmed || /<el-|@click=|v-for=|v-if=/.test(trimmed)) {
  197. return full
  198. }
  199. return `${nl}${indent}<div class="list-footer">${nl}${indent} <div class="list-footer-summary"${attrs}>${body}</div>${nl}${indent} ${paginationTag.trim()}${nl}${indent}</div>`
  200. })
  201. }
  202. function shouldSkip(file, content) {
  203. const r = rel(file)
  204. if (SKIP_FILES.has(r)) return true
  205. if (!content.includes('ref="queryForm"')) return true
  206. if (!content.includes('app-container')) return true
  207. const dialogIdx = content.indexOf('<el-dialog')
  208. const queryIdx = content.indexOf('ref="queryForm"')
  209. if (dialogIdx >= 0 && dialogIdx < queryIdx) return true
  210. return false
  211. }
  212. function processFile(file) {
  213. const isCompanyUi = viewsDir.includes('saas-companyui')
  214. let content = fs.readFileSync(file, 'utf8')
  215. if (shouldSkip(file, content)) return null
  216. const original = content
  217. content = addClassToQueryForm(content)
  218. content = addListPageAlertClass(content)
  219. content = stripQueryControlWidths(content)
  220. content = moveSearchIntoForm(content, isCompanyUi)
  221. content = normalizeStatsRows(content)
  222. content = addListToolbarClass(content)
  223. content = wrapListFooter(content)
  224. if (content === original) return null
  225. fs.writeFileSync(file, content, 'utf8')
  226. return rel(file)
  227. }
  228. let changed = 0
  229. for (const file of walk(viewsDir)) {
  230. const result = processFile(file)
  231. if (result) {
  232. changed++
  233. console.log('updated: ' + result)
  234. }
  235. }
  236. console.log('\nDone. ' + changed + ' files updated in ' + viewsDir)