front/scripts/build-page-button-catalog.mjs
zzh 7585e0c7b7 feat: 完善三级按钮同步、我的订单与助手等前端能力
新增页面按钮目录与菜单同步三级按钮;增强我的订单(问题件/理赔件、费用、详情抽屉)、Nana 助手、联系客服、渠道问题模板及登录/RBAC 等相关功能。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 12:36:18 +08:00

198 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 扫描前端并输出 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));