All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m33s
Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
5.7 KiB
JavaScript
153 lines
5.7 KiB
JavaScript
/**
|
||
* 扫描订单/仓储/渠道页面 hasPermission 与附近可见文案,并与 pageButtonCatalog 对比
|
||
* 用法: node scripts/scan-module-buttons.mjs
|
||
*/
|
||
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 catalogPath = path.join(root, 'src/constants/pageButtonCatalog.ts');
|
||
const modules = [
|
||
{ name: 'Order', dirs: ['src/views/order'] },
|
||
{ name: 'Warehouse', dirs: ['src/views/warehouse'] },
|
||
{ name: 'Channel', dirs: ['src/views/channel'] },
|
||
];
|
||
|
||
function walk(dir, 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()) walk(full, out);
|
||
else if (/\.(vue|ts|tsx|js)$/.test(full)) out.push(full);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function extractLabelNear(text, index) {
|
||
// look ahead ~400 chars for button text / t('...') / title=
|
||
const slice = text.slice(index, index + 500);
|
||
const tMatch = slice.match(/\{\{\s*t\(\s*['"]([^'"]+)['"]\s*\)/);
|
||
if (tMatch) return { type: 'i18n', value: tMatch[1] };
|
||
const titleMatch = slice.match(/title=["']([^"']+)["']/);
|
||
if (titleMatch) return { type: 'title', value: titleMatch[1] };
|
||
const textMatch = slice.match(/>([^<{][^<]{0,40})</);
|
||
if (textMatch && textMatch[1].trim()) return { type: 'text', value: textMatch[1].trim() };
|
||
return null;
|
||
}
|
||
|
||
function parseCatalogGroups(src) {
|
||
const groups = new Map();
|
||
const bRe = /B\(\s*['"]([^'"]+)['"]\s*,\s*\[([\s\S]*?)\]\s*\)/g;
|
||
let m;
|
||
while ((m = bRe.exec(src))) {
|
||
const parent = m[1];
|
||
if (!/^(Order|Warehouse|Channel)\./.test(parent)) continue;
|
||
const body = m[2];
|
||
const buttons = [];
|
||
const itemRe = /\{\s*code:\s*['"]([^'"]+)['"]\s*,\s*name:\s*['"]([^'"]*)['"]\s*\}/g;
|
||
let im;
|
||
while ((im = itemRe.exec(body))) {
|
||
const code = im[1].includes('.') ? im[1] : `${parent}.${im[1]}`;
|
||
buttons.push({ permissionCode: code, name: im[2] });
|
||
}
|
||
groups.set(parent, buttons);
|
||
}
|
||
return groups;
|
||
}
|
||
|
||
const zhLocale = fs.readFileSync(path.join(root, 'src/locale/zh-CN.ts'), 'utf8');
|
||
function resolveI18n(key) {
|
||
const re = new RegExp(`['"]${key.replace(/\./g, '\\.')}['"]\\s*:\\s*['"]([^'"]*)['"]`);
|
||
const m = zhLocale.match(re);
|
||
return m ? m[1] : null;
|
||
}
|
||
|
||
const catalog = parseCatalogGroups(fs.readFileSync(catalogPath, 'utf8'));
|
||
const foundByParent = new Map();
|
||
|
||
for (const mod of modules) {
|
||
for (const d of mod.dirs) {
|
||
for (const file of walk(path.join(root, d))) {
|
||
const text = fs.readFileSync(file, 'utf8');
|
||
const re = /hasPermission\(\s*['"]([^'"]+)['"]\s*\)/g;
|
||
let m;
|
||
while ((m = re.exec(text))) {
|
||
const code = m[1];
|
||
if (!/^(Order|Warehouse|Channel)\./.test(code)) continue;
|
||
const parts = code.split('.');
|
||
if (parts.length < 3) continue;
|
||
const parent = `${parts[0]}.${parts[1]}`;
|
||
const label = extractLabelNear(text, m.index);
|
||
let display = null;
|
||
if (label?.type === 'i18n') display = resolveI18n(label.value) || label.value;
|
||
else if (label) display = label.value;
|
||
if (!foundByParent.has(parent)) foundByParent.set(parent, new Map());
|
||
const map = foundByParent.get(parent);
|
||
if (!map.has(code)) map.set(code, { permissionCode: code, displayHints: [], files: [] });
|
||
const rec = map.get(code);
|
||
if (display) rec.displayHints.push(display);
|
||
rec.files.push(path.relative(root, file).replace(/\\/g, '/'));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function majority(arr) {
|
||
if (!arr.length) return null;
|
||
const counts = new Map();
|
||
for (const a of arr) counts.set(a, (counts.get(a) || 0) + 1);
|
||
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
||
}
|
||
|
||
console.log('# Order / Warehouse / Channel 按钮对比(页面 vs pageButtonCatalog)\n');
|
||
|
||
const parents = new Set([...catalog.keys(), ...foundByParent.keys()]);
|
||
for (const parent of [...parents].sort()) {
|
||
const cat = catalog.get(parent) || [];
|
||
const found = foundByParent.get(parent) || new Map();
|
||
const catCodes = new Set(cat.map((b) => b.permissionCode));
|
||
const foundCodes = new Set(found.keys());
|
||
|
||
const missingInCatalog = [...foundCodes].filter((c) => !catCodes.has(c));
|
||
const extraInCatalog = [...catCodes].filter((c) => !foundCodes.has(c));
|
||
const nameDiffs = [];
|
||
for (const b of cat) {
|
||
const f = found.get(b.permissionCode);
|
||
if (!f) continue;
|
||
const hint = majority(f.displayHints);
|
||
if (hint && hint !== b.name && !hint.startsWith('order.') && !hint.startsWith('warehouse.') && !hint.startsWith('channel.') && !hint.startsWith('common.')) {
|
||
nameDiffs.push({ code: b.permissionCode, catalog: b.name, page: hint });
|
||
}
|
||
}
|
||
|
||
if (!missingInCatalog.length && !extraInCatalog.length && !nameDiffs.length) {
|
||
console.log(`## ${parent} OK (${cat.length} buttons, ${foundCodes.size} used in pages)`);
|
||
continue;
|
||
}
|
||
|
||
console.log(`## ${parent}`);
|
||
console.log(` catalog=${cat.length} pageUsed=${foundCodes.size}`);
|
||
if (missingInCatalog.length) {
|
||
console.log(' [catalog缺少 / 页面有]');
|
||
missingInCatalog.forEach((c) => {
|
||
const f = found.get(c);
|
||
console.log(` + ${c} pageHint=${majority(f.displayHints) || '-'} @${f.files[0]}`);
|
||
});
|
||
}
|
||
if (extraInCatalog.length) {
|
||
console.log(' [catalog多余 / 页面未见 hasPermission]');
|
||
extraInCatalog.forEach((c) => {
|
||
const b = cat.find((x) => x.permissionCode === c);
|
||
console.log(` - ${c} name=${b?.name}`);
|
||
});
|
||
}
|
||
if (nameDiffs.length) {
|
||
console.log(' [名称不一致]');
|
||
nameDiffs.forEach((d) => console.log(` ~ ${d.code}: catalog="${d.catalog}" page="${d.page}"`));
|
||
}
|
||
console.log('');
|
||
}
|