159 lines
5.7 KiB
JavaScript
159 lines
5.7 KiB
JavaScript
const fs = require('fs')
|
|
const path = require('path')
|
|
const { modules } = require('./capture-full-manual-pages')
|
|
|
|
const root = path.resolve(__dirname, '..', '..')
|
|
const outputPath = path.resolve(__dirname, 'manual-action-audit.json')
|
|
|
|
function unique(values) {
|
|
return [...new Set(values.map(value => String(value || '').trim()).filter(Boolean))]
|
|
}
|
|
|
|
function cleanText(value) {
|
|
return String(value || '')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/\{\{[\s\S]*?\}\}/g, ' ')
|
|
.replace(/ /g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
}
|
|
|
|
function literalAttribute(attributes, name) {
|
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
const match = attributes.match(new RegExp(`(?:^|\\s)${escaped}\\s*=\\s*["']([^"']+)["']`))
|
|
return match ? match[1].trim() : ''
|
|
}
|
|
|
|
function handlerName(expression) {
|
|
const match = String(expression || '').match(/(?:^|[;\s])([A-Za-z_$][\w$]*)\s*(?:\(|$)/)
|
|
return match ? match[1] : ''
|
|
}
|
|
|
|
function matchingTooltip(template, index) {
|
|
const before = template.slice(0, index)
|
|
const openingIndex = before.lastIndexOf('<el-tooltip')
|
|
const closingIndex = before.lastIndexOf('</el-tooltip>')
|
|
if (openingIndex < 0 || openingIndex < closingIndex) return ''
|
|
const opening = before.slice(openingIndex, before.indexOf('>', openingIndex) + 1)
|
|
return literalAttribute(opening, 'content')
|
|
}
|
|
|
|
function extractBlock(script, method) {
|
|
if (!method) return ''
|
|
const escaped = method.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
const pattern = new RegExp(`(?:async\\s+)?${escaped}\\s*\\([^)]*\\)\\s*\\{`, 'm')
|
|
const match = pattern.exec(script)
|
|
if (!match) return ''
|
|
const start = match.index + match[0].lastIndexOf('{')
|
|
let depth = 0
|
|
let quote = ''
|
|
let lineComment = false
|
|
let blockComment = false
|
|
for (let index = start; index < script.length; index++) {
|
|
const char = script[index]
|
|
const next = script[index + 1]
|
|
if (lineComment) {
|
|
if (char === '\n') lineComment = false
|
|
continue
|
|
}
|
|
if (blockComment) {
|
|
if (char === '*' && next === '/') {
|
|
blockComment = false
|
|
index++
|
|
}
|
|
continue
|
|
}
|
|
if (quote) {
|
|
if (char === '\\') {
|
|
index++
|
|
} else if (char === quote) {
|
|
quote = ''
|
|
}
|
|
continue
|
|
}
|
|
if (char === '/' && next === '/') {
|
|
lineComment = true
|
|
index++
|
|
continue
|
|
}
|
|
if (char === '/' && next === '*') {
|
|
blockComment = true
|
|
index++
|
|
continue
|
|
}
|
|
if (char === '"' || char === "'" || char === '`') {
|
|
quote = char
|
|
continue
|
|
}
|
|
if (char === '{') depth++
|
|
if (char === '}') {
|
|
depth--
|
|
if (depth === 0) return script.slice(start + 1, index)
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function methodDetails(script, method) {
|
|
const body = extractBlock(script, method)
|
|
const procedures = unique([...body.matchAll(/CreateData\s*\([^,]+,\s*["']([^"']+)["']/g)].map(match => match[1]))
|
|
const parameters = unique([...body.matchAll(/\[\s*["']([^"']+)["']\s*,/g)].map(match => match[1]))
|
|
const calls = unique([...body.matchAll(/this\.([A-Za-z_$][\w$]*)\s*\(/g)].map(match => match[1])
|
|
.filter(name => !['CreateData', 'ExecDatabase'].includes(name)))
|
|
const closesDialog = /this\.[A-Za-z_$][\w$]*(?:Visible|dialog[A-Za-z_$]*)\s*=\s*false/.test(body)
|
|
const refreshesList = calls.some(name => /^(?:search|get|query|load|refresh|init)/i.test(name))
|
|
return { procedures, parameters, calls, closesDialog, refreshesList }
|
|
}
|
|
|
|
function addAction(actions, template, index, attributes, body, fallbackLabel = '') {
|
|
const expression = literalAttribute(attributes, '@click') ||
|
|
literalAttribute(attributes, '@click.native') ||
|
|
literalAttribute(attributes, '@row-click')
|
|
if (!expression) return
|
|
const label = cleanText(body) || matchingTooltip(template, index) || fallbackLabel
|
|
if (!label) return
|
|
actions.push({ label, expression, method: handlerName(expression) })
|
|
}
|
|
|
|
const results = []
|
|
for (const module of modules) {
|
|
for (const [title, route, source] of module.pages) {
|
|
const sourcePath = path.resolve(root, 'src', 'views', source)
|
|
const content = fs.readFileSync(sourcePath, 'utf8')
|
|
const scriptStart = content.indexOf('<script')
|
|
const template = (scriptStart >= 0 ? content.slice(0, scriptStart) : content)
|
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
const script = scriptStart >= 0 ? content.slice(scriptStart) : ''
|
|
const actions = []
|
|
|
|
for (const match of template.matchAll(/<el-button\b([^>]*?)>([\s\S]*?)<\/el-button>/g)) {
|
|
addAction(actions, template, match.index, match[1], match[2])
|
|
}
|
|
for (const match of template.matchAll(/<el-button\b([^>]*?)\/>/g)) {
|
|
addAction(actions, template, match.index, match[1], '', '图标操作')
|
|
}
|
|
for (const match of template.matchAll(/<el-dropdown-item\b([^>]*)>([\s\S]*?)<\/el-dropdown-item>/g)) {
|
|
addAction(actions, template, match.index, match[1], match[2])
|
|
}
|
|
for (const match of template.matchAll(/<el-table\b([^>]*@row-click[^>]*)>/g)) {
|
|
addAction(actions, template, match.index, match[1], '', '点击列表行')
|
|
}
|
|
|
|
const deduplicated = []
|
|
const keys = new Set()
|
|
for (const action of actions) {
|
|
const key = `${action.label}|${action.method}|${action.expression}`
|
|
if (keys.has(key)) continue
|
|
keys.add(key)
|
|
deduplicated.push({ ...action, ...methodDetails(script, action.method) })
|
|
}
|
|
|
|
results.push({ module: module.title, title, route, source, actions: deduplicated })
|
|
}
|
|
}
|
|
|
|
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2), 'utf8')
|
|
console.log(`Audited ${results.length} pages`)
|
|
console.log(`Actions: ${results.reduce((total, page) => total + page.actions.length, 0)}`)
|
|
console.log(`Actions with database effects: ${results.reduce((total, page) => total + page.actions.filter(action => action.procedures.length).length, 0)}`)
|