672 lines
23 KiB
JavaScript
672 lines
23 KiB
JavaScript
/**
|
||
* 批量提取所有页面接口地址的自动化脚本
|
||
*
|
||
* 处理流程:
|
||
* 1. 解析 menu.json 提取所有 SYSTEMMODULE_URL
|
||
* 2. 解析 routes.ts 构建嵌套路径映射(子父级拼接)
|
||
* 3. 定位组件文件夹及所有子组件
|
||
* 4. 提取 import 的 service 文件
|
||
* 5. 在 service 文件中提取 request() 路径,并根据引用的 request 工具确定 prefix
|
||
* 6. 输出结果到 config/api_mapping.json 和 config/api_mapping.md
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// 项目根目录
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
const SRC = path.join(ROOT, 'src');
|
||
const PAGES = path.join(SRC, 'pages');
|
||
|
||
// ============ 第一部分:request prefix 映射 ============
|
||
// 根据 service 文件 import 的 request 来源确定 prefix
|
||
const REQUEST_PREFIX_MAP = {
|
||
'request': 'https://eshangtech.com:18900/EShangApiMain',
|
||
'requestUpLoad': 'http://220.180.35.180:8000/EShangApiMain',
|
||
'requestTest': 'http://dev.eshangtech.com:8900/EShangApiMain',
|
||
'requestTestTest': 'http://dev.eshangtech.com:8001/EShangApiMain',
|
||
'requestNoPrefix': 'https://eshangtech.com:18900/',
|
||
'requestPythonTest': 'http://192.168.1.207:8002/',
|
||
'requestNewTest': 'https://eshangtech.com:18900/EShangApiMain',
|
||
'requestNewJava': 'https://java.es.eshangtech.com:443',
|
||
'requestnew': '/EShangApiDashboard',
|
||
'requestEncryption': 'https://eshangtech.com:18900/MemberApi',
|
||
'requestJava': 'https://java.es.eshangtech.com',
|
||
'requestDashboard': 'https://eshangtech.com:18900/EshangApiDashboard',
|
||
'requestCodeBuilder': 'http://dev.eshangtech.com:8001/CodeBuilderApi',
|
||
'requestCode': 'https://eshangtech.com:18900/CommercialApi',
|
||
'requestAHYD': 'https://ahyd.eshangtech.com/EShangApiMain',
|
||
};
|
||
|
||
// ============ 第二部分:解析 routes.ts 构建路径映射 ============
|
||
|
||
/**
|
||
* 解析 routes.ts 文件,构建 fullPath → component 映射
|
||
* 处理嵌套路由的路径拼接
|
||
*/
|
||
function parseRoutes() {
|
||
const routesFile = path.join(ROOT, 'config', 'routes.ts');
|
||
const content = fs.readFileSync(routesFile, 'utf-8');
|
||
|
||
// 用正则提取所有路由对象(简化处理:逐行匹配 path 和 component)
|
||
const routeMap = {};
|
||
|
||
// 使用栈来跟踪嵌套路径
|
||
const pathStack = [];
|
||
let braceDepth = 0;
|
||
const depthToPath = {}; // 记录每个深度对应的 path 值
|
||
|
||
const lines = content.split('\n');
|
||
let currentPath = null;
|
||
let currentComponent = null;
|
||
let currentObjStartDepth = -1;
|
||
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
|
||
// 计算花括号深度
|
||
for (const ch of trimmed) {
|
||
if (ch === '{') braceDepth++;
|
||
if (ch === '}') {
|
||
// 当离开一个层级时,清除该层级的 path
|
||
if (depthToPath[braceDepth]) {
|
||
delete depthToPath[braceDepth];
|
||
}
|
||
braceDepth--;
|
||
}
|
||
}
|
||
|
||
// 匹配 path 属性
|
||
const pathMatch = trimmed.match(/^path:\s*['"]([^'"]+)['"]/);
|
||
if (pathMatch) {
|
||
const pathVal = pathMatch[1];
|
||
depthToPath[braceDepth] = pathVal;
|
||
}
|
||
|
||
// 匹配 component 属性
|
||
const compMatch = trimmed.match(/^component:\s*['"]([^'"]+)['"]/);
|
||
if (compMatch) {
|
||
const comp = compMatch[1];
|
||
// 跳过 layout 组件
|
||
if (comp.includes('Layout') || comp.includes('layout')) continue;
|
||
|
||
// 构建完整路径:从 depthToPath 中按层级拼接
|
||
const fullPath = buildFullPath(depthToPath, braceDepth);
|
||
if (fullPath && fullPath !== '/') {
|
||
routeMap[fullPath] = comp;
|
||
}
|
||
}
|
||
}
|
||
|
||
return routeMap;
|
||
}
|
||
|
||
/**
|
||
* 根据深度映射构建完整路径
|
||
*/
|
||
function buildFullPath(depthToPath, currentDepth) {
|
||
// 收集所有当前有效的路径段
|
||
const segments = [];
|
||
const depths = Object.keys(depthToPath).map(Number).sort((a, b) => a - b);
|
||
|
||
for (const d of depths) {
|
||
if (d <= currentDepth) {
|
||
segments.push(depthToPath[d]);
|
||
}
|
||
}
|
||
|
||
if (segments.length === 0) return null;
|
||
|
||
// 拼接路径
|
||
let result = '';
|
||
for (const seg of segments) {
|
||
if (seg.startsWith('/')) {
|
||
// 绝对路径,重新开始
|
||
result = seg;
|
||
} else {
|
||
// 相对路径,拼接
|
||
if (result.endsWith('/')) {
|
||
result += seg;
|
||
} else {
|
||
result += '/' + seg;
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
// ============ 第三部分:备用路径匹配 ============
|
||
|
||
/**
|
||
* 当 routes.ts 中找不到完全匹配时,尝试通过 URL 路径推断组件位置
|
||
* 扫描 src/pages 目录查找匹配的文件夹
|
||
*/
|
||
function findComponentByUrl(menuUrl) {
|
||
// 从 URL 提取最后一段作为组件名
|
||
const segments = menuUrl.split('/').filter(Boolean);
|
||
if (segments.length === 0) return null;
|
||
|
||
const lastSegment = segments[segments.length - 1];
|
||
|
||
// 在 pages 目录下递归搜索包含该名称的文件夹
|
||
const found = findDirRecursive(PAGES, lastSegment);
|
||
return found;
|
||
}
|
||
|
||
function findDirRecursive(dir, targetName) {
|
||
if (!fs.existsSync(dir)) return null;
|
||
|
||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
if (entry.isDirectory()) {
|
||
// 不区分大小写匹配
|
||
if (entry.name.toLowerCase() === targetName.toLowerCase()) {
|
||
const fullPath = path.join(dir, entry.name);
|
||
// 确认有 index.tsx 或 index.ts
|
||
if (fs.existsSync(path.join(fullPath, 'index.tsx')) ||
|
||
fs.existsSync(path.join(fullPath, 'index.ts'))) {
|
||
return fullPath;
|
||
}
|
||
}
|
||
// 递归搜索子目录
|
||
const result = findDirRecursive(path.join(dir, entry.name), targetName);
|
||
if (result) return result;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ============ 第四部分:定位组件并提取所有被引用的文件 ============
|
||
|
||
/**
|
||
* 根据 component 路径解析到实际文件夹
|
||
* 处理两种情况:
|
||
* 1. component 指向目录(如 './contract' → src/pages/contract/)
|
||
* 2. component 指向文件(如 './contract/list' → src/pages/contract/list.tsx)
|
||
* 此时返回文件所在的父目录
|
||
*/
|
||
function resolveComponentDir(componentPath) {
|
||
let resolved = componentPath.replace(/^\.\//, '');
|
||
const fullPath = path.join(PAGES, resolved);
|
||
|
||
// 情况1:路径本身是目录(如 src/pages/contract/)
|
||
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
|
||
return fullPath;
|
||
}
|
||
|
||
// 情况2:路径指向文件(如 src/pages/contract/list → 实际是 list.tsx)
|
||
// 检查是否存在同名的 .tsx / .ts 文件
|
||
const extensions = ['.tsx', '.ts', '.jsx', '.js'];
|
||
for (const ext of extensions) {
|
||
if (fs.existsSync(fullPath + ext)) {
|
||
// 文件存在,返回其父目录作为组件目录
|
||
return path.dirname(fullPath);
|
||
}
|
||
}
|
||
|
||
// 情况3:尝试去掉末尾的 /index
|
||
if (resolved.endsWith('/index')) {
|
||
resolved = resolved.replace(/\/index$/, '');
|
||
const altPath = path.join(PAGES, resolved);
|
||
if (fs.existsSync(altPath) && fs.statSync(altPath).isDirectory()) {
|
||
return altPath;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取组件目录下所有 .tsx / .ts 文件(包括 components 子目录)
|
||
*/
|
||
function getAllComponentFiles(dir) {
|
||
const files = [];
|
||
if (!fs.existsSync(dir)) return files;
|
||
|
||
function scan(currentDir) {
|
||
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
const fullPath = path.join(currentDir, entry.name);
|
||
if (entry.isDirectory()) {
|
||
scan(fullPath);
|
||
} else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {
|
||
// 排除 service.ts 本身和 .d.ts
|
||
if (!entry.name.endsWith('.d.ts')) {
|
||
files.push(fullPath);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
scan(dir);
|
||
return files;
|
||
}
|
||
|
||
// ============ 第五部分:提取 service 导入和接口路径 ============
|
||
|
||
/**
|
||
* 从组件文件中提取所有 import 的 service 路径
|
||
*/
|
||
function extractServiceImports(filePath) {
|
||
const content = fs.readFileSync(filePath, 'utf-8');
|
||
const imports = [];
|
||
|
||
// 匹配各种 import 模式
|
||
// import { xxx } from '../xxx/service'
|
||
// import { xxx } from '@/pages/xxx/service'
|
||
// import { xxx } from '@/services/xxx'
|
||
const importRegex = /import\s+\{([^}]+)\}\s+from\s+['"](.*?)['"]/g;
|
||
let match;
|
||
|
||
while ((match = importRegex.exec(content)) !== null) {
|
||
const functions = match[1].split(',').map(f => f.trim()).filter(Boolean);
|
||
let importPath = match[2];
|
||
|
||
// 只关注 service 文件、services 目录、options 目录的导入
|
||
if (importPath.includes('service') || importPath.includes('services') ||
|
||
importPath.includes('options')) {
|
||
imports.push({
|
||
functions,
|
||
importPath,
|
||
sourceFile: filePath
|
||
});
|
||
}
|
||
}
|
||
|
||
return imports;
|
||
}
|
||
|
||
/**
|
||
* 解析 import 路径到实际文件路径
|
||
*/
|
||
function resolveImportPath(importPath, sourceFile) {
|
||
let resolved;
|
||
|
||
if (importPath.startsWith('@/')) {
|
||
// @/ 别名 → src/
|
||
resolved = path.join(SRC, importPath.replace('@/', ''));
|
||
} else if (importPath.startsWith('.')) {
|
||
// 相对路径
|
||
resolved = path.resolve(path.dirname(sourceFile), importPath);
|
||
} else {
|
||
return null; // node_modules 等,跳过
|
||
}
|
||
|
||
// 尝试各种扩展名
|
||
const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx', '.js', '/index.js'];
|
||
for (const ext of extensions) {
|
||
const candidate = resolved + ext;
|
||
if (fs.existsSync(candidate)) {
|
||
return candidate;
|
||
}
|
||
}
|
||
|
||
// 如果直接存在
|
||
if (fs.existsSync(resolved)) {
|
||
if (fs.statSync(resolved).isDirectory()) {
|
||
for (const ext of ['/index.ts', '/index.tsx', '/index.js']) {
|
||
const idx = resolved + ext;
|
||
if (fs.existsSync(idx)) return idx;
|
||
}
|
||
}
|
||
return resolved;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 缓存已解析的 service 文件
|
||
const serviceCache = {};
|
||
|
||
/**
|
||
* 从 service 文件中提取所有 request() 调用的路径
|
||
* 同时确定引用的是哪个 request 工具(确定 prefix)
|
||
* 并记录每个 request() 调用所属的函数名
|
||
*/
|
||
function extractApisFromService(serviceFilePath) {
|
||
if (serviceCache[serviceFilePath]) {
|
||
return serviceCache[serviceFilePath];
|
||
}
|
||
|
||
if (!fs.existsSync(serviceFilePath)) {
|
||
serviceCache[serviceFilePath] = [];
|
||
return [];
|
||
}
|
||
|
||
const content = fs.readFileSync(serviceFilePath, 'utf-8');
|
||
const apis = [];
|
||
|
||
// 1. 确定该 service 文件使用的 request 来源
|
||
let prefix = REQUEST_PREFIX_MAP['request']; // 默认 prefix
|
||
|
||
// 匹配 import request from '@/utils/requestXXX'
|
||
const requestImportRegex = /import\s+\w+\s+from\s+['"]@\/utils\/(\w+)['"]/g;
|
||
let reqMatch;
|
||
while ((reqMatch = requestImportRegex.exec(content)) !== null) {
|
||
const requestName = reqMatch[1];
|
||
if (REQUEST_PREFIX_MAP[requestName] !== undefined) {
|
||
prefix = REQUEST_PREFIX_MAP[requestName];
|
||
}
|
||
}
|
||
|
||
// 也匹配相对路径的 request 导入
|
||
const relRequestRegex = /import\s+\w+\s+from\s+['"]\.\.?\/.*?(\w+)['"].*?/g;
|
||
while ((reqMatch = relRequestRegex.exec(content)) !== null) {
|
||
const fileName = reqMatch[1];
|
||
if (REQUEST_PREFIX_MAP[fileName] !== undefined) {
|
||
prefix = REQUEST_PREFIX_MAP[fileName];
|
||
}
|
||
}
|
||
|
||
// 2. 逐行扫描,追踪当前函数名,并提取 request() 调用
|
||
const lines = content.split('\n');
|
||
let currentFuncName = '(unknown)';
|
||
|
||
// 函数定义的正则匹配模式:
|
||
// export async function handleXxx(...)
|
||
// export function handleXxx(...)
|
||
// export const handleXxx = async (...)
|
||
// export const handleXxx = (...)
|
||
const funcDefRegex = /^export\s+(?:async\s+)?function\s+(\w+)|^export\s+const\s+(\w+)\s*=/;
|
||
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
|
||
// 检查是否是函数定义行
|
||
const funcMatch = trimmed.match(funcDefRegex);
|
||
if (funcMatch) {
|
||
currentFuncName = funcMatch[1] || funcMatch[2];
|
||
}
|
||
|
||
// 检查是否有 request() 调用
|
||
const requestCallRegex = /request\(\s*['"`]([^'"`\$]+)['"`]/g;
|
||
let apiMatch;
|
||
while ((apiMatch = requestCallRegex.exec(trimmed)) !== null) {
|
||
const apiPath = apiMatch[1];
|
||
// 构建完整 URL
|
||
let fullUrl;
|
||
if (prefix.endsWith('/')) {
|
||
fullUrl = prefix + apiPath.replace(/^\//, '');
|
||
} else {
|
||
fullUrl = prefix + apiPath;
|
||
}
|
||
|
||
apis.push({
|
||
path: apiPath,
|
||
fullUrl: fullUrl,
|
||
prefix: prefix,
|
||
functionName: currentFuncName,
|
||
serviceFile: path.relative(ROOT, serviceFilePath)
|
||
});
|
||
}
|
||
}
|
||
|
||
// 3. 也检查该 service 文件是否还 import 了其他 service(二级引用)
|
||
const serviceImports = extractServiceImports(serviceFilePath);
|
||
for (const imp of serviceImports) {
|
||
// 避免循环引用
|
||
const resolvedPath = resolveImportPath(imp.importPath, serviceFilePath);
|
||
if (resolvedPath && resolvedPath !== serviceFilePath && !serviceCache[resolvedPath]) {
|
||
const subApis = extractApisFromService(resolvedPath);
|
||
apis.push(...subApis);
|
||
}
|
||
}
|
||
|
||
serviceCache[serviceFilePath] = apis;
|
||
return apis;
|
||
}
|
||
|
||
// ============ 第六部分:解析 menu.json 提取所有模块 ============
|
||
|
||
function extractModules(items, parentPath = '') {
|
||
const modules = [];
|
||
for (const item of items) {
|
||
if (item.SYSTEMMODULE_URL && item.SYSTEMMODULE_URL !== '.' && item.SYSTEMMODULE_URL !== '/') {
|
||
// 跳过外部链接
|
||
if (!item.SYSTEMMODULE_URL.startsWith('http')) {
|
||
modules.push({
|
||
name: item.SYSTEMMODULE_NAME,
|
||
url: item.SYSTEMMODULE_URL,
|
||
parentMenu: parentPath
|
||
});
|
||
}
|
||
}
|
||
if (item.children) {
|
||
const menuName = item.SYSTEMMENU_NAME || '';
|
||
const newParent = parentPath ? `${parentPath} > ${menuName}` : menuName;
|
||
modules.push(...extractModules(item.children, newParent));
|
||
}
|
||
}
|
||
return modules;
|
||
}
|
||
|
||
// ============ 第七部分:主流程 ============
|
||
|
||
function main() {
|
||
console.log('🚀 开始批量提取页面接口...\n');
|
||
|
||
// 1. 读取 menu.json
|
||
const menuData = require(path.join(ROOT, 'config', 'menu.json'));
|
||
const modules = extractModules(menuData);
|
||
console.log(`📋 共发现 ${modules.length} 个有效页面模块\n`);
|
||
|
||
// 2. 解析 routes.ts 构建路径映射
|
||
const routeMap = parseRoutes();
|
||
console.log(`🗺️ 路由映射构建完成,共 ${Object.keys(routeMap).length} 条路由\n`);
|
||
|
||
// 3. 逐个处理每个模块
|
||
const results = [];
|
||
let matchedCount = 0;
|
||
let unmatchedCount = 0;
|
||
let totalApis = 0;
|
||
|
||
for (const mod of modules) {
|
||
const result = {
|
||
menuName: mod.name,
|
||
menuUrl: mod.url,
|
||
parentMenu: mod.parentMenu,
|
||
componentPath: null,
|
||
componentDir: null,
|
||
apiCount: 0,
|
||
apis: [],
|
||
status: 'unmatched'
|
||
};
|
||
|
||
// 3.1 在路由映射中查找 component
|
||
let component = routeMap[mod.url];
|
||
|
||
// 3.2 如果没有直接匹配,尝试带参数路径的匹配
|
||
if (!component) {
|
||
// 处理带动态参数的路由,比如 /setting/department/:id 匹配 /setting/department/userstype
|
||
for (const [routePath, comp] of Object.entries(routeMap)) {
|
||
if (routePath.includes(':')) {
|
||
const routeRegex = routePath.replace(/:[^/]+/g, '[^/]+');
|
||
if (new RegExp(`^${routeRegex}$`).test(mod.url)) {
|
||
component = comp;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (component) {
|
||
result.componentPath = component;
|
||
|
||
// 3.3 解析 component 路径到实际文件夹
|
||
const componentDir = resolveComponentDir(component);
|
||
if (componentDir) {
|
||
result.componentDir = path.relative(ROOT, componentDir);
|
||
result.status = 'matched';
|
||
matchedCount++;
|
||
|
||
// 3.4 获取所有组件文件
|
||
const componentFiles = getAllComponentFiles(componentDir);
|
||
|
||
// 3.5 提取所有 service 导入
|
||
const allApis = new Set(); // 用 Set 去重
|
||
const apiDetails = [];
|
||
|
||
for (const file of componentFiles) {
|
||
const serviceImports = extractServiceImports(file);
|
||
|
||
for (const imp of serviceImports) {
|
||
const resolvedService = resolveImportPath(imp.importPath, file);
|
||
if (resolvedService) {
|
||
const apis = extractApisFromService(resolvedService);
|
||
for (const api of apis) {
|
||
if (!allApis.has(api.fullUrl)) {
|
||
allApis.add(api.fullUrl);
|
||
apiDetails.push(api);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
result.apiCount = apiDetails.length;
|
||
result.apis = apiDetails.map(a => ({
|
||
fullUrl: a.fullUrl,
|
||
path: a.path,
|
||
prefix: a.prefix,
|
||
functionName: a.functionName,
|
||
serviceFile: a.serviceFile
|
||
}));
|
||
totalApis += apiDetails.length;
|
||
} else {
|
||
result.status = 'dir_not_found';
|
||
unmatchedCount++;
|
||
}
|
||
} else {
|
||
// 3.6 备用:通过 URL 最后一段搜索
|
||
const fallbackDir = findComponentByUrl(mod.url);
|
||
if (fallbackDir) {
|
||
result.componentDir = path.relative(ROOT, fallbackDir);
|
||
result.status = 'matched_fallback';
|
||
matchedCount++;
|
||
|
||
const componentFiles = getAllComponentFiles(fallbackDir);
|
||
const allApis = new Set();
|
||
const apiDetails = [];
|
||
|
||
for (const file of componentFiles) {
|
||
const serviceImports = extractServiceImports(file);
|
||
for (const imp of serviceImports) {
|
||
const resolvedService = resolveImportPath(imp.importPath, file);
|
||
if (resolvedService) {
|
||
const apis = extractApisFromService(resolvedService);
|
||
for (const api of apis) {
|
||
if (!allApis.has(api.fullUrl)) {
|
||
allApis.add(api.fullUrl);
|
||
apiDetails.push(api);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
result.apiCount = apiDetails.length;
|
||
result.apis = apiDetails.map(a => ({
|
||
fullUrl: a.fullUrl,
|
||
path: a.path,
|
||
prefix: a.prefix,
|
||
functionName: a.functionName,
|
||
serviceFile: a.serviceFile
|
||
}));
|
||
totalApis += apiDetails.length;
|
||
} else {
|
||
unmatchedCount++;
|
||
}
|
||
}
|
||
|
||
results.push(result);
|
||
}
|
||
|
||
// 4. 输出统计
|
||
console.log('========== 统计结果 ==========');
|
||
console.log(`✅ 匹配成功: ${matchedCount} 个页面`);
|
||
console.log(`❌ 未匹配: ${unmatchedCount} 个页面`);
|
||
console.log(`📡 总接口数: ${totalApis} 个`);
|
||
console.log('');
|
||
|
||
// 5. 输出 JSON 文件
|
||
const jsonOutput = path.join(ROOT, 'config', 'api_mapping.json');
|
||
fs.writeFileSync(jsonOutput, JSON.stringify(results, null, 2), 'utf-8');
|
||
console.log(`📄 JSON 已输出: ${jsonOutput}`);
|
||
|
||
// 6. 输出 Markdown 文件
|
||
const mdOutput = path.join(ROOT, 'config', 'api_mapping.md');
|
||
let md = '# 系统页面接口映射表\n\n';
|
||
md += `> 自动生成时间: ${new Date().toLocaleString()}\n\n`;
|
||
md += `> 总页面数: ${modules.length} | 匹配成功: ${matchedCount} | 未匹配: ${unmatchedCount} | 总接口数: ${totalApis}\n\n`;
|
||
|
||
// 按父级菜单分组
|
||
const groupedByParent = {};
|
||
for (const r of results) {
|
||
const parent = r.parentMenu || '未分类';
|
||
if (!groupedByParent[parent]) {
|
||
groupedByParent[parent] = [];
|
||
}
|
||
groupedByParent[parent].push(r);
|
||
}
|
||
|
||
for (const [parentMenu, items] of Object.entries(groupedByParent)) {
|
||
md += `## ${parentMenu}\n\n`;
|
||
|
||
for (const item of items) {
|
||
if (item.status === 'unmatched' || item.status === 'dir_not_found') {
|
||
md += `### ❌ ${item.menuName}\n`;
|
||
md += `- 路径: \`${item.menuUrl}\`\n`;
|
||
md += `- 状态: 未找到对应组件\n\n`;
|
||
continue;
|
||
}
|
||
|
||
md += `### ${item.menuName}\n`;
|
||
md += `- 路径: \`${item.menuUrl}\`\n`;
|
||
md += `- 组件: \`${item.componentDir || item.componentPath}\`\n`;
|
||
md += `- 调用了 **${item.apiCount}** 个接口\n\n`;
|
||
|
||
if (item.apis.length > 0) {
|
||
md += '| # | 函数名 | 接口路径 | 完整地址 | 来源 |\n';
|
||
md += '|---|--------|----------|----------|------|\n';
|
||
|
||
item.apis.forEach((api, idx) => {
|
||
md += `| ${idx + 1} | \`${api.functionName || '-'}\` | \`${api.path}\` | \`${api.fullUrl}\` | \`${api.serviceFile}\` |\n`;
|
||
});
|
||
|
||
md += '\n';
|
||
}
|
||
}
|
||
|
||
md += '---\n\n';
|
||
}
|
||
|
||
// 7. 添加未匹配页面汇总
|
||
const unmatched = results.filter(r => r.status === 'unmatched' || r.status === 'dir_not_found');
|
||
if (unmatched.length > 0) {
|
||
md += '## ⚠️ 未匹配页面汇总\n\n';
|
||
md += '| # | 页面名称 | 路径 | 所属菜单 |\n';
|
||
md += '|---|----------|------|----------|\n';
|
||
unmatched.forEach((item, idx) => {
|
||
md += `| ${idx + 1} | ${item.menuName} | \`${item.menuUrl}\` | ${item.parentMenu} |\n`;
|
||
});
|
||
md += '\n';
|
||
}
|
||
|
||
fs.writeFileSync(mdOutput, md, 'utf-8');
|
||
console.log(`📝 Markdown 已输出: ${mdOutput}`);
|
||
|
||
// 8. 打印部分结果预览
|
||
console.log('\n========== 结果预览(前 10 个有接口的页面)==========\n');
|
||
const withApis = results.filter(r => r.apiCount > 0).slice(0, 10);
|
||
for (const r of withApis) {
|
||
console.log(`📌 【${r.menuName}】调用了 ${r.apiCount} 个接口,分别为:`);
|
||
r.apis.forEach((api, idx) => {
|
||
console.log(` ${idx + 1}. ${api.fullUrl}`);
|
||
});
|
||
console.log('');
|
||
}
|
||
}
|
||
|
||
main();
|