front/scripts/scan-module-buttons.mjs
zzh b011a5caee
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m33s
feat: 下单申报表头配置与产品线路排序,完善自动触发、店铺订单与权限相关能力
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 14:03:30 +08:00

153 lines
5.7 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.

/**
* 扫描订单/仓储/渠道页面 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('');
}