|
|
|
const { readFileSync, writeFileSync, existsSync, readdirSync } = require('fs')
|
|
|
|
const { resolve } = require('path')
|
|
|
|
const parser = require('@babel/parser')
|
|
|
|
const traverse = require('@babel/traverse').default
|
|
|
|
const { parse } = require('@vue/compiler-sfc')
|
|
|
|
const AstAnalyzer = require('./ast-analyzer')
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 触发器分析模块
|
|
|
|
* 负责分析组件中的按钮和事件触发器
|
|
|
|
*/
|
|
|
|
class TriggerAnalyzer {
|
|
|
|
constructor() {
|
|
|
|
this.astAnalyzer = new AstAnalyzer()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找API方法的触发器源 - 第一步:第一层触发源分析
|
|
|
|
* @param {Array} apiMappings - API映射数组
|
|
|
|
* @param {Array} routes - 路由配置数组
|
|
|
|
* @returns {Array} 增强的API映射数组
|
|
|
|
*/
|
|
|
|
findTriggerSourcesForApiMappings(apiMappings, routes) {
|
|
|
|
const enhancedApiMappings = []
|
|
|
|
|
|
|
|
// 创建组件到authType的映射
|
|
|
|
const componentAuthMap = this.createComponentAuthMap(routes)
|
|
|
|
|
|
|
|
apiMappings.forEach(moduleMapping => {
|
|
|
|
const enhancedModuleMapping = {
|
|
|
|
...moduleMapping,
|
|
|
|
apiMappings: []
|
|
|
|
}
|
|
|
|
|
|
|
|
moduleMapping.apiMappings.forEach(apiMapping => {
|
|
|
|
// 第一步:查找第一层触发源
|
|
|
|
const triggerSources = this.findFirstLevelTriggerSources(
|
|
|
|
moduleMapping.serviceName, // 使用第一层的serviceName
|
|
|
|
apiMapping.apiMethodName,
|
|
|
|
moduleMapping.module
|
|
|
|
)
|
|
|
|
|
|
|
|
// 过滤掉authType为public的组件的触发源
|
|
|
|
const filteredTriggerSources = this.filterPublicComponentTriggers(
|
|
|
|
triggerSources,
|
|
|
|
componentAuthMap
|
|
|
|
)
|
|
|
|
|
|
|
|
// 只有当有触发源时才添加API映射
|
|
|
|
if (filteredTriggerSources.length > 0) {
|
|
|
|
enhancedModuleMapping.apiMappings.push({
|
|
|
|
...apiMapping,
|
|
|
|
triggerSources: filteredTriggerSources
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 只有当模块有API映射时才添加模块映射
|
|
|
|
if (enhancedModuleMapping.apiMappings.length > 0) {
|
|
|
|
enhancedApiMappings.push(enhancedModuleMapping)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return enhancedApiMappings
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找第一层触发源 - 使用Babel分析真正的调用关系
|
|
|
|
* @param {string} serviceName - 服务名称
|
|
|
|
* @param {string} apiMethodName - API方法名称
|
|
|
|
* @param {string} moduleName - 模块名称
|
|
|
|
* @returns {Array} 触发器源数组
|
|
|
|
*/
|
|
|
|
findFirstLevelTriggerSources(serviceName, apiMethodName, moduleName) {
|
|
|
|
const triggerSources = []
|
|
|
|
|
|
|
|
try {
|
|
|
|
// 1. 遍历模块中的所有Vue组件文件
|
|
|
|
const modulePath = resolve(__dirname, `../../src/renderer/modules/${moduleName}`)
|
|
|
|
if (existsSync(modulePath)) {
|
|
|
|
const files = readdirSync(modulePath, { recursive: true })
|
|
|
|
|
|
|
|
files.forEach(file => {
|
|
|
|
if (typeof file === 'string' && file.endsWith('.vue')) {
|
|
|
|
const filePath = resolve(modulePath, file)
|
|
|
|
const componentName = this.extractComponentNameFromPath(file)
|
|
|
|
|
|
|
|
// 2. 使用Babel分析Vue组件中的API调用
|
|
|
|
const componentTriggerSources = this.analyzeComponentForApiCalls(
|
|
|
|
filePath,
|
|
|
|
serviceName,
|
|
|
|
apiMethodName,
|
|
|
|
componentName
|
|
|
|
)
|
|
|
|
|
|
|
|
triggerSources.push(...componentTriggerSources)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.warn(`分析模块 ${moduleName} 时出错:`, error.message)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 去重
|
|
|
|
const uniqueTriggerSources = this.deduplicateTriggerSources(triggerSources)
|
|
|
|
|
|
|
|
return uniqueTriggerSources
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找API方法的触发器源(保留原方法用于后续扩展)
|
|
|
|
* @param {string} serviceName - 服务名称
|
|
|
|
* @param {string} apiMethodName - API方法名称
|
|
|
|
* @param {string} moduleName - 模块名称
|
|
|
|
* @returns {Array} 触发器源数组
|
|
|
|
*/
|
|
|
|
findTriggerSourcesForApiMethod(serviceName, apiMethodName, moduleName) {
|
|
|
|
const triggerSources = []
|
|
|
|
|
|
|
|
// 1. 检查路由组件
|
|
|
|
const routeComponents = this.getRouteComponents(moduleName)
|
|
|
|
routeComponents.forEach(route => {
|
|
|
|
const triggerAnalysis = this.analyzeComponentWithAST(
|
|
|
|
route.component,
|
|
|
|
serviceName,
|
|
|
|
apiMethodName
|
|
|
|
)
|
|
|
|
|
|
|
|
if (triggerAnalysis.triggerSources.length > 0) {
|
|
|
|
triggerSources.push(...triggerAnalysis.triggerSources)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 2. 检查模块中的所有组件
|
|
|
|
const componentsInModule = this.getModuleComponents(moduleName)
|
|
|
|
componentsInModule.forEach(componentName => {
|
|
|
|
const triggerAnalysis = this.analyzeComponentWithAST(
|
|
|
|
componentName,
|
|
|
|
serviceName,
|
|
|
|
apiMethodName
|
|
|
|
)
|
|
|
|
|
|
|
|
if (triggerAnalysis.triggerSources.length > 0) {
|
|
|
|
triggerSources.push(...triggerAnalysis.triggerSources)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 去重
|
|
|
|
const uniqueTriggerSources = this.deduplicateTriggerSources(triggerSources)
|
|
|
|
|
|
|
|
return uniqueTriggerSources
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 获取路由组件
|
|
|
|
* @param {string} moduleName - 模块名称
|
|
|
|
* @returns {Array} 路由组件数组
|
|
|
|
*/
|
|
|
|
getRouteComponents(moduleName) {
|
|
|
|
const routeComponents = []
|
|
|
|
|
|
|
|
try {
|
|
|
|
const routeConfigPath = resolve(__dirname, '../../src/renderer/router/index.js')
|
|
|
|
if (existsSync(routeConfigPath)) {
|
|
|
|
const routeContent = readFileSync(routeConfigPath, 'utf-8')
|
|
|
|
|
|
|
|
// 根据模块名称映射到对应的组件名称
|
|
|
|
const moduleToComponentMap = {
|
|
|
|
'role-management': 'RoleManagement',
|
|
|
|
'user-management': 'UserManagement',
|
|
|
|
'system-settings': 'Settings',
|
|
|
|
'user-profile': 'UserProfile'
|
|
|
|
}
|
|
|
|
|
|
|
|
const componentName = moduleToComponentMap[moduleName]
|
|
|
|
if (componentName) {
|
|
|
|
// 查找包含该组件的路由
|
|
|
|
const routeMatches = routeContent.match(new RegExp(`component:\\s*${componentName}`, 'g'))
|
|
|
|
if (routeMatches) {
|
|
|
|
routeComponents.push({
|
|
|
|
component: componentName
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// 静默处理错误
|
|
|
|
}
|
|
|
|
|
|
|
|
return routeComponents
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 获取模块中的所有组件
|
|
|
|
* @param {string} moduleName - 模块名称
|
|
|
|
* @returns {Array} 组件名称数组
|
|
|
|
*/
|
|
|
|
getModuleComponents(moduleName) {
|
|
|
|
const components = []
|
|
|
|
|
|
|
|
try {
|
|
|
|
const modulePath = resolve(__dirname, '../../src/renderer/modules', moduleName)
|
|
|
|
if (existsSync(modulePath)) {
|
|
|
|
// 查找views目录
|
|
|
|
const viewsPath = resolve(modulePath, 'views')
|
|
|
|
if (existsSync(viewsPath)) {
|
|
|
|
const viewFiles = readdirSync(viewsPath).filter(file => file.endsWith('.vue'))
|
|
|
|
viewFiles.forEach(file => {
|
|
|
|
components.push(file.replace('.vue', ''))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// 查找components目录
|
|
|
|
const componentsPath = resolve(modulePath, 'components')
|
|
|
|
if (existsSync(componentsPath)) {
|
|
|
|
const componentFiles = readdirSync(componentsPath).filter(file => file.endsWith('.vue'))
|
|
|
|
componentFiles.forEach(file => {
|
|
|
|
components.push(file.replace('.vue', ''))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// 静默处理错误
|
|
|
|
}
|
|
|
|
|
|
|
|
return components
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 使用AST分析组件
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @param {string} serviceName - 服务名称
|
|
|
|
* @param {string} apiMethodName - API方法名称
|
|
|
|
* @returns {Object} 触发器分析结果
|
|
|
|
*/
|
|
|
|
analyzeComponentWithAST(componentName, serviceName, apiMethodName) {
|
|
|
|
const triggerSources = []
|
|
|
|
|
|
|
|
try {
|
|
|
|
const filePath = this.findComponentFile(componentName)
|
|
|
|
if (!filePath) {
|
|
|
|
return { triggerSources: [] }
|
|
|
|
}
|
|
|
|
|
|
|
|
const content = readFileSync(filePath, 'utf-8')
|
|
|
|
|
|
|
|
// 1. 检查组件的authType属性,如果是public则跳过
|
|
|
|
const authTypeMatch = content.match(/authType\s*[=:]\s*["']([^"']+)["']/)
|
|
|
|
if (authTypeMatch && authTypeMatch[1] === 'public') {
|
|
|
|
return { triggerSources: [] }
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2. 检查组件是否包含目标API调用
|
|
|
|
if (!content.includes(`${serviceName}.${apiMethodName}`)) {
|
|
|
|
return { triggerSources: [] }
|
|
|
|
}
|
|
|
|
|
|
|
|
// 3. 使用Babel AST分析找到调用该API的方法
|
|
|
|
const callingMethods = this.astAnalyzer.findMethodsCallingServiceWithBabel(content, serviceName, apiMethodName)
|
|
|
|
|
|
|
|
if (callingMethods.length > 0) {
|
|
|
|
// 4. 分析每个调用方法的触发源
|
|
|
|
const methodTriggers = this.analyzeMethodTriggerSources(content, componentName, callingMethods, filePath)
|
|
|
|
triggerSources.push(...methodTriggers)
|
|
|
|
} else {
|
|
|
|
// 如果没有找到按钮触发器,记录页面级触发器
|
|
|
|
triggerSources.push({
|
|
|
|
component: componentName,
|
|
|
|
triggerName: null,
|
|
|
|
triggerType: 'page'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
// 静默处理错误
|
|
|
|
}
|
|
|
|
|
|
|
|
return { triggerSources }
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找组件文件
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @returns {string|null} 组件文件路径
|
|
|
|
*/
|
|
|
|
findComponentFile(componentName) {
|
|
|
|
const possiblePaths = [
|
|
|
|
resolve(__dirname, '../../src/renderer/modules', componentName, 'views', `${componentName}.vue`),
|
|
|
|
resolve(__dirname, '../../src/renderer/modules', componentName, 'components', `${componentName}.vue`),
|
|
|
|
resolve(__dirname, '../../src/renderer/components', `${componentName}.vue`),
|
|
|
|
resolve(__dirname, '../../src/renderer/views', `${componentName}.vue`)
|
|
|
|
]
|
|
|
|
|
|
|
|
for (const path of possiblePaths) {
|
|
|
|
if (existsSync(path)) {
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 分析方法的触发源
|
|
|
|
* @param {string} content - 组件内容
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @param {Array} methodNames - 方法名数组
|
|
|
|
* @param {string} filePath - 文件路径
|
|
|
|
* @returns {Array} 触发器源数组
|
|
|
|
*/
|
|
|
|
analyzeMethodTriggerSources(content, componentName, methodNames, filePath = null) {
|
|
|
|
const triggerSources = []
|
|
|
|
|
|
|
|
// 提取模板部分
|
|
|
|
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/)
|
|
|
|
if (!templateMatch) {
|
|
|
|
return triggerSources
|
|
|
|
}
|
|
|
|
|
|
|
|
const templateContent = templateMatch[1]
|
|
|
|
|
|
|
|
// 使用正则表达式查找调用这些方法的按钮
|
|
|
|
methodNames.forEach(methodName => {
|
|
|
|
// 1. 查找@click="methodName"或@click="methodName("
|
|
|
|
const clickPattern = new RegExp(`@click\\s*=\\s*["']${methodName}\\s*\\([^)]*\\)["']`, 'g')
|
|
|
|
const clickMatches = templateContent.match(clickPattern)
|
|
|
|
|
|
|
|
if (clickMatches) {
|
|
|
|
clickMatches.forEach((match, index) => {
|
|
|
|
// 查找包含这个@click的button标签
|
|
|
|
const buttonMatch = this.findButtonContainingClick(templateContent, match)
|
|
|
|
if (buttonMatch) {
|
|
|
|
const buttonName = this.extractButtonName(buttonMatch)
|
|
|
|
const triggerType = buttonName ? 'button' : 'button'
|
|
|
|
|
|
|
|
triggerSources.push({
|
|
|
|
component: componentName,
|
|
|
|
triggerName: buttonName || this.astAnalyzer.generateUniqueButtonName(componentName, methodName),
|
|
|
|
triggerType: triggerType
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2. 查找@submit.prevent="methodName"的表单提交按钮
|
|
|
|
const submitPattern = new RegExp(`@submit\\.prevent\\s*=\\s*["']${methodName}["']`, 'g')
|
|
|
|
const submitMatches = templateContent.match(submitPattern)
|
|
|
|
|
|
|
|
if (submitMatches) {
|
|
|
|
submitMatches.forEach((match, index) => {
|
|
|
|
// 查找包含这个@submit.prevent的form标签
|
|
|
|
const formMatch = this.findFormContainingSubmit(templateContent, match)
|
|
|
|
if (formMatch) {
|
|
|
|
// 查找表单中的type="submit"按钮
|
|
|
|
const submitButtonMatch = this.findSubmitButtonInForm(formMatch)
|
|
|
|
if (submitButtonMatch) {
|
|
|
|
const buttonName = this.extractButtonName(submitButtonMatch)
|
|
|
|
const triggerType = buttonName ? 'button' : 'button'
|
|
|
|
|
|
|
|
triggerSources.push({
|
|
|
|
component: componentName,
|
|
|
|
triggerName: buttonName || this.astAnalyzer.generateUniqueButtonName(componentName, methodName),
|
|
|
|
triggerType: triggerType
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// 如果没有找到具体的提交按钮,记录为form类型
|
|
|
|
triggerSources.push({
|
|
|
|
component: componentName,
|
|
|
|
triggerName: null,
|
|
|
|
triggerType: 'form'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return triggerSources
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找包含指定@click的button标签
|
|
|
|
* @param {string} templateContent - 模板内容
|
|
|
|
* @param {string} clickMatch - @click匹配
|
|
|
|
* @returns {string|null} 按钮HTML
|
|
|
|
*/
|
|
|
|
findButtonContainingClick(templateContent, clickMatch) {
|
|
|
|
const clickIndex = templateContent.indexOf(clickMatch)
|
|
|
|
|
|
|
|
if (clickIndex === -1) return null
|
|
|
|
|
|
|
|
// 从@click位置往前查找最近的<button标签
|
|
|
|
const beforeClick = templateContent.substring(0, clickIndex)
|
|
|
|
const lastButtonIndex = beforeClick.lastIndexOf('<button')
|
|
|
|
|
|
|
|
if (lastButtonIndex === -1) return null
|
|
|
|
|
|
|
|
// 从<button开始往后查找对应的</button>
|
|
|
|
const afterButton = templateContent.substring(lastButtonIndex)
|
|
|
|
const buttonEndIndex = afterButton.indexOf('</button>')
|
|
|
|
|
|
|
|
if (buttonEndIndex === -1) return null
|
|
|
|
|
|
|
|
return afterButton.substring(0, buttonEndIndex + 9) // 包含</button>
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 提取按钮名称
|
|
|
|
* @param {string} buttonHtml - 按钮HTML
|
|
|
|
* @returns {string|null} 按钮名称
|
|
|
|
*/
|
|
|
|
extractButtonName(buttonHtml) {
|
|
|
|
const nameMatch = buttonHtml.match(/name\s*=\s*["']([^"']+)["']/)
|
|
|
|
return nameMatch ? nameMatch[1] : null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找包含指定@submit.prevent的form标签
|
|
|
|
* @param {string} templateContent - 模板内容
|
|
|
|
* @param {string} submitMatch - @submit.prevent匹配
|
|
|
|
* @returns {string|null} 表单HTML
|
|
|
|
*/
|
|
|
|
findFormContainingSubmit(templateContent, submitMatch) {
|
|
|
|
const submitIndex = templateContent.indexOf(submitMatch)
|
|
|
|
|
|
|
|
if (submitIndex === -1) return null
|
|
|
|
|
|
|
|
// 从@submit.prevent位置往前查找最近的<form标签
|
|
|
|
const beforeSubmit = templateContent.substring(0, submitIndex)
|
|
|
|
const lastFormIndex = beforeSubmit.lastIndexOf('<form')
|
|
|
|
|
|
|
|
if (lastFormIndex === -1) return null
|
|
|
|
|
|
|
|
// 从<form开始往后查找对应的</form>
|
|
|
|
const afterForm = templateContent.substring(lastFormIndex)
|
|
|
|
const formEndIndex = afterForm.indexOf('</form>')
|
|
|
|
|
|
|
|
if (formEndIndex === -1) return null
|
|
|
|
|
|
|
|
return afterForm.substring(0, formEndIndex + 7) // 包含</form>
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找表单中的type="submit"按钮
|
|
|
|
* @param {string} formHtml - 表单HTML
|
|
|
|
* @returns {string|null} 提交按钮HTML
|
|
|
|
*/
|
|
|
|
findSubmitButtonInForm(formHtml) {
|
|
|
|
const submitButtonPattern = /<button[^>]*type\s*=\s*["']submit["'][^>]*>[\s\S]*?<\/button>/g
|
|
|
|
const match = submitButtonPattern.exec(formHtml)
|
|
|
|
return match ? match[0] : null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 去重触发器源
|
|
|
|
* @param {Array} triggerSources - 触发器源数组
|
|
|
|
* @returns {Array} 去重后的触发器源数组
|
|
|
|
*/
|
|
|
|
deduplicateTriggerSources(triggerSources) {
|
|
|
|
const seen = new Set()
|
|
|
|
return triggerSources.filter(trigger => {
|
|
|
|
const key = `${trigger.component}-${trigger.triggerName}-${trigger.triggerType}`
|
|
|
|
if (seen.has(key)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
seen.add(key)
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 创建组件到authType的映射
|
|
|
|
* @param {Array} routes - 路由配置数组
|
|
|
|
* @returns {Map} 组件到authType的映射
|
|
|
|
*/
|
|
|
|
createComponentAuthMap(routes) {
|
|
|
|
const componentAuthMap = new Map()
|
|
|
|
|
|
|
|
// 从Vue组件文件中读取authType属性
|
|
|
|
routes.forEach(route => {
|
|
|
|
if (route.component) {
|
|
|
|
const authType = this.getComponentAuthType(route.component)
|
|
|
|
if (authType) {
|
|
|
|
componentAuthMap.set(route.component, authType)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 扫描所有模块目录中的组件文件,查找authType属性
|
|
|
|
const moduleDirs = ['core', 'user-management', 'role-management', 'system-settings']
|
|
|
|
moduleDirs.forEach(moduleDir => {
|
|
|
|
this.scanComponentsForAuthType(moduleDir, componentAuthMap)
|
|
|
|
})
|
|
|
|
|
|
|
|
return componentAuthMap
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 扫描指定模块目录中的组件文件,查找authType属性
|
|
|
|
* @param {string} moduleDir - 模块目录名
|
|
|
|
* @param {Map} componentAuthMap - 组件到authType的映射
|
|
|
|
*/
|
|
|
|
scanComponentsForAuthType(moduleDir, componentAuthMap) {
|
|
|
|
try {
|
|
|
|
const modulePath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}`)
|
|
|
|
if (existsSync(modulePath)) {
|
|
|
|
const files = readdirSync(modulePath, { recursive: true })
|
|
|
|
|
|
|
|
files.forEach(file => {
|
|
|
|
if (typeof file === 'string' && file.endsWith('.vue')) {
|
|
|
|
const filePath = resolve(modulePath, file)
|
|
|
|
const componentName = this.extractComponentNameFromPath(file)
|
|
|
|
|
|
|
|
// 如果还没有这个组件的authType信息,则尝试获取
|
|
|
|
if (!componentAuthMap.has(componentName)) {
|
|
|
|
const authType = this.getComponentAuthType(componentName)
|
|
|
|
if (authType) {
|
|
|
|
componentAuthMap.set(componentName, authType)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.warn(`扫描模块 ${moduleDir} 的组件时出错:`, error.message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 从Vue组件文件中获取authType属性
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @returns {string|null} authType值
|
|
|
|
*/
|
|
|
|
getComponentAuthType(componentName) {
|
|
|
|
try {
|
|
|
|
// 在modules目录中查找组件文件
|
|
|
|
const moduleDirs = ['core', 'user-management', 'role-management', 'system-settings']
|
|
|
|
|
|
|
|
for (const moduleDir of moduleDirs) {
|
|
|
|
const componentPath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}/components/${componentName}.vue`)
|
|
|
|
if (existsSync(componentPath)) {
|
|
|
|
const content = readFileSync(componentPath, 'utf-8')
|
|
|
|
const authTypeMatch = content.match(/<template\s+authType\s*=\s*["']([^"']+)["']/)
|
|
|
|
if (authTypeMatch) {
|
|
|
|
return authTypeMatch[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 也检查views目录
|
|
|
|
const viewPath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}/views/${componentName}.vue`)
|
|
|
|
if (existsSync(viewPath)) {
|
|
|
|
const content = readFileSync(viewPath, 'utf-8')
|
|
|
|
const authTypeMatch = content.match(/<template\s+authType\s*=\s*["']([^"']+)["']/)
|
|
|
|
if (authTypeMatch) {
|
|
|
|
return authTypeMatch[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.warn(`获取组件 ${componentName} 的authType时出错:`, error.message)
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 过滤掉authType为public的组件的触发源
|
|
|
|
* @param {Array} triggerSources - 触发源数组
|
|
|
|
* @param {Map} componentAuthMap - 组件到authType的映射
|
|
|
|
* @returns {Array} 过滤后的触发源数组
|
|
|
|
*/
|
|
|
|
filterPublicComponentTriggers(triggerSources, componentAuthMap) {
|
|
|
|
return triggerSources.filter(trigger => {
|
|
|
|
const authType = componentAuthMap.get(trigger.component)
|
|
|
|
// 如果组件没有定义authType,或者authType不是public,则保留
|
|
|
|
return !authType || authType !== 'public'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 从文件路径提取组件名称
|
|
|
|
* @param {string} filePath - 文件路径
|
|
|
|
* @returns {string} 组件名称
|
|
|
|
*/
|
|
|
|
extractComponentNameFromPath(filePath) {
|
|
|
|
const fileName = filePath.split('/').pop().split('\\').pop()
|
|
|
|
return fileName.replace('.vue', '')
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 分析Vue组件中的API调用
|
|
|
|
* @param {string} filePath - 组件文件路径
|
|
|
|
* @param {string} serviceName - 服务名称
|
|
|
|
* @param {string} apiMethodName - API方法名称
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @returns {Array} 触发源数组
|
|
|
|
*/
|
|
|
|
analyzeComponentForApiCalls(filePath, serviceName, apiMethodName, componentName) {
|
|
|
|
const triggerSources = []
|
|
|
|
|
|
|
|
try {
|
|
|
|
const content = readFileSync(filePath, 'utf-8')
|
|
|
|
|
|
|
|
// 使用Babel解析Vue组件
|
|
|
|
const ast = this.parseVueComponent(content)
|
|
|
|
if (!ast) return triggerSources
|
|
|
|
|
|
|
|
// 查找API调用和函数调用关系,传递文件路径用于追溯
|
|
|
|
const apiCalls = this.findApiCallsWithContext(ast, serviceName, apiMethodName, filePath, componentName)
|
|
|
|
|
|
|
|
// 为每个API调用创建触发源
|
|
|
|
apiCalls.forEach(call => {
|
|
|
|
triggerSources.push({
|
|
|
|
component: componentName,
|
|
|
|
triggerName: call.triggerName || '',
|
|
|
|
triggerType: call.triggerType || 'function'
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
console.warn(`分析组件 ${filePath} 时出错:`, error.message)
|
|
|
|
}
|
|
|
|
|
|
|
|
return triggerSources
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 解析Vue组件内容
|
|
|
|
* @param {string} content - 组件内容
|
|
|
|
* @returns {Object|null} AST对象
|
|
|
|
*/
|
|
|
|
parseVueComponent(content) {
|
|
|
|
try {
|
|
|
|
// 提取<script>部分
|
|
|
|
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/)
|
|
|
|
if (!scriptMatch) return null
|
|
|
|
|
|
|
|
const scriptContent = scriptMatch[1]
|
|
|
|
|
|
|
|
// 使用Babel解析JavaScript代码
|
|
|
|
const ast = parser.parse(scriptContent, {
|
|
|
|
sourceType: 'module',
|
|
|
|
plugins: ['jsx', 'decorators-legacy', 'classProperties', 'objectRestSpread']
|
|
|
|
})
|
|
|
|
|
|
|
|
return ast
|
|
|
|
} catch (error) {
|
|
|
|
console.warn('解析Vue组件脚本时出错:', error.message)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 在AST中查找API调用和函数调用关系
|
|
|
|
* @param {Object} ast - AST对象
|
|
|
|
* @param {string} serviceName - 服务名称
|
|
|
|
* @param {string} apiMethodName - API方法名称
|
|
|
|
* @param {string} filePath - 组件文件路径
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @returns {Array} API调用信息数组
|
|
|
|
*/
|
|
|
|
findApiCallsWithContext(ast, serviceName, apiMethodName, filePath, componentName) {
|
|
|
|
const apiCalls = []
|
|
|
|
const self = this
|
|
|
|
const functionCallMap = new Map() // 存储函数调用关系
|
|
|
|
|
|
|
|
// 第一遍:收集所有函数定义和调用关系
|
|
|
|
traverse(ast, {
|
|
|
|
// 收集函数定义
|
|
|
|
FunctionDeclaration(path) {
|
|
|
|
if (path.node.id && path.node.id.name) {
|
|
|
|
functionCallMap.set(path.node.id.name, {
|
|
|
|
type: 'function',
|
|
|
|
path: path
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// 收集箭头函数和const函数
|
|
|
|
VariableDeclarator(path) {
|
|
|
|
if (path.node.id && path.node.id.type === 'Identifier' &&
|
|
|
|
path.node.init && path.node.init.type === 'ArrowFunctionExpression') {
|
|
|
|
functionCallMap.set(path.node.id.name, {
|
|
|
|
type: 'arrowFunction',
|
|
|
|
path: path
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// 收集函数调用关系
|
|
|
|
CallExpression(path) {
|
|
|
|
const { node } = path
|
|
|
|
|
|
|
|
// 记录函数调用
|
|
|
|
if (node.callee.type === 'Identifier') {
|
|
|
|
const functionName = node.callee.name
|
|
|
|
const parentFunction = self.findParentFunction(path)
|
|
|
|
if (parentFunction) {
|
|
|
|
if (!functionCallMap.has(functionName)) {
|
|
|
|
functionCallMap.set(functionName, { callers: [] })
|
|
|
|
}
|
|
|
|
const funcInfo = functionCallMap.get(functionName)
|
|
|
|
if (!funcInfo.callers) funcInfo.callers = []
|
|
|
|
funcInfo.callers.push(parentFunction)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 第二遍:查找API调用并分析调用链
|
|
|
|
traverse(ast, {
|
|
|
|
CallExpression(path) {
|
|
|
|
const { node } = path
|
|
|
|
|
|
|
|
// 查找 serviceName.apiMethodName() 调用
|
|
|
|
if (node.callee.type === 'MemberExpression' &&
|
|
|
|
node.callee.object.name === serviceName &&
|
|
|
|
node.callee.property.name === apiMethodName) {
|
|
|
|
|
|
|
|
// 查找调用上下文
|
|
|
|
const context = self.findCallContextWithChain(path, functionCallMap, filePath, componentName)
|
|
|
|
|
|
|
|
apiCalls.push({
|
|
|
|
triggerName: context.triggerName,
|
|
|
|
triggerType: context.triggerType
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return apiCalls
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找父级函数
|
|
|
|
* @param {Object} path - Babel路径对象
|
|
|
|
* @returns {string|null} 父级函数名
|
|
|
|
*/
|
|
|
|
findParentFunction(path) {
|
|
|
|
let currentPath = path.parentPath
|
|
|
|
while (currentPath) {
|
|
|
|
const { node } = currentPath
|
|
|
|
|
|
|
|
if (node.type === 'FunctionDeclaration' && node.id) {
|
|
|
|
return node.id.name
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.type === 'VariableDeclarator' &&
|
|
|
|
node.id && node.id.type === 'Identifier' &&
|
|
|
|
node.init && node.init.type === 'ArrowFunctionExpression') {
|
|
|
|
return node.id.name
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.type === 'ObjectMethod' && node.key) {
|
|
|
|
return node.key.name
|
|
|
|
}
|
|
|
|
|
|
|
|
currentPath = currentPath.parentPath
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找API调用的上下文(包含调用链分析)
|
|
|
|
* @param {Object} path - Babel路径对象
|
|
|
|
* @param {Map} functionCallMap - 函数调用关系映射
|
|
|
|
* @param {string} filePath - 组件文件路径
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @returns {Object} 上下文信息
|
|
|
|
*/
|
|
|
|
findCallContextWithChain(path, functionCallMap, filePath, componentName) {
|
|
|
|
let currentPath = path
|
|
|
|
let triggerName = ''
|
|
|
|
let triggerType = 'function'
|
|
|
|
|
|
|
|
// 向上遍历AST,查找触发源
|
|
|
|
while (currentPath) {
|
|
|
|
const { node } = currentPath
|
|
|
|
|
|
|
|
// 检查是否在Vue 3 Composition API生命周期钩子中
|
|
|
|
if (node.type === 'CallExpression' &&
|
|
|
|
node.callee.type === 'Identifier' &&
|
|
|
|
['onMounted', 'onCreated', 'onBeforeMount', 'onBeforeCreate', 'onUpdated', 'onBeforeUpdate', 'onUnmounted', 'onBeforeUnmount'].includes(node.callee.name)) {
|
|
|
|
triggerName = node.callee.name
|
|
|
|
triggerType = 'lifecycle'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在Vue 3 Composition API的setup方法中
|
|
|
|
if (node.type === 'ObjectMethod' && node.key && node.key.name === 'setup') {
|
|
|
|
triggerName = 'setup'
|
|
|
|
triggerType = 'lifecycle' // setup是Vue 3的生命周期钩子
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在方法定义中
|
|
|
|
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
|
|
|
|
if (node.id && node.id.name) {
|
|
|
|
triggerName = node.id.name
|
|
|
|
triggerType = 'function'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在对象方法中(非setup)
|
|
|
|
if (node.type === 'ObjectMethod') {
|
|
|
|
if (node.key && node.key.name && node.key.name !== 'setup') {
|
|
|
|
triggerName = node.key.name
|
|
|
|
triggerType = 'method'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在箭头函数中
|
|
|
|
if (node.type === 'ArrowFunctionExpression') {
|
|
|
|
// 查找父级的属性名
|
|
|
|
const parent = currentPath.parent
|
|
|
|
if (parent && parent.type === 'ObjectProperty' && parent.key) {
|
|
|
|
const methodName = parent.key.name || ''
|
|
|
|
if (methodName === 'setup') {
|
|
|
|
triggerName = 'setup'
|
|
|
|
triggerType = 'lifecycle'
|
|
|
|
} else {
|
|
|
|
triggerName = methodName
|
|
|
|
triggerType = 'method'
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在const声明的箭头函数中
|
|
|
|
if (parent && parent.type === 'VariableDeclarator' && parent.id) {
|
|
|
|
const functionName = parent.id.name || ''
|
|
|
|
triggerName = functionName
|
|
|
|
triggerType = 'method'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在Vue 2生命周期钩子中
|
|
|
|
if (node.type === 'CallExpression' &&
|
|
|
|
node.callee.type === 'MemberExpression' &&
|
|
|
|
node.callee.object.name === 'this' &&
|
|
|
|
['mounted', 'created', 'beforeMount', 'beforeCreate'].includes(node.callee.property.name)) {
|
|
|
|
triggerName = node.callee.property.name
|
|
|
|
triggerType = 'lifecycle'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
currentPath = currentPath.parentPath
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果找到的是method类型,继续向上追溯找到最终的可视组件
|
|
|
|
if (triggerType === 'method') {
|
|
|
|
const finalContext = this.traceToVisualComponent(path, triggerName, functionCallMap, filePath, componentName)
|
|
|
|
if (finalContext) {
|
|
|
|
return finalContext
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { triggerName, triggerType }
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 追溯method类型的触发源到最终的可视组件
|
|
|
|
* @param {Object} path - Babel路径对象
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @param {Map} functionCallMap - 函数调用关系映射
|
|
|
|
* @param {string} filePath - 组件文件路径
|
|
|
|
* @returns {Object|null} 最终的可视组件上下文
|
|
|
|
*/
|
|
|
|
traceToVisualComponent(path, methodName, functionCallMap, filePath, componentName) {
|
|
|
|
try {
|
|
|
|
// 使用传入的文件路径
|
|
|
|
const componentPath = filePath
|
|
|
|
if (!componentPath) return null
|
|
|
|
|
|
|
|
// 读取组件文件内容
|
|
|
|
const content = readFileSync(componentPath, 'utf-8')
|
|
|
|
|
|
|
|
// 分析模板中的事件绑定
|
|
|
|
const visualContext = this.analyzeTemplateForMethod(content, methodName, componentName, filePath)
|
|
|
|
if (visualContext) {
|
|
|
|
return visualContext
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果模板中没有找到,检查是否有其他方法调用了这个方法
|
|
|
|
const callerContext = this.findMethodCaller(methodName, functionCallMap)
|
|
|
|
if (callerContext) {
|
|
|
|
// 检查调用者是否是生命周期钩子
|
|
|
|
const callerInfo = functionCallMap.get(callerContext)
|
|
|
|
if (callerInfo) {
|
|
|
|
// 检查调用者是否是生命周期钩子
|
|
|
|
if (this.isLifecycleHook(callerContext)) {
|
|
|
|
return {
|
|
|
|
triggerName: '',
|
|
|
|
triggerType: 'page'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 继续递归追溯
|
|
|
|
return this.traceToVisualComponent(path, callerContext, functionCallMap, filePath, componentName)
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
console.warn(`追溯可视组件时出错:`, error.message)
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 检查方法名是否是生命周期钩子
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @returns {boolean} 是否是生命周期钩子
|
|
|
|
*/
|
|
|
|
isLifecycleHook(methodName) {
|
|
|
|
const lifecycleHooks = [
|
|
|
|
'setup',
|
|
|
|
'onMounted',
|
|
|
|
'onCreated',
|
|
|
|
'onBeforeMount',
|
|
|
|
'onBeforeCreate',
|
|
|
|
'onUpdated',
|
|
|
|
'onBeforeUpdate',
|
|
|
|
'onUnmounted',
|
|
|
|
'onBeforeUnmount',
|
|
|
|
'mounted',
|
|
|
|
'created',
|
|
|
|
'beforeMount',
|
|
|
|
'beforeCreate',
|
|
|
|
'updated',
|
|
|
|
'beforeUpdate',
|
|
|
|
'unmounted',
|
|
|
|
'beforeUnmount',
|
|
|
|
'watch'
|
|
|
|
]
|
|
|
|
|
|
|
|
return lifecycleHooks.includes(methodName)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 分析模板中与指定方法相关的事件绑定
|
|
|
|
* @param {string} content - 组件文件内容
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @param {string} filePath - 组件文件路径
|
|
|
|
* @returns {Object|null} 可视组件上下文
|
|
|
|
*/
|
|
|
|
analyzeTemplateForMethod(content, methodName, componentName, filePath) {
|
|
|
|
try {
|
|
|
|
// 使用Vue编译器解析单文件组件
|
|
|
|
const { descriptor } = parse(content)
|
|
|
|
|
|
|
|
if (!descriptor.template) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// 解析模板AST
|
|
|
|
const templateAst = descriptor.template.ast
|
|
|
|
if (!templateAst) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// 遍历模板AST,查找事件绑定
|
|
|
|
return this.findEventBindingInTemplate(templateAst, methodName, componentName, filePath)
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
console.warn(`解析Vue模板时出错:`, error.message)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 在模板AST中查找事件绑定
|
|
|
|
* @param {Object} node - AST节点
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @param {string} filePath - 组件文件路径
|
|
|
|
* @returns {Object|null} 可视组件上下文
|
|
|
|
*/
|
|
|
|
findEventBindingInTemplate(node, methodName, componentName, filePath) {
|
|
|
|
if (!node) return null
|
|
|
|
|
|
|
|
// 检查当前节点是否有事件绑定
|
|
|
|
if (node.props) {
|
|
|
|
for (const prop of node.props) {
|
|
|
|
if (prop.type === 7) { // DIRECTIVE类型
|
|
|
|
const eventName = prop.name
|
|
|
|
if (eventName === 'on' && prop.arg) {
|
|
|
|
const eventType = prop.arg.content
|
|
|
|
if (['click', 'submit', 'change', 'input'].includes(eventType)) {
|
|
|
|
// 检查事件处理函数
|
|
|
|
if (prop.exp && this.isMethodCall(prop.exp, methodName)) {
|
|
|
|
const context = this.createVisualContext(node, eventType, componentName, methodName, filePath)
|
|
|
|
|
|
|
|
// 如果找到的是form,需要查找其中的submit按钮
|
|
|
|
if (context.triggerType === 'form') {
|
|
|
|
const submitButton = this.findSubmitButtonInForm(node, componentName, methodName, filePath)
|
|
|
|
if (submitButton) {
|
|
|
|
return submitButton
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return context
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 递归检查子节点
|
|
|
|
if (node.children) {
|
|
|
|
for (const child of node.children) {
|
|
|
|
if (child.type === 1) { // ELEMENT类型
|
|
|
|
const result = this.findEventBindingInTemplate(child, methodName, componentName, filePath)
|
|
|
|
if (result) return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 在form中查找submit按钮
|
|
|
|
* @param {Object} formNode - form AST节点
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @returns {Object|null} submit按钮上下文
|
|
|
|
*/
|
|
|
|
findSubmitButtonInForm(formNode, componentName, methodName, filePath) {
|
|
|
|
if (!formNode || !formNode.children) return null
|
|
|
|
|
|
|
|
// 递归查找submit按钮
|
|
|
|
for (const child of formNode.children) {
|
|
|
|
if (child.type === 1) { // ELEMENT类型
|
|
|
|
if (child.tag === 'button') {
|
|
|
|
// 检查是否是submit按钮
|
|
|
|
const props = child.props || []
|
|
|
|
const attributes = {}
|
|
|
|
props.forEach(prop => {
|
|
|
|
if (prop.type === 6) { // ATTRIBUTE
|
|
|
|
attributes[prop.name] = prop.value ? prop.value.content : ''
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (attributes.type === 'submit') {
|
|
|
|
let triggerName = attributes.name
|
|
|
|
if (!triggerName) {
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
|
|
|
|
// 自动为Vue文件添加name属性
|
|
|
|
this.addNameAttributeToVueFile(filePath, child, triggerName)
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
triggerName: triggerName,
|
|
|
|
triggerType: 'button'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 递归查找子元素
|
|
|
|
const result = this.findSubmitButtonInForm(child, componentName, methodName, filePath)
|
|
|
|
if (result) return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 检查表达式是否是方法调用
|
|
|
|
* @param {Object} exp - 表达式节点
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @returns {boolean} 是否是方法调用
|
|
|
|
*/
|
|
|
|
isMethodCall(exp, methodName) {
|
|
|
|
if (!exp) return false
|
|
|
|
|
|
|
|
// 简单的方法调用:methodName
|
|
|
|
if (exp.type === 4 && exp.content === methodName) { // SIMPLE_EXPRESSION
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// 带参数的方法调用:methodName(...) - 检查content是否以methodName开头
|
|
|
|
if (exp.type === 4 && exp.content && exp.content.startsWith(methodName + '(')) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// 带参数的方法调用:methodName(...)
|
|
|
|
if (exp.type === 8) { // COMPOUND_EXPRESSION
|
|
|
|
const children = exp.children
|
|
|
|
if (children && children.length >= 1) {
|
|
|
|
const firstChild = children[0]
|
|
|
|
if (firstChild.type === 4 && firstChild.content === methodName) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查AST中的方法调用
|
|
|
|
if (exp.ast) {
|
|
|
|
return this.checkAstMethodCall(exp.ast, methodName)
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 检查AST中的方法调用
|
|
|
|
* @param {Object} ast - AST节点
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @returns {boolean} 是否是方法调用
|
|
|
|
*/
|
|
|
|
checkAstMethodCall(ast, methodName) {
|
|
|
|
if (!ast) return false
|
|
|
|
|
|
|
|
// 简单标识符:methodName
|
|
|
|
if (ast.type === 'Identifier' && ast.name === methodName) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// 方法调用:methodName(...)
|
|
|
|
if (ast.type === 'CallExpression') {
|
|
|
|
if (ast.callee && ast.callee.type === 'Identifier' && ast.callee.name === methodName) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 创建可视组件上下文
|
|
|
|
* @param {Object} node - AST节点
|
|
|
|
* @param {string} eventType - 事件类型
|
|
|
|
* @param {string} componentName - 组件名称
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @param {string} filePath - 组件文件路径
|
|
|
|
* @returns {Object} 可视组件上下文
|
|
|
|
*/
|
|
|
|
createVisualContext(node, eventType, componentName, methodName, filePath) {
|
|
|
|
const tag = node.tag
|
|
|
|
const props = node.props || []
|
|
|
|
|
|
|
|
// 提取元素属性
|
|
|
|
const attributes = {}
|
|
|
|
props.forEach(prop => {
|
|
|
|
if (prop.type === 6) { // ATTRIBUTE
|
|
|
|
attributes[prop.name] = prop.value ? prop.value.content : ''
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 根据标签类型确定组件类型和名称
|
|
|
|
if (tag === 'button') {
|
|
|
|
let triggerName = attributes.name
|
|
|
|
if (!triggerName) {
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
|
|
|
|
// 自动为Vue文件添加name属性
|
|
|
|
this.addNameAttributeToVueFile(filePath, node, triggerName)
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
triggerName: triggerName,
|
|
|
|
triggerType: 'button'
|
|
|
|
}
|
|
|
|
} else if (tag === 'form') {
|
|
|
|
let triggerName = attributes.name
|
|
|
|
if (!triggerName) {
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
triggerName: triggerName,
|
|
|
|
triggerType: 'form'
|
|
|
|
}
|
|
|
|
} else if (tag === 'input') {
|
|
|
|
let triggerName = attributes.name
|
|
|
|
if (!triggerName) {
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
triggerName: triggerName,
|
|
|
|
triggerType: 'input'
|
|
|
|
}
|
|
|
|
} else if (tag === 'a') {
|
|
|
|
let triggerName = attributes.name
|
|
|
|
if (!triggerName) {
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
triggerName: triggerName,
|
|
|
|
triggerType: 'link'
|
|
|
|
}
|
|
|
|
} else if (tag === 'i') {
|
|
|
|
let triggerName = attributes.name
|
|
|
|
if (!triggerName) {
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
triggerName: triggerName,
|
|
|
|
triggerType: 'icon'
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let triggerName = attributes.name
|
|
|
|
if (!triggerName) {
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
triggerName: triggerName,
|
|
|
|
triggerType: 'element'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 生成唯一的尾缀
|
|
|
|
* @returns {string} 唯一尾缀
|
|
|
|
*/
|
|
|
|
generateUniqueSuffix() {
|
|
|
|
// 生成6位随机字符串,类似 "2m2snp"
|
|
|
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
|
|
let result = ''
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 为Vue文件中的button添加name属性
|
|
|
|
* @param {string} filePath - Vue文件路径
|
|
|
|
* @param {Object} node - AST节点
|
|
|
|
* @param {string} nameValue - name属性值
|
|
|
|
*/
|
|
|
|
addNameAttributeToVueFile(filePath, node, nameValue) {
|
|
|
|
try {
|
|
|
|
// 读取Vue文件内容
|
|
|
|
const content = readFileSync(filePath, 'utf-8')
|
|
|
|
|
|
|
|
// 获取节点的位置信息
|
|
|
|
const loc = node.loc
|
|
|
|
if (!loc) {
|
|
|
|
console.warn(`节点没有位置信息,无法添加name属性`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// 找到button标签的开始位置
|
|
|
|
const startLine = loc.start.line
|
|
|
|
const startColumn = loc.start.column
|
|
|
|
|
|
|
|
console.log(`尝试为第${startLine}行第${startColumn}列的button添加name属性: ${nameValue}`)
|
|
|
|
|
|
|
|
// 将内容按行分割
|
|
|
|
const lines = content.split('\n')
|
|
|
|
|
|
|
|
// 找到对应的行
|
|
|
|
if (startLine > 0 && startLine <= lines.length) {
|
|
|
|
const lineIndex = startLine - 1
|
|
|
|
let line = lines[lineIndex]
|
|
|
|
|
|
|
|
console.log(`当前行内容: ${line}`)
|
|
|
|
|
|
|
|
// 在button标签中添加name属性
|
|
|
|
// 查找button标签的开始位置
|
|
|
|
const buttonStartMatch = line.match(/<button[^>]*>/)
|
|
|
|
if (buttonStartMatch) {
|
|
|
|
// 如果button标签在同一行结束
|
|
|
|
const buttonEndIndex = buttonStartMatch.index + buttonStartMatch[0].length
|
|
|
|
const beforeButton = line.substring(0, buttonEndIndex - 1) // 去掉最后的>
|
|
|
|
const afterButton = line.substring(buttonEndIndex - 1)
|
|
|
|
|
|
|
|
// 添加name属性
|
|
|
|
const newLine = `${beforeButton} name="${nameValue}">${afterButton}`
|
|
|
|
lines[lineIndex] = newLine
|
|
|
|
|
|
|
|
// 写回文件
|
|
|
|
const newContent = lines.join('\n')
|
|
|
|
writeFileSync(filePath, newContent, 'utf-8')
|
|
|
|
|
|
|
|
console.log(`已为 ${filePath} 中的button添加name属性: ${nameValue}`)
|
|
|
|
} else {
|
|
|
|
// 如果button标签跨行,查找button开始标签
|
|
|
|
const buttonStartMatch = line.match(/<button[^>]*$/)
|
|
|
|
if (buttonStartMatch) {
|
|
|
|
// 在button开始标签后添加name属性
|
|
|
|
const beforeButton = line.substring(0, buttonStartMatch.index + buttonStartMatch[0].length)
|
|
|
|
const afterButton = line.substring(buttonStartMatch.index + buttonStartMatch[0].length)
|
|
|
|
|
|
|
|
// 添加name属性
|
|
|
|
const newLine = `${beforeButton} name="${nameValue}"${afterButton}`
|
|
|
|
lines[lineIndex] = newLine
|
|
|
|
|
|
|
|
// 写回文件
|
|
|
|
const newContent = lines.join('\n')
|
|
|
|
writeFileSync(filePath, newContent, 'utf-8')
|
|
|
|
|
|
|
|
console.log(`已为 ${filePath} 中的跨行button添加name属性: ${nameValue}`)
|
|
|
|
} else {
|
|
|
|
console.warn(`未找到button标签`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn(`行号超出范围: ${startLine}`)
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.warn(`为Vue文件添加name属性时出错:`, error.message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找方法的调用者
|
|
|
|
*/
|
|
|
|
findMethodCaller(methodName, functionCallMap) {
|
|
|
|
const funcInfo = functionCallMap.get(methodName)
|
|
|
|
if (funcInfo && funcInfo.callers && funcInfo.callers.length > 0) {
|
|
|
|
return funcInfo.callers[0] // 返回第一个调用者
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 查找API调用的上下文
|
|
|
|
* @param {Object} path - Babel路径对象
|
|
|
|
* @returns {Object} 上下文信息
|
|
|
|
*/
|
|
|
|
findCallContext(path) {
|
|
|
|
let currentPath = path
|
|
|
|
let triggerName = ''
|
|
|
|
let triggerType = 'function'
|
|
|
|
|
|
|
|
// 向上遍历AST,查找触发源
|
|
|
|
while (currentPath) {
|
|
|
|
const { node } = currentPath
|
|
|
|
|
|
|
|
// 检查是否在Vue 3 Composition API生命周期钩子中
|
|
|
|
if (node.type === 'CallExpression' &&
|
|
|
|
node.callee.type === 'Identifier' &&
|
|
|
|
['onMounted', 'onCreated', 'onBeforeMount', 'onBeforeCreate', 'onUpdated', 'onBeforeUpdate', 'onUnmounted', 'onBeforeUnmount'].includes(node.callee.name)) {
|
|
|
|
triggerName = node.callee.name
|
|
|
|
triggerType = 'lifecycle'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在Vue 3 Composition API的setup方法中
|
|
|
|
if (node.type === 'ObjectMethod' && node.key && node.key.name === 'setup') {
|
|
|
|
triggerName = 'setup'
|
|
|
|
triggerType = 'lifecycle' // setup是Vue 3的生命周期钩子
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在方法定义中
|
|
|
|
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
|
|
|
|
if (node.id && node.id.name) {
|
|
|
|
triggerName = node.id.name
|
|
|
|
triggerType = 'function'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在对象方法中(非setup)
|
|
|
|
if (node.type === 'ObjectMethod') {
|
|
|
|
if (node.key && node.key.name && node.key.name !== 'setup') {
|
|
|
|
triggerName = node.key.name
|
|
|
|
triggerType = 'method'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在箭头函数中
|
|
|
|
if (node.type === 'ArrowFunctionExpression') {
|
|
|
|
// 查找父级的属性名
|
|
|
|
const parent = currentPath.parent
|
|
|
|
if (parent && parent.type === 'ObjectProperty' && parent.key) {
|
|
|
|
const methodName = parent.key.name || ''
|
|
|
|
if (methodName === 'setup') {
|
|
|
|
triggerName = 'setup'
|
|
|
|
triggerType = 'lifecycle'
|
|
|
|
} else {
|
|
|
|
triggerName = methodName
|
|
|
|
triggerType = 'method'
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 检查是否在Vue 2生命周期钩子中
|
|
|
|
if (node.type === 'CallExpression' &&
|
|
|
|
node.callee.type === 'MemberExpression' &&
|
|
|
|
node.callee.object.name === 'this' &&
|
|
|
|
['mounted', 'created', 'beforeMount', 'beforeCreate'].includes(node.callee.property.name)) {
|
|
|
|
triggerName = node.callee.property.name
|
|
|
|
triggerType = 'lifecycle'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
currentPath = currentPath.parentPath
|
|
|
|
}
|
|
|
|
|
|
|
|
return { triggerName, triggerType }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = TriggerAnalyzer
|