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('') 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('= 0 ? content.slice(0, scriptStart) : content) .replace(//g, '') const script = scriptStart >= 0 ? content.slice(scriptStart) : '' const actions = [] for (const match of template.matchAll(/]*?)>([\s\S]*?)<\/el-button>/g)) { addAction(actions, template, match.index, match[1], match[2]) } for (const match of template.matchAll(/]*?)\/>/g)) { addAction(actions, template, match.index, match[1], '', '图标操作') } for (const match of template.matchAll(/]*)>([\s\S]*?)<\/el-dropdown-item>/g)) { addAction(actions, template, match.index, match[1], match[2]) } for (const match of template.matchAll(/]*@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)}`)