/** * 扫描前端并输出 pageButtonCatalog 分组数据 */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); const srcDir = path.join(root, 'src'); const ACTION_LABELS = { Query: '查询', Create: '新增', Update: '修改', Delete: '删除', Move: '移动', AssignRoles: '分配角色', AssignPermissions: '分配权限', ResetPassword: '重置密码', Template: '模板管理', Audit: '审计', Use: '使用', Save: '保存', Export: '导出', Import: '导入', Publish: '发布', Pay: '支付', Print: '打印', Intercept: '拦截', CancelIntercept: '取消拦截', PreDeclare: '预报', DeleteOrder: '删除订单', View: '查看', Edit: '编辑', Approve: '审核', Reject: '驳回', Enable: '启用', Disable: '禁用', Copy: '复制', Download: '下载', Upload: '上传', Sync: '同步', Retry: '重试', Cancel: '取消', Submit: '提交', BatchDelete: '批量删除', BatchImport: '批量导入', BatchExport: '批量导出', Forward: '转发', Debug: '调试', Test: '测试', Control: '控制', Analyze: '分析', Generate: '生成', Recharge: '充值', Bind: '绑定', Unbind: '解绑', Reset: '重置', Config: '配置', SaveConfig: '保存配置', }; function walk(dir, ext, out = []) { if (!fs.existsSync(dir)) return out; for (const name of fs.readdirSync(dir)) { const full = path.join(dir, name); const stat = fs.statSync(full); if (stat.isDirectory()) { if (name === 'node_modules') continue; walk(full, ext, out); } else if (full.endsWith(ext)) { out.push(full); } } return out; } function inferName(code) { const action = code.split('.').pop(); return ACTION_LABELS[action] || action; } function normalizeResource(code) { const parts = code.split('.'); if (parts.length <= 2) return code; const action = parts[parts.length - 1]; if (ACTION_LABELS[action] || /^[A-Z][a-zA-Z0-9]+$/.test(action)) { return parts.slice(0, -1).join('.'); } return code; } function parseMenuExcelJs() { const text = fs.readFileSync(path.join(root, 'generate-menu-excel.js'), 'utf8'); const re = /permissionCode:\s*'([^']+)'/g; const codes = []; let m; while ((m = re.exec(text))) codes.push(m[1]); return codes; } const permissionCodes = new Set(); const vueFiles = walk(path.join(srcDir, 'views'), '.vue'); const routeFiles = walk(path.join(srcDir, 'router'), '.ts'); for (const file of [...vueFiles, ...routeFiles]) { const text = fs.readFileSync(file, 'utf8'); const patterns = [ /hasPermission\(\s*['"]([^'"]+)['"]/g, /permission:\s*['"]([^'"]+)['"]/g, /meta:\s*\{[^}]*permission:\s*['"]([^'"]+)['"]/g, /['"]([A-Z][a-zA-Z0-9]*(?:\.[A-Z][a-zA-Z0-9]*)+)['"]/g, ]; for (const re of patterns) { let m; while ((m = re.exec(text))) { const code = m[1]; if (code.includes('.') && !code.startsWith('http') && code.length < 80) { permissionCodes.add(code); } } } } parseMenuExcelJs().forEach((c) => permissionCodes.add(c)); // 标准 CRUD 模板(资源级页面菜单) const STANDARD_ACTIONS = ['Query', 'Create', 'Update', 'Delete', 'Export', 'Import']; const pageMenus = [...permissionCodes] .filter((code) => { const parts = code.split('.'); if (parts.length < 2) return false; const last = parts[parts.length - 1]; // 页面级:2段资源 或 末段非标准动作 if (parts.length === 2) return true; if (!ACTION_LABELS[last] && !STANDARD_ACTIONS.includes(last)) return true; return false; }) .sort(); const grouped = new Map(); function addButton(parent, code, name) { if (!parent || !code || code === parent) return; if (!grouped.has(parent)) grouped.set(parent, new Map()); grouped.get(parent).set(code, { permissionCode: code, name: name || inferName(code) }); } // 1) 已有权限码按资源分组 for (const code of [...permissionCodes].sort()) { const resource = normalizeResource(code); if (resource === code && code.split('.').length >= 2) { // 可能是页面菜单本身,暂不作为按钮 continue; } addButton(resource, code, inferName(code)); } // 2) 为每个页面菜单补标准 CRUD(若尚无) for (const menuCode of pageMenus) { const resource = normalizeResource(menuCode); const base = resource.split('.'); if (base.length < 2) continue; const prefix = resource; STANDARD_ACTIONS.forEach((action, idx) => { const code = `${prefix}.${action}`; if (!permissionCodes.has(code)) { // 仅对 RBAC/Api/Track 等模块补全,业务模块用 PascalCase 页面名 } addButton(prefix, code, inferName(code)); }); } // 3) generate-menu-excel 中带动作后缀的三级码 for (const code of parseMenuExcelJs()) { const parts = code.split('.'); if (parts.length >= 3) { const parent = parts.slice(0, -1).join('.'); addButton(parent, code, inferName(code)); addButton(normalizeResource(parent), code, inferName(code)); } } // 输出 JSON const result = [...grouped.entries()] .sort((a, b) => a[0].localeCompare(b[0])) .map(([parentPermissionCode, btnMap], gi) => ({ parentPermissionCode, buttons: [...btnMap.values()] .sort((a, b) => a.permissionCode.localeCompare(b.permissionCode)) .map((b, i) => ({ ...b, order: i })), })) .filter((g) => g.buttons.length > 0); console.log(JSON.stringify(result, null, 2));