| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- /**
- * 批量为列表页应用统一布局 class,并将搜索/操作按钮移入查询表单。
- * Usage:
- * node scripts/apply-list-page-layout.js
- * node scripts/apply-list-page-layout.js ../saas-companyui/src/views
- */
- const fs = require('fs')
- const path = require('path')
- const defaultViewsDir = path.join(__dirname, '../src/views')
- const viewsDir = process.argv[2] ? path.resolve(process.argv[2]) : defaultViewsDir
- const SKIP_FILES = new Set([
- 'hisStore/storeOrder/index.vue',
- 'system/user/index.vue',
- 'company/companyUser/index.vue',
- 'company/companyUser/indexOld.vue',
- 'company/companyUser/companyUserList.vue',
- 'addressBook/all.vue'
- ])
- const WIDTH_STYLE_RE = /\sstyle="width:\s*\d+(?:\.\d+)?px"/gi
- function walk(dir, files = []) {
- for (const name of fs.readdirSync(dir)) {
- const full = path.join(dir, name)
- if (fs.statSync(full).isDirectory()) {
- walk(full, files)
- } else if (name.endsWith('.vue')) {
- files.push(full)
- }
- }
- return files
- }
- function rel(file) {
- return path.relative(viewsDir, file).replace(/\\/g, '/')
- }
- function addClassToQueryForm(content) {
- return content.replace(
- /(<el-form\b(?=[^>]*ref="queryForm")[^>]*)(>)/,
- (match, start, end) => {
- if (/class="[^"]*list-search-form/.test(start)) {
- return match
- }
- if (/class="/.test(start)) {
- return start.replace(/class="([^"]*)"/, 'class="$1 list-search-form"') + end
- }
- return `${start} class="list-search-form"${end}`
- }
- )
- }
- function addListPageAlertClass(content) {
- const idx = content.indexOf('app-container')
- if (idx < 0) return content
- const slice = content.slice(idx)
- const re = /<el-alert\b(?![^>]*list-page-alert)([^>]*?)(\/?>)/i
- const match = slice.match(re)
- if (!match) return content
- const start = idx + slice.indexOf(match[0])
- const end = start + match[0].length
- let tagStart = `<el-alert${match[1]}`
- if (/class="/.test(tagStart)) {
- tagStart = tagStart.replace(/class="([^"]*)"/, 'class="$1 list-page-alert"')
- } else {
- tagStart += ' class="list-page-alert"'
- }
- const updated = `${tagStart}${match[2]}`
- return content.slice(0, start) + updated + content.slice(end)
- }
- function addListToolbarClass(content) {
- return content.replace(
- /(<el-row\b[^>]*class=")([^"]*mb8[^"]*)(")/g,
- (match, p1, classes, p3) => {
- if (classes.includes('list-toolbar') || classes.includes('store-order-toolbar') || classes.includes('list-stats-row')) {
- return match
- }
- return `${p1}${classes} list-toolbar${p3}`
- }
- )
- }
- function normalizeStatsRows(content) {
- return content.replace(
- /<el-row\b([^>]*)>([\s\S]*?)<\/el-row>/g,
- (row, attrs, inner) => {
- if (!/stat-card|stat-value|stat-label/.test(inner) || !/<el-card/.test(inner)) {
- return row
- }
- let nextAttrs = attrs
- .replace(/\s:gutter="[^"]*"/g, '')
- .replace(/\sclass="([^"]*)"/, (m, classes) => {
- const parts = classes.split(/\s+/).filter(Boolean)
- const filtered = parts.filter(c => c !== 'list-toolbar')
- if (!filtered.includes('list-stats-row')) filtered.push('list-stats-row')
- if (!filtered.includes('mb8')) filtered.push('mb8')
- return ` class="${filtered.join(' ')}"`
- })
- if (!/\sclass="/.test(nextAttrs)) {
- nextAttrs += ' class="mb8 list-stats-row"'
- }
- const nextInner = inner.replace(/<el-col\b[^>]*>/g, '<el-col>')
- return `<el-row${nextAttrs}>${nextInner}</el-row>`
- }
- )
- }
- function extractQueryToolbarRow(content) {
- const rowRe = /<el-row\b[^>]*>[\s\S]*?<\/el-row>/g
- let match
- while ((match = rowRe.exec(content)) !== null) {
- const row = match[0]
- if (/handleQuery|resetQuery/.test(row) && !/stat-card/.test(row)) {
- return row
- }
- }
- return null
- }
- function extractToolbarButtons(row) {
- const parts = []
- const colRe = /<el-col[^>]*>([\s\S]*?)<\/el-col>/g
- let match
- while ((match = colRe.exec(row)) !== null) {
- const inner = match[1].trim()
- if (inner) parts.push(inner)
- }
- const rightToolbar = row.match(/<right-toolbar[\s\S]*?\/>|<right-toolbar[\s\S]*?<\/right-toolbar>/)
- if (rightToolbar && /:columns=/.test(rightToolbar[0])) {
- parts.push(rightToolbar[0].trim())
- }
- return parts.join('\n ')
- }
- function buildSearchActionsItem(buttonsHtml) {
- if (!buttonsHtml) return ''
- return ` <el-form-item class="list-search-actions">\n ${buttonsHtml}\n </el-form-item>`
- }
- function insertSearchActionsIntoForm(formHtml, actionsItem) {
- if (!actionsItem || formHtml.includes('list-search-actions')) {
- return formHtml
- }
- const patterns = [
- /<el-form-item[^>]*label="[^"]*时间[^"]*"[^>]*>[\s\S]*?<\/el-form-item>/,
- /<el-form-item[^>]*prop="dateRange"[^>]*>[\s\S]*?<\/el-form-item>/,
- /<el-form-item[^>]*>[\s\S]*?type="daterange"[\s\S]*?<\/el-form-item>/,
- /<el-form-item[^>]*>[\s\S]*?type="datetimerange"[\s\S]*?<\/el-form-item>/
- ]
- for (const re of patterns) {
- const match = formHtml.match(re)
- if (match) {
- return formHtml.replace(match[0], `${match[0]}\n${actionsItem}`)
- }
- }
- return formHtml.replace(/<\/el-form>\s*$/, `\n${actionsItem}\n </el-form>`)
- }
- function moveSearchIntoForm(content, isCompanyUi) {
- const toolbarRow = extractQueryToolbarRow(content)
- if (!toolbarRow) return content
- const buttonsHtml = extractToolbarButtons(toolbarRow)
- if (!buttonsHtml) return content
- let normalizedButtons = buttonsHtml
- if (isCompanyUi) {
- normalizedButtons = normalizedButtons.replace(
- /<el-button([^>]*@click="handleQuery"[^>]*)>/g,
- (match, attrs) => {
- const cleaned = attrs.replace(/\stype="[^"]+"/g, '')
- return `<el-button type="cyan"${cleaned}>`
- }
- )
- }
- const actionsItem = buildSearchActionsItem(normalizedButtons)
- const formStart = content.indexOf('ref="queryForm"')
- if (formStart < 0) return content
- const formOpen = content.lastIndexOf('<el-form', formStart)
- const formClose = content.indexOf('</el-form>', formStart)
- if (formOpen < 0 || formClose < 0) return content
- const before = content.slice(0, formOpen)
- let form = content.slice(formOpen, formClose + '</el-form>'.length)
- const after = content.slice(formClose + '</el-form>'.length)
- form = insertSearchActionsIntoForm(form, actionsItem)
- let next = before + form + after
- next = next.replace(toolbarRow, '')
- next = next.replace(/\n\s*\n\s*\n/g, '\n\n')
- return next
- }
- function stripQueryControlWidths(content) {
- const formStart = content.indexOf('ref="queryForm"')
- if (formStart < 0) return content
- const formOpen = content.lastIndexOf('<el-form', formStart)
- const formClose = content.indexOf('</el-form>', formStart)
- if (formOpen < 0 || formClose < 0) return content
- const head = content.slice(0, formOpen)
- const form = content.slice(formOpen, formClose + '</el-form>'.length)
- const tail = content.slice(formClose + '</el-form>'.length)
- return head + form.replace(WIDTH_STYLE_RE, '') + tail
- }
- function wrapListFooter(content) {
- if (content.includes('list-footer') || content.includes('store-order-footer')) {
- return content
- }
- const re = /(\n(\s*))<div(?![^>]*class="[^"]*(?:sort-info|dialog-footer|remark-block|list-page-remark))([^>]*)>([\s\S]*?)<\/div>\s*\n\s*(<pagination[\s\S]*?\/>)/
- return content.replace(re, (full, nl, indent, attrs, body, paginationTag) => {
- const trimmed = body.trim()
- if (!trimmed || /<el-|@click=|v-for=|v-if=/.test(trimmed)) {
- return full
- }
- return `${nl}${indent}<div class="list-footer">${nl}${indent} <div class="list-footer-summary"${attrs}>${body}</div>${nl}${indent} ${paginationTag.trim()}${nl}${indent}</div>`
- })
- }
- function shouldSkip(file, content) {
- const r = rel(file)
- if (SKIP_FILES.has(r)) return true
- if (!content.includes('ref="queryForm"')) return true
- if (!content.includes('app-container')) return true
- const dialogIdx = content.indexOf('<el-dialog')
- const queryIdx = content.indexOf('ref="queryForm"')
- if (dialogIdx >= 0 && dialogIdx < queryIdx) return true
- return false
- }
- function processFile(file) {
- const isCompanyUi = viewsDir.includes('saas-companyui')
- let content = fs.readFileSync(file, 'utf8')
- if (shouldSkip(file, content)) return null
- const original = content
- content = addClassToQueryForm(content)
- content = addListPageAlertClass(content)
- content = stripQueryControlWidths(content)
- content = moveSearchIntoForm(content, isCompanyUi)
- content = normalizeStatsRows(content)
- content = addListToolbarClass(content)
- content = wrapListFooter(content)
- if (content === original) return null
- fs.writeFileSync(file, content, 'utf8')
- return rel(file)
- }
- let changed = 0
- for (const file of walk(viewsDir)) {
- const result = processFile(file)
- if (result) {
- changed++
- console.log('updated: ' + result)
- }
- }
- console.log('\nDone. ' + changed + ' files updated in ' + viewsDir)
|