/** * 批量为列表页应用统一布局 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( /(]*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 = /]*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 = `]*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( /]*)>([\s\S]*?)<\/el-row>/g, (row, attrs, inner) => { if (!/stat-card|stat-value|stat-label/.test(inner) || !/ { 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(/]*>/g, '') return `${nextInner}` } ) } function extractQueryToolbarRow(content) { const rowRe = /]*>[\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 = /]*>([\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(/|/) if (rightToolbar && /:columns=/.test(rightToolbar[0])) { parts.push(rightToolbar[0].trim()) } return parts.join('\n ') } function buildSearchActionsItem(buttonsHtml) { if (!buttonsHtml) return '' return ` \n ${buttonsHtml}\n ` } function insertSearchActionsIntoForm(formHtml, actionsItem) { if (!actionsItem || formHtml.includes('list-search-actions')) { return formHtml } const patterns = [ /]*label="[^"]*时间[^"]*"[^>]*>[\s\S]*?<\/el-form-item>/, /]*prop="dateRange"[^>]*>[\s\S]*?<\/el-form-item>/, /]*>[\s\S]*?type="daterange"[\s\S]*?<\/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 `) } 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( /]*@click="handleQuery"[^>]*)>/g, (match, attrs) => { const cleaned = attrs.replace(/\stype="[^"]+"/g, '') return `` } ) } const actionsItem = buildSearchActionsItem(normalizedButtons) const formStart = content.indexOf('ref="queryForm"') if (formStart < 0) return content const formOpen = content.lastIndexOf('', formStart) if (formOpen < 0 || formClose < 0) return content const before = content.slice(0, formOpen) let form = content.slice(formOpen, formClose + ''.length) const after = content.slice(formClose + ''.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('', formStart) if (formOpen < 0 || formClose < 0) return content const head = content.slice(0, formOpen) const form = content.slice(formOpen, formClose + ''.length) const tail = content.slice(formClose + ''.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*))]*class="[^"]*(?:sort-info|dialog-footer|remark-block|list-page-remark))([^>]*)>([\s\S]*?)<\/div>\s*\n\s*()/ return content.replace(re, (full, nl, indent, attrs, body, paginationTag) => { const trimmed = body.trim() if (!trimmed || /${nl}${indent} ${nl}${indent} ${paginationTag.trim()}${nl}${indent}` }) } 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('= 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)