10 changed files with 1617 additions and 71 deletions
@ -0,0 +1,537 @@ |
|||||||
|
const { readFileSync, existsSync } = require('fs') |
||||||
|
const { resolve } = require('path') |
||||||
|
const { parse } = require('@vue/compiler-dom') |
||||||
|
const parser = require('@babel/parser') |
||||||
|
const traverse = require('@babel/traverse').default |
||||||
|
|
||||||
|
/** |
||||||
|
* 调用链追溯器 |
||||||
|
* 专门用于追溯method类型的触发源到最终的可视控件 |
||||||
|
*/ |
||||||
|
class CallChainTracer { |
||||||
|
constructor() { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 追溯所有method类型的触发源到可视控件 |
||||||
|
* @param {Array} apiMappings - API映射数组 |
||||||
|
* @returns {Array} 增强的API映射数组 |
||||||
|
*/ |
||||||
|
traceMethodTriggersToVisualComponents(apiMappings) { |
||||||
|
const enhancedApiMappings = apiMappings.map(module => { |
||||||
|
const enhancedApiMappings = module.apiMappings.map(api => { |
||||||
|
if (!api.triggerSources || api.triggerSources.length === 0) { |
||||||
|
return api |
||||||
|
} |
||||||
|
|
||||||
|
// 只处理triggerType为method的触发源
|
||||||
|
const enhancedTriggerSources = api.triggerSources.map(trigger => { |
||||||
|
if (trigger.triggerType === 'method') { |
||||||
|
return this.traceMethodToVisualComponent( |
||||||
|
trigger, |
||||||
|
module.module, |
||||||
|
module.serviceName, |
||||||
|
api.apiMethodName |
||||||
|
) |
||||||
|
} |
||||||
|
return trigger |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
...api, |
||||||
|
triggerSources: enhancedTriggerSources |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
...module, |
||||||
|
apiMappings: enhancedApiMappings |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return enhancedApiMappings |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 追溯单个method到可视控件 |
||||||
|
* @param {Object} trigger - 触发源对象 |
||||||
|
* @param {string} moduleName - 模块名称 |
||||||
|
* @param {string} serviceName - 服务名称 |
||||||
|
* @param {string} apiMethodName - API方法名称 |
||||||
|
* @returns {Object} 增强的触发源对象 |
||||||
|
*/ |
||||||
|
traceMethodToVisualComponent(trigger, moduleName, serviceName, apiMethodName) { |
||||||
|
try { |
||||||
|
// 查找组件文件
|
||||||
|
const componentPath = this.findComponentFile(trigger.component, moduleName) |
||||||
|
if (!componentPath) { |
||||||
|
return trigger |
||||||
|
} |
||||||
|
|
||||||
|
const content = readFileSync(componentPath, 'utf-8') |
||||||
|
|
||||||
|
// 解析Vue组件
|
||||||
|
const ast = this.parseVueComponent(content) |
||||||
|
if (!ast) return trigger |
||||||
|
|
||||||
|
// 建立函数调用关系映射
|
||||||
|
const functionCallMap = this.buildFunctionCallMap(ast) |
||||||
|
|
||||||
|
// 追溯method到可视控件
|
||||||
|
const visualContext = this.traceToVisualComponent( |
||||||
|
trigger.triggerName, |
||||||
|
functionCallMap, |
||||||
|
componentPath, |
||||||
|
trigger.component |
||||||
|
) |
||||||
|
|
||||||
|
if (visualContext) { |
||||||
|
return { |
||||||
|
...trigger, |
||||||
|
triggerName: visualContext.triggerName, |
||||||
|
triggerType: visualContext.triggerType |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return trigger |
||||||
|
} catch (error) { |
||||||
|
return trigger |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 查找组件文件 |
||||||
|
* @param {string} componentName - 组件名称 |
||||||
|
* @param {string} moduleName - 模块名称 |
||||||
|
* @returns {string|null} 组件文件路径 |
||||||
|
*/ |
||||||
|
findComponentFile(componentName, moduleName) { |
||||||
|
// 优先在指定模块中查找
|
||||||
|
if (moduleName) { |
||||||
|
// 检查views目录
|
||||||
|
const viewPath = resolve(__dirname, `../../src/renderer/modules/${moduleName}/views/${componentName}.vue`) |
||||||
|
if (existsSync(viewPath)) { |
||||||
|
return viewPath |
||||||
|
} |
||||||
|
|
||||||
|
// 检查components目录
|
||||||
|
const componentPath = resolve(__dirname, `../../src/renderer/modules/${moduleName}/components/${componentName}.vue`) |
||||||
|
if (existsSync(componentPath)) { |
||||||
|
return componentPath |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析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', 'typescript'] |
||||||
|
}) |
||||||
|
|
||||||
|
return ast |
||||||
|
} catch (error) { |
||||||
|
return null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 建立函数调用关系映射 |
||||||
|
* @param {Object} ast - AST对象 |
||||||
|
* @returns {Map} 函数调用关系映射 |
||||||
|
*/ |
||||||
|
buildFunctionCallMap(ast) { |
||||||
|
const functionCallMap = new Map() |
||||||
|
const self = this |
||||||
|
|
||||||
|
traverse(ast, { |
||||||
|
CallExpression(path) { |
||||||
|
const { node } = path |
||||||
|
if (node.callee && 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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return functionCallMap |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 查找父级函数 |
||||||
|
* @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 |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 追溯method到可视控件 |
||||||
|
* @param {string} methodName - 方法名称 |
||||||
|
* @param {Map} functionCallMap - 函数调用关系映射 |
||||||
|
* @param {string} filePath - 组件文件路径 |
||||||
|
* @param {string} componentName - 组件名称 |
||||||
|
* @returns {Object|null} 可视控件上下文 |
||||||
|
*/ |
||||||
|
traceToVisualComponent(methodName, functionCallMap, filePath, componentName) { |
||||||
|
try { |
||||||
|
// 读取组件文件内容
|
||||||
|
const content = readFileSync(filePath, 'utf-8') |
||||||
|
|
||||||
|
// 分析模板中的事件绑定
|
||||||
|
const visualContext = this.analyzeTemplateForMethod(content, methodName, componentName, filePath) |
||||||
|
if (visualContext) { |
||||||
|
return visualContext |
||||||
|
} |
||||||
|
|
||||||
|
// 如果模板中没有找到,检查是否有其他方法调用了这个方法
|
||||||
|
const callerContext = this.findMethodCaller(methodName, functionCallMap) |
||||||
|
if (callerContext) { |
||||||
|
// 检查调用者是否是生命周期钩子
|
||||||
|
if (this.isLifecycleHook(callerContext)) { |
||||||
|
return { |
||||||
|
triggerName: '', |
||||||
|
triggerType: 'page' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 继续递归追溯
|
||||||
|
return this.traceToVisualComponent(callerContext, functionCallMap, filePath, componentName) |
||||||
|
} |
||||||
|
|
||||||
|
} catch (error) { |
||||||
|
// 静默处理错误
|
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 分析模板中的方法调用 |
||||||
|
* @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) { |
||||||
|
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 - 方法名称 |
||||||
|
* @param {string} filePath - 组件文件路径 |
||||||
|
* @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()}-submit-${this.generateUniqueSuffix()}` |
||||||
|
} |
||||||
|
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 : '' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
let triggerName = attributes.name || attributes.id || '' |
||||||
|
let triggerType = 'button' // 默认类型
|
||||||
|
|
||||||
|
// 根据标签类型确定触发源类型
|
||||||
|
if (tag === 'form') { |
||||||
|
triggerType = 'form' |
||||||
|
} else if (tag === 'button') { |
||||||
|
triggerType = 'button' |
||||||
|
} else if (tag === 'input') { |
||||||
|
triggerType = 'input' |
||||||
|
} else if (tag === 'select') { |
||||||
|
triggerType = 'select' |
||||||
|
} else if (tag === 'textarea') { |
||||||
|
triggerType = 'textarea' |
||||||
|
} else { |
||||||
|
// 其他元素类型
|
||||||
|
triggerType = 'element' |
||||||
|
} |
||||||
|
|
||||||
|
// 如果没有名称,生成一个
|
||||||
|
if (!triggerName) { |
||||||
|
triggerName = `${componentName.toLowerCase()}-${tag}-${this.generateUniqueSuffix()}` |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
triggerName: triggerName, |
||||||
|
triggerType: triggerType |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 查找方法的调用者 |
||||||
|
* @param {string} methodName - 方法名称 |
||||||
|
* @param {Map} functionCallMap - 函数调用关系映射 |
||||||
|
* @returns {string|null} 调用者方法名 |
||||||
|
*/ |
||||||
|
findMethodCaller(methodName, functionCallMap) { |
||||||
|
const funcInfo = functionCallMap.get(methodName) |
||||||
|
if (funcInfo && funcInfo.callers && funcInfo.callers.length > 0) { |
||||||
|
// 返回第一个调用者
|
||||||
|
return funcInfo.callers[0] |
||||||
|
} |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 检查方法名是否是生命周期钩子 |
||||||
|
* @param {string} methodName - 方法名称 |
||||||
|
* @returns {boolean} 是否是生命周期钩子 |
||||||
|
*/ |
||||||
|
isLifecycleHook(methodName) { |
||||||
|
const lifecycleHooks = [ |
||||||
|
'onMounted', 'onCreated', 'onBeforeMount', 'onBeforeCreate', |
||||||
|
'onUpdated', 'onBeforeUpdate', 'onUnmounted', 'onBeforeUnmount', |
||||||
|
'mounted', 'created', 'beforeMount', 'beforeCreate', |
||||||
|
'updated', 'beforeUpdate', 'unmounted', 'beforeUnmount', |
||||||
|
'setup' |
||||||
|
] |
||||||
|
return lifecycleHooks.includes(methodName) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 生成唯一后缀 |
||||||
|
* @returns {string} 唯一后缀 |
||||||
|
*/ |
||||||
|
generateUniqueSuffix() { |
||||||
|
return Math.random().toString(36).substr(2, 6) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = CallChainTracer |
@ -0,0 +1,168 @@ |
|||||||
|
const { readFileSync, writeFileSync } = require('fs') |
||||||
|
const path = require('path') |
||||||
|
|
||||||
|
/** |
||||||
|
* 数据清理器 |
||||||
|
* 清理direct-route-mappings.js中的数据结构 |
||||||
|
*/ |
||||||
|
class DataCleaner { |
||||||
|
constructor() { |
||||||
|
this.mappingsPath = path.join(__dirname, '../../src/renderer/modules/route-sync/direct-route-mappings.js') |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 清理数据结构 |
||||||
|
*/ |
||||||
|
cleanData() { |
||||||
|
try { |
||||||
|
// 读取direct-route-mappings.js
|
||||||
|
const content = readFileSync(this.mappingsPath, 'utf-8') |
||||||
|
|
||||||
|
// 解析mappings内容
|
||||||
|
const mappings = this.parseMappings(content) |
||||||
|
|
||||||
|
// 清理数据
|
||||||
|
const cleanedMappings = this.cleanMappings(mappings) |
||||||
|
|
||||||
|
// 生成新的文件内容
|
||||||
|
const newContent = this.generateNewContent(cleanedMappings) |
||||||
|
|
||||||
|
// 写回文件
|
||||||
|
writeFileSync(this.mappingsPath, newContent, 'utf-8') |
||||||
|
|
||||||
|
// 返回清理后的数据,供后续处理
|
||||||
|
return cleanedMappings |
||||||
|
|
||||||
|
} catch (error) { |
||||||
|
console.error('❌ 清理数据结构失败:', error.message) |
||||||
|
return null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析mappings内容 |
||||||
|
*/ |
||||||
|
parseMappings(content) { |
||||||
|
// 提取export default部分,文件可能没有以分号结尾
|
||||||
|
const mappingsMatch = content.match(/export default ([\s\S]*?)(?:;|$)/) |
||||||
|
if (!mappingsMatch) { |
||||||
|
throw new Error('无法解析direct-route-mappings.js') |
||||||
|
} |
||||||
|
|
||||||
|
// 使用eval解析(在生产环境中应该使用更安全的方式)
|
||||||
|
const mappings = eval(`(${mappingsMatch[1]})`) |
||||||
|
return mappings |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 清理mappings数据 |
||||||
|
*/ |
||||||
|
cleanMappings(mappings) { |
||||||
|
// 保持原有的结构,只清理apiMappings部分
|
||||||
|
const cleanedMappings = { |
||||||
|
routes: mappings.routes || [], |
||||||
|
apiMappings: [] |
||||||
|
} |
||||||
|
|
||||||
|
if (!mappings.apiMappings || mappings.apiMappings.length === 0) { |
||||||
|
return cleanedMappings |
||||||
|
} |
||||||
|
|
||||||
|
mappings.apiMappings.forEach(module => { |
||||||
|
if (!module.apiMappings || module.apiMappings.length === 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const cleanedApiMappings = [] |
||||||
|
|
||||||
|
module.apiMappings.forEach(api => { |
||||||
|
if (!api.triggerSources || api.triggerSources.length === 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// 1. 只保留triggerType为button的triggerSources
|
||||||
|
const buttonTriggerSources = api.triggerSources.filter(trigger => { |
||||||
|
return trigger.triggerType === 'button' |
||||||
|
}) |
||||||
|
|
||||||
|
// 2. 如果还有button类型的triggerSources,保留这个API
|
||||||
|
if (buttonTriggerSources.length > 0) { |
||||||
|
const cleanedApi = { |
||||||
|
...api, |
||||||
|
triggerSources: buttonTriggerSources |
||||||
|
} |
||||||
|
cleanedApiMappings.push(cleanedApi) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 3. 如果还有apiMappings,保留这个模块
|
||||||
|
if (cleanedApiMappings.length > 0) { |
||||||
|
const cleanedModule = { |
||||||
|
...module, |
||||||
|
apiMappings: cleanedApiMappings |
||||||
|
} |
||||||
|
cleanedMappings.apiMappings.push(cleanedModule) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return cleanedMappings |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 生成新的文件内容 |
||||||
|
*/ |
||||||
|
generateNewContent(cleanedMappings) { |
||||||
|
const header = `// 路由映射数据 - 构建时生成
|
||||||
|
export default ${JSON.stringify(cleanedMappings, null, 2)};` |
||||||
|
|
||||||
|
return header |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 统计清理结果 |
||||||
|
*/ |
||||||
|
getCleanupStats(originalMappings, cleanedMappings) { |
||||||
|
const stats = { |
||||||
|
original: { |
||||||
|
modules: originalMappings.length, |
||||||
|
apis: 0, |
||||||
|
triggerSources: 0, |
||||||
|
buttonTriggerSources: 0 |
||||||
|
}, |
||||||
|
cleaned: { |
||||||
|
modules: cleanedMappings.length, |
||||||
|
apis: 0, |
||||||
|
triggerSources: 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 统计原始数据
|
||||||
|
originalMappings.forEach(module => { |
||||||
|
if (module.apiMappings) { |
||||||
|
stats.original.apis += module.apiMappings.length |
||||||
|
module.apiMappings.forEach(api => { |
||||||
|
if (api.triggerSources) { |
||||||
|
stats.original.triggerSources += api.triggerSources.length |
||||||
|
stats.original.buttonTriggerSources += api.triggerSources.filter(t => t.triggerType === 'button').length |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 统计清理后数据
|
||||||
|
cleanedMappings.forEach(module => { |
||||||
|
if (module.apiMappings) { |
||||||
|
stats.cleaned.apis += module.apiMappings.length |
||||||
|
module.apiMappings.forEach(api => { |
||||||
|
if (api.triggerSources) { |
||||||
|
stats.cleaned.triggerSources += api.triggerSources.length |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return stats |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = DataCleaner |
@ -0,0 +1,453 @@ |
|||||||
|
const { readFileSync, existsSync, readdirSync } = require('fs') |
||||||
|
const { resolve } = require('path') |
||||||
|
const { parse } = require('@vue/compiler-dom') |
||||||
|
const parser = require('@babel/parser') |
||||||
|
const traverse = require('@babel/traverse').default |
||||||
|
|
||||||
|
/** |
||||||
|
* 简化版Trigger分析器 |
||||||
|
* 从原有trigger-analyzer中提取核心逻辑 |
||||||
|
*/ |
||||||
|
class TriggerAnalyzerSimple { |
||||||
|
constructor() { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 为API映射查找触发源 |
||||||
|
* 使用Babel插件追溯API调用控件,记录控件的类型和名称 |
||||||
|
* @param {Array} apiMappings - API映射数组 |
||||||
|
* @param {Array} routes - 路由数组 |
||||||
|
* @returns {Array} 增强的API映射数组 |
||||||
|
*/ |
||||||
|
findTriggerSourcesForApiMappings(apiMappings, routes) { |
||||||
|
const enhancedApiMappings = apiMappings.map(module => { |
||||||
|
const enhancedApiMappings = module.apiMappings.map(api => { |
||||||
|
// 查找该API的触发源
|
||||||
|
const triggerSources = this.findTriggerSourcesForApiMethod( |
||||||
|
module.serviceName, |
||||||
|
api.apiMethodName, |
||||||
|
module.module |
||||||
|
) |
||||||
|
|
||||||
|
return { |
||||||
|
...api, |
||||||
|
triggerSources: triggerSources |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
...module, |
||||||
|
apiMappings: enhancedApiMappings |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return enhancedApiMappings |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 查找API方法的触发源 |
||||||
|
* @param {string} serviceName - 服务名称 |
||||||
|
* @param {string} apiMethodName - API方法名称 |
||||||
|
* @param {string} moduleName - 模块名称 |
||||||
|
* @returns {Array} 触发源数组 |
||||||
|
*/ |
||||||
|
findTriggerSourcesForApiMethod(serviceName, apiMethodName, moduleName) { |
||||||
|
const triggerSources = [] |
||||||
|
|
||||||
|
// 检查模块中的所有组件
|
||||||
|
const componentsInModule = this.getModuleComponents(moduleName) |
||||||
|
componentsInModule.forEach(componentName => { |
||||||
|
const componentTriggerSources = this.analyzeComponentForApiCalls( |
||||||
|
componentName, |
||||||
|
serviceName, |
||||||
|
apiMethodName, |
||||||
|
moduleName |
||||||
|
) |
||||||
|
triggerSources.push(...componentTriggerSources) |
||||||
|
}) |
||||||
|
|
||||||
|
// 去重
|
||||||
|
return this.deduplicateTriggerSources(triggerSources) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 获取模块中的所有组件 |
||||||
|
* @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) { |
||||||
|
console.warn(`获取模块组件时出错: ${error.message}`) |
||||||
|
} |
||||||
|
|
||||||
|
return components |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 分析组件中的API调用 |
||||||
|
* @param {string} componentName - 组件名称 |
||||||
|
* @param {string} serviceName - 服务名称 |
||||||
|
* @param {string} apiMethodName - API方法名称 |
||||||
|
* @param {string} moduleName - 模块名称 |
||||||
|
* @returns {Array} 触发源数组 |
||||||
|
*/ |
||||||
|
analyzeComponentForApiCalls(componentName, serviceName, apiMethodName, moduleName) { |
||||||
|
const triggerSources = [] |
||||||
|
|
||||||
|
try { |
||||||
|
// 查找组件文件,优先在指定模块中查找
|
||||||
|
const componentPath = this.findComponentFile(componentName, moduleName) |
||||||
|
if (!componentPath) { |
||||||
|
return triggerSources |
||||||
|
} |
||||||
|
|
||||||
|
// 检查组件的authType,如果是瀑布理财则跳过
|
||||||
|
const authType = this.getComponentAuthType(componentName, moduleName) |
||||||
|
if (authType === '瀑布理财') { |
||||||
|
return triggerSources |
||||||
|
} |
||||||
|
|
||||||
|
const content = readFileSync(componentPath, 'utf-8') |
||||||
|
|
||||||
|
// 使用Babel解析Vue组件
|
||||||
|
const ast = this.parseVueComponent(content) |
||||||
|
if (!ast) return triggerSources |
||||||
|
|
||||||
|
// 查找API调用和函数调用关系
|
||||||
|
const apiCalls = this.findApiCallsWithContext(ast, serviceName, apiMethodName, componentPath, componentName) |
||||||
|
|
||||||
|
// 为每个API调用创建触发源
|
||||||
|
apiCalls.forEach(call => { |
||||||
|
triggerSources.push({ |
||||||
|
component: componentName, |
||||||
|
triggerName: call.triggerName || null, // 没有名称则为null
|
||||||
|
triggerType: call.triggerType || 'function' |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
} catch (error) { |
||||||
|
// 静默处理错误
|
||||||
|
} |
||||||
|
|
||||||
|
return triggerSources |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 查找组件文件 |
||||||
|
* @param {string} componentName - 组件名称 |
||||||
|
* @param {string} moduleName - 模块名称(可选,如果提供则优先在该模块中查找) |
||||||
|
* @returns {string|null} 组件文件路径 |
||||||
|
*/ |
||||||
|
findComponentFile(componentName, moduleName = null) { |
||||||
|
// 如果提供了模块名称,优先在该模块中查找
|
||||||
|
if (moduleName) { |
||||||
|
// 检查views目录
|
||||||
|
const viewPath = resolve(__dirname, `../../src/renderer/modules/${moduleName}/views/${componentName}.vue`) |
||||||
|
if (existsSync(viewPath)) { |
||||||
|
return viewPath |
||||||
|
} |
||||||
|
|
||||||
|
// 检查components目录
|
||||||
|
const componentPath = resolve(__dirname, `../../src/renderer/modules/${moduleName}/components/${componentName}.vue`) |
||||||
|
if (existsSync(componentPath)) { |
||||||
|
return componentPath |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 如果没有找到或没有提供模块名称,则在所有模块中查找
|
||||||
|
// 这里我们动态获取所有模块目录
|
||||||
|
const modulesPath = resolve(__dirname, '../../src/renderer/modules') |
||||||
|
if (existsSync(modulesPath)) { |
||||||
|
const { readdirSync } = require('fs') |
||||||
|
const moduleDirs = readdirSync(modulesPath, { withFileTypes: true }) |
||||||
|
.filter(dirent => dirent.isDirectory()) |
||||||
|
.map(dirent => dirent.name) |
||||||
|
|
||||||
|
for (const moduleDir of moduleDirs) { |
||||||
|
// 检查views目录
|
||||||
|
const viewPath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}/views/${componentName}.vue`) |
||||||
|
if (existsSync(viewPath)) { |
||||||
|
return viewPath |
||||||
|
} |
||||||
|
|
||||||
|
// 检查components目录
|
||||||
|
const componentPath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}/components/${componentName}.vue`) |
||||||
|
if (existsSync(componentPath)) { |
||||||
|
return componentPath |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 从Vue组件文件中获取authType属性 |
||||||
|
* @param {string} componentName - 组件名称 |
||||||
|
* @param {string} moduleName - 模块名称(可选) |
||||||
|
* @returns {string|null} authType值 |
||||||
|
*/ |
||||||
|
getComponentAuthType(componentName, moduleName = null) { |
||||||
|
try { |
||||||
|
const componentPath = this.findComponentFile(componentName, moduleName) |
||||||
|
if (componentPath) { |
||||||
|
const content = readFileSync(componentPath, 'utf-8') |
||||||
|
const authTypeMatch = content.match(/<template\s+authType\s*=\s*["']([^"']+)["']/) |
||||||
|
if (authTypeMatch) { |
||||||
|
return authTypeMatch[1] |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
// 静默处理错误
|
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析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', 'typescript'] |
||||||
|
}) |
||||||
|
|
||||||
|
return ast |
||||||
|
} catch (error) { |
||||||
|
return null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 查找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, { |
||||||
|
CallExpression(path) { |
||||||
|
const { node } = path |
||||||
|
if (node.callee && 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.findCallContext(path) |
||||||
|
|
||||||
|
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路径对象 |
||||||
|
* @returns {Object} 上下文信息 |
||||||
|
*/ |
||||||
|
findCallContext(path) { |
||||||
|
let currentPath = path |
||||||
|
let triggerName = null |
||||||
|
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' |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
return { triggerName, triggerType } |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 去重触发源 |
||||||
|
* @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 |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
module.exports = TriggerAnalyzerSimple |
@ -0,0 +1,160 @@ |
|||||||
|
/** |
||||||
|
* 权限控制指令 |
||||||
|
* 监听button的可用状态变化,进行权限校验 |
||||||
|
*/ |
||||||
|
|
||||||
|
// 权限状态存储
|
||||||
|
const permissionStates = new Map() |
||||||
|
const buttonStates = new Map() |
||||||
|
|
||||||
|
/** |
||||||
|
* 设置用户权限 |
||||||
|
* @param {Object} permissions - 权限对象 |
||||||
|
*/ |
||||||
|
export function setUserPermissions(permissions) { |
||||||
|
// 清除旧的权限状态
|
||||||
|
permissionStates.clear() |
||||||
|
|
||||||
|
// 设置新的权限状态
|
||||||
|
Object.entries(permissions).forEach(([permission, hasPermission]) => { |
||||||
|
permissionStates.set(permission, hasPermission) |
||||||
|
}) |
||||||
|
|
||||||
|
console.log('🔐 用户权限已更新:', permissions) |
||||||
|
|
||||||
|
// 重新校验所有button的权限状态
|
||||||
|
revalidateAllButtons() |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取按钮权限 |
||||||
|
* @param {string} buttonName - 按钮名称 |
||||||
|
* @returns {string} 权限标识符 |
||||||
|
*/ |
||||||
|
function getButtonPermission(buttonName) { |
||||||
|
// 根据按钮名称生成权限标识符
|
||||||
|
// 格式:模块名.组件名.按钮名
|
||||||
|
const parts = buttonName.split('-') |
||||||
|
if (parts.length >= 2) { |
||||||
|
const module = parts[0] |
||||||
|
const component = parts[1] |
||||||
|
return `${module}.${component}.${buttonName}` |
||||||
|
} |
||||||
|
return buttonName |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 检查按钮权限 |
||||||
|
* @param {string} buttonName - 按钮名称 |
||||||
|
* @returns {boolean} 是否有权限 |
||||||
|
*/ |
||||||
|
function hasButtonPermission(buttonName) { |
||||||
|
const permission = getButtonPermission(buttonName) |
||||||
|
return permissionStates.get(permission) || false |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 重新校验所有button的权限状态 |
||||||
|
*/ |
||||||
|
function revalidateAllButtons() { |
||||||
|
buttonStates.forEach((state, buttonName) => { |
||||||
|
const hasPermission = hasButtonPermission(buttonName) |
||||||
|
if (!hasPermission && !state.element.disabled) { |
||||||
|
// 没有权限但按钮可用,强制禁用
|
||||||
|
state.element.disabled = true |
||||||
|
state.element.classList.add('permission-denied') |
||||||
|
console.log(`🚫 按钮 ${buttonName} 权限被拒绝,已禁用`) |
||||||
|
} else if (hasPermission && state.element.disabled && state.element.classList.contains('permission-denied')) { |
||||||
|
// 有权限但被权限控制禁用,恢复原状态
|
||||||
|
state.element.disabled = state.originalDisabled |
||||||
|
state.element.classList.remove('permission-denied') |
||||||
|
console.log(`✅ 按钮 ${buttonName} 权限已恢复`) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 权限控制指令 |
||||||
|
*/ |
||||||
|
export const permissionControl = { |
||||||
|
mounted(el, binding) { |
||||||
|
const buttonName = el.getAttribute('name') |
||||||
|
if (!buttonName) { |
||||||
|
console.warn('权限控制指令需要button的name属性') |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// 保存按钮的原始状态
|
||||||
|
const originalDisabled = el.disabled |
||||||
|
buttonStates.set(buttonName, { |
||||||
|
element: el, |
||||||
|
originalDisabled: originalDisabled, |
||||||
|
hasPermission: hasButtonPermission(buttonName) |
||||||
|
}) |
||||||
|
|
||||||
|
// 初始权限检查
|
||||||
|
if (!hasButtonPermission(buttonName)) { |
||||||
|
el.disabled = true |
||||||
|
el.classList.add('permission-denied') |
||||||
|
console.log(`🚫 按钮 ${buttonName} 初始权限检查失败,已禁用`) |
||||||
|
} |
||||||
|
|
||||||
|
// 监听disabled属性变化
|
||||||
|
const observer = new MutationObserver((mutations) => { |
||||||
|
mutations.forEach((mutation) => { |
||||||
|
if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') { |
||||||
|
const hasPermission = hasButtonPermission(buttonName) |
||||||
|
const isDisabled = el.disabled |
||||||
|
|
||||||
|
// 如果按钮被启用但没有权限,强制禁用
|
||||||
|
if (!isDisabled && !hasPermission) { |
||||||
|
el.disabled = true |
||||||
|
el.classList.add('permission-denied') |
||||||
|
console.log(`🚫 按钮 ${buttonName} 权限检查失败,已禁用`) |
||||||
|
} else if (isDisabled && hasPermission && el.classList.contains('permission-denied')) { |
||||||
|
// 如果有权限但被权限控制禁用,恢复原状态
|
||||||
|
el.disabled = originalDisabled |
||||||
|
el.classList.remove('permission-denied') |
||||||
|
console.log(`✅ 按钮 ${buttonName} 权限检查通过,已恢复`) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
// 开始观察
|
||||||
|
observer.observe(el, { |
||||||
|
attributes: true, |
||||||
|
attributeFilter: ['disabled'] |
||||||
|
}) |
||||||
|
|
||||||
|
// 保存observer引用,用于清理
|
||||||
|
el._permissionObserver = observer |
||||||
|
}, |
||||||
|
|
||||||
|
unmounted(el) { |
||||||
|
// 清理observer
|
||||||
|
if (el._permissionObserver) { |
||||||
|
el._permissionObserver.disconnect() |
||||||
|
delete el._permissionObserver |
||||||
|
} |
||||||
|
|
||||||
|
// 清理按钮状态
|
||||||
|
const buttonName = el.getAttribute('name') |
||||||
|
if (buttonName) { |
||||||
|
buttonStates.delete(buttonName) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 全局权限控制对象 |
||||||
|
*/ |
||||||
|
export const permissionControlGlobal = { |
||||||
|
setUserPermissions, |
||||||
|
hasButtonPermission, |
||||||
|
revalidateAllButtons, |
||||||
|
getButtonStates: () => Object.fromEntries(buttonStates) |
||||||
|
} |
||||||
|
|
||||||
|
export default permissionControl |
||||||
|
|
@ -0,0 +1,20 @@ |
|||||||
|
/** |
||||||
|
* 权限控制插件 |
||||||
|
*/ |
||||||
|
import { permissionControl, permissionControlGlobal } from '../directives/permission-control.js' |
||||||
|
|
||||||
|
export default { |
||||||
|
install(app, options = {}) { |
||||||
|
// 注册权限控制指令
|
||||||
|
app.directive('permission-control', permissionControl) |
||||||
|
|
||||||
|
// 将权限控制对象挂载到全局属性
|
||||||
|
app.config.globalProperties.$permission = permissionControlGlobal |
||||||
|
|
||||||
|
// 提供权限控制方法
|
||||||
|
app.provide('permission', permissionControlGlobal) |
||||||
|
|
||||||
|
console.log('🔐 权限控制插件已安装') |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,135 @@ |
|||||||
|
// Vue Add Attribute Loader
|
||||||
|
// 用于在构建时为Vue文件的button标签添加name属性和权限控制属性
|
||||||
|
|
||||||
|
const { parse, compileTemplate } = require('@vue/compiler-sfc') |
||||||
|
|
||||||
|
module.exports = function(source) { |
||||||
|
const callback = this.async() |
||||||
|
|
||||||
|
|
||||||
|
try { |
||||||
|
// 解析Vue文件
|
||||||
|
const { descriptor } = parse(source, { filename: this.resourcePath }) |
||||||
|
|
||||||
|
|
||||||
|
if (!descriptor.template || !descriptor.template.ast) { |
||||||
|
// 如果没有template,直接返回原内容
|
||||||
|
return callback(null, source) |
||||||
|
} |
||||||
|
|
||||||
|
let modified = false |
||||||
|
|
||||||
|
// 遍历AST,找到button节点并添加属性
|
||||||
|
const addAttributesToButton = (node) => { |
||||||
|
if (node.type === 1 && node.tag === 'button') { |
||||||
|
// 检查是否已经有name属性
|
||||||
|
const hasNameAttr = node.props.some(prop =>
|
||||||
|
prop.type === 6 && prop.name === 'name' |
||||||
|
) |
||||||
|
|
||||||
|
if (!hasNameAttr) { |
||||||
|
// 生成唯一的name值
|
||||||
|
const componentName = this.resourcePath.split('/').pop().replace('.vue', '').toLowerCase() |
||||||
|
const uniqueSuffix = Math.random().toString(36).substr(2, 6) |
||||||
|
const nameValue = `${componentName}-${uniqueSuffix}` |
||||||
|
|
||||||
|
// 添加name属性
|
||||||
|
node.props.push({ |
||||||
|
type: 6, |
||||||
|
name: 'name', |
||||||
|
value: { |
||||||
|
type: 2, |
||||||
|
content: nameValue, |
||||||
|
loc: node.loc |
||||||
|
}, |
||||||
|
loc: node.loc |
||||||
|
}) |
||||||
|
|
||||||
|
// 检查是否已经有v-permission-control指令
|
||||||
|
const hasPermissionControl = node.props.some(prop =>
|
||||||
|
prop.type === 7 && prop.name === 'v-permission-control' |
||||||
|
) |
||||||
|
|
||||||
|
if (!hasPermissionControl) { |
||||||
|
// 添加v-permission-control指令(不添加:disabled,避免重复)
|
||||||
|
node.props.push({ |
||||||
|
type: 7, |
||||||
|
name: 'v-permission-control', |
||||||
|
exp: { |
||||||
|
type: 4, |
||||||
|
content: 'true', |
||||||
|
isStatic: false, |
||||||
|
loc: node.loc |
||||||
|
}, |
||||||
|
loc: node.loc |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
modified = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 递归处理子节点
|
||||||
|
if (node.children) { |
||||||
|
node.children.forEach(addAttributesToButton) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 开始遍历AST
|
||||||
|
addAttributesToButton(descriptor.template.ast) |
||||||
|
|
||||||
|
// 如果修改了AST,重新生成Vue文件
|
||||||
|
if (modified) { |
||||||
|
// 由于Vue编译器没有直接的AST到字符串转换,我们使用字符串替换的方式
|
||||||
|
// 先找到template部分
|
||||||
|
const templateMatch = source.match(/(<template[^>]*>)([\s\S]*?)(<\/template>)/) |
||||||
|
if (templateMatch) { |
||||||
|
let templateContent = templateMatch[2] |
||||||
|
|
||||||
|
// 为每个button添加属性(使用字符串替换)
|
||||||
|
templateContent = templateContent.replace( |
||||||
|
/<button([^>]*?)(\s*\/?>)/g, |
||||||
|
(match, attrs, closing) => { |
||||||
|
// 检查是否已经有name属性
|
||||||
|
if (attrs.includes('name=')) { |
||||||
|
return match // 已经有name属性,不修改
|
||||||
|
} |
||||||
|
|
||||||
|
// 生成唯一的name值
|
||||||
|
const componentName = this.resourcePath.split('/').pop().replace('.vue', '').toLowerCase() |
||||||
|
const uniqueSuffix = Math.random().toString(36).substr(2, 6) |
||||||
|
const nameValue = `${componentName}-${uniqueSuffix}` |
||||||
|
|
||||||
|
// 检查是否已经有v-permission-control指令
|
||||||
|
const hasPermissionControl = attrs.includes('v-permission-control') |
||||||
|
|
||||||
|
// 构建新的属性字符串
|
||||||
|
let newAttrs = attrs |
||||||
|
newAttrs += ` name="${nameValue}"` |
||||||
|
if (!hasPermissionControl) { |
||||||
|
newAttrs += ` v-permission-control` |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
return `<button${newAttrs}${closing}` |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
// 替换原文件中的template部分
|
||||||
|
const newSource = source.replace( |
||||||
|
templateMatch[0], |
||||||
|
`${templateMatch[1]}${templateContent}${templateMatch[3]}` |
||||||
|
) |
||||||
|
return callback(null, newSource) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 如果没有修改,返回原内容
|
||||||
|
callback(null, source) |
||||||
|
|
||||||
|
} catch (error) { |
||||||
|
console.warn(`[Vue Add Attr Loader] 处理文件时出错:`, error.message) |
||||||
|
// 出错时返回原内容
|
||||||
|
callback(null, source) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue