新增页面按钮目录与菜单同步三级按钮;增强我的订单(问题件/理赔件、费用、详情抽屉)、Nana 助手、联系客服、渠道问题模板及登录/RBAC 等相关功能。 Co-authored-by: Cursor <cursoragent@cursor.com>
198 lines
5.4 KiB
JavaScript
198 lines
5.4 KiB
JavaScript
/**
|
||
* 扫描前端并输出 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));
|