Add product/channel billing condition editors, place-order additional services, billing simulator, forecast-to-inbound fee delta and margin warnings, insurance-aware claim tips, OMS preset dict seeds, and related AI skills/system pages. Co-authored-by: Cursor <cursoragent@cursor.com>
257 lines
8.8 KiB
JavaScript
257 lines
8.8 KiB
JavaScript
/**
|
||
* 深度扫描前端页面按钮(含同目录 components),对比 pageButtonCatalog 输出遗漏
|
||
* node scripts/scan-page-buttons-deep.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 srcDir = path.join(root, 'src');
|
||
const catalogPath = path.join(srcDir, 'constants/pageButtonCatalog.ts');
|
||
const reportPath = path.join(root, 'scripts/scan-report.txt');
|
||
|
||
const SKIP_VIEWS = new Set([
|
||
'login', 'redirect', 'not-found', 'exception', 'result', 'form', 'list',
|
||
'wechat', 'h5', 'home/workplace', 'home/monitor', 'track/trackGuide',
|
||
'user/profile', 'xhs/agentPair', 'system/permission/button',
|
||
]);
|
||
|
||
/** viewKey -> menu permissionCode(generate-menu-excel.js) */
|
||
const VIEW_TO_MENU_PARENT = {
|
||
'api/upstreamInterface': 'Api.UpstreamInterface',
|
||
'api/downstreamInterface': 'Api.DownstreamInterface',
|
||
'api/appList': 'Api.AppList',
|
||
'api/requestLog': 'Api.RequestLog',
|
||
'api/callStatistics': 'Api.CallStatistics',
|
||
'api/apiDebugger': 'Api.ApiDebugger',
|
||
'api/apiTest': 'Api.ApiTest',
|
||
'track/trackOrder': 'Track.Order',
|
||
'track/trackWarningOrder': 'Track.WarningOrder',
|
||
'track/trackState': 'Track.State',
|
||
'track/trackFlag': 'Track.Flag',
|
||
'track/trackContent': 'Track.Content',
|
||
'track/kpiRegion': 'Track.KpiRegion',
|
||
'track/kpiEarlyWarningConfig': 'Track.KpiEarlyWarningConfig',
|
||
'order/priceCheck': 'Order.PriceCheck',
|
||
'order/placeOrder': 'Order.PlaceOrder',
|
||
'order/myOrders': 'Order.MyOrders',
|
||
'channel/salesProducts': 'Channel.SalesProducts',
|
||
'channel/serviceChannels': 'Channel.ServiceChannels',
|
||
'channel/priceArea': 'Channel.PriceArea',
|
||
'channel/blacklist': 'Channel.Blacklist',
|
||
'channel/remoteZones': 'Channel.RemoteZones',
|
||
'channel/weightRules': 'Channel.WeightRules',
|
||
'channel/weightRounding': 'Channel.WeightRounding',
|
||
'channel/autoTrigger': 'Channel.AutoTrigger',
|
||
'channel/issueTemplate': 'Channel.IssueTemplate',
|
||
'channel/labelDesign': 'Channel.LabelDesign',
|
||
'operation/inboundScan': 'Operation.InboundScan',
|
||
'operation/outboundScan': 'Operation.OutboundScan',
|
||
'operation/printLabel': 'Operation.PrintLabel',
|
||
'user/customer': 'User.Customer',
|
||
'user/sub-account': 'User.SubAccount',
|
||
'user/recharge/deposit': 'User.Recharge',
|
||
'finance/rechargeRecord': 'Finance.RechargeRecord',
|
||
'finance/accountFlow': 'Finance.AccountFlow',
|
||
'finance/receivablePayable': 'Finance.ReceivablePayable',
|
||
'system/permission/menu': 'Rbac.Menus',
|
||
'system/permission/role': 'Rbac.Roles',
|
||
'system/organization': 'Rbac.Organizations',
|
||
'system/organization/user': 'Rbac.Users',
|
||
'system/dataPerm/templates': 'Rbac.DataPerm.Template',
|
||
'system/dataPerm/audit': 'Rbac.DataPerm.Audit',
|
||
'system/dataPerm/users': 'Rbac.DataPerm',
|
||
'system/announcement': 'System.Announcement',
|
||
'system/dict': 'System.Dict',
|
||
'system/warehouseAddress': 'System.WarehouseAddress',
|
||
'system/otherConfig': 'System.OtherConfig',
|
||
'system/batchEmail': 'System.BatchEmail',
|
||
'system/aiAssistant': 'System.Ai',
|
||
'system/aiSkills': 'System.AiSkill',
|
||
'xhs/startService': 'Xhs.Agent',
|
||
'xhs/publishHistory': 'Xhs.Publish',
|
||
'xhs/skills': 'Xhs.Skills',
|
||
'xhs/humanSettings': 'Xhs.HumanSettings',
|
||
'xhs/analysis': 'Xhs.Analysis',
|
||
'xhs/creation': 'Xhs.Creation',
|
||
};
|
||
|
||
function walk(dir, ext, out = []) {
|
||
if (!fs.existsSync(dir)) return out;
|
||
for (const name of fs.readdirSync(dir)) {
|
||
const full = path.join(dir, name);
|
||
if (fs.statSync(full).isDirectory()) walk(full, ext, out);
|
||
else if (full.endsWith(ext)) out.push(full);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function viewKey(file) {
|
||
const rel = path.relative(path.join(srcDir, 'views'), file).replace(/\\/g, '/');
|
||
return rel.replace(/\/index\.vue$/, '');
|
||
}
|
||
|
||
function loadLocaleMap() {
|
||
const text = fs.readFileSync(path.join(srcDir, 'locale/zh-CN.ts'), 'utf8');
|
||
const map = new Map();
|
||
const re = /'([^']+)':\s*'((?:\\'|[^'])*)'/g;
|
||
let m;
|
||
while ((m = re.exec(text))) {
|
||
map.set(m[1], m[1].replace(/\\'/g, "'"));
|
||
map.set(m[1], m[2].replace(/\\'/g, "'"));
|
||
}
|
||
return map;
|
||
}
|
||
|
||
function resolveI18n(raw, localeMap) {
|
||
const key = raw.trim();
|
||
return localeMap.get(key) || key.split('.').pop() || key;
|
||
}
|
||
|
||
function extractButtonsFromVue(text, localeMap) {
|
||
const labels = new Set();
|
||
const noise = new Set([
|
||
'template', 'div', 'slot', 'icon', 'span', 'KG', 'name', 'save', 'import', 'send',
|
||
'track.region.actions.add', '<', '×', '脳', '<',
|
||
]);
|
||
|
||
const i18nPatterns = [
|
||
/<a-button[^>]*>\s*\{\{\s*\$t\(['"]([^'"]+)['"]\)/gi,
|
||
/<a-button[^>]*>\s*\{\{\s*t\(['"]([^'"]+)['"]\)/gi,
|
||
/<a-link[^>]*>\s*\{\{\s*\$t\(['"]([^'"]+)['"]\)/gi,
|
||
/<a-link[^>]*>\s*\{\{\s*t\(['"]([^'"]+)['"]\)/gi,
|
||
/ok-text="([^"]+)"/gi,
|
||
/cancel-text="([^"]+)"/gi,
|
||
/:title="t\(['"]([^'"]+)['"]\)/g,
|
||
/title="t\(['"]([^'"]+)['"]\)/g,
|
||
];
|
||
|
||
for (const re of i18nPatterns) {
|
||
let m;
|
||
while ((m = re.exec(text))) {
|
||
const label = resolveI18n(m[1], localeMap);
|
||
if (label && label.length <= 24 && !noise.has(label)) labels.add(label);
|
||
}
|
||
}
|
||
|
||
const plainPatterns = [
|
||
/<a-button[^>]*>([^<{]+)</gi,
|
||
/<a-link[^>]*>([^<{]+)</gi,
|
||
/<button[^>]*>([^<{]+)<\/button/gi,
|
||
/>([\u4e00-\u9fa5A-Za-z·]{2,20})<\/(?:a-button|button)/g,
|
||
];
|
||
|
||
for (const re of plainPatterns) {
|
||
let m;
|
||
while ((m = re.exec(text))) {
|
||
let label = m[1].trim();
|
||
if (!label || label.length > 24) continue;
|
||
if (/^[\d\s{}/.$]+$/.test(label)) continue;
|
||
if (noise.has(label)) continue;
|
||
if (label.startsWith('order.') || label.startsWith('system.') || label.startsWith('channel.')) {
|
||
label = resolveI18n(label, localeMap);
|
||
}
|
||
labels.add(label);
|
||
}
|
||
}
|
||
|
||
return [...labels].sort();
|
||
}
|
||
|
||
function collectViewVueFiles(viewKeyStr) {
|
||
const viewDir = path.join(srcDir, 'views', ...viewKeyStr.split('/'));
|
||
const files = [];
|
||
const indexFile = path.join(viewDir, 'index.vue');
|
||
if (fs.existsSync(indexFile)) files.push(indexFile);
|
||
const compDir = path.join(viewDir, 'components');
|
||
if (fs.existsSync(compDir)) {
|
||
walk(compDir, '.vue', files);
|
||
}
|
||
// sibling vue in view folder (e.g. trackWarningOrder/*.vue)
|
||
if (fs.existsSync(viewDir) && fs.statSync(viewDir).isDirectory()) {
|
||
for (const name of fs.readdirSync(viewDir)) {
|
||
const full = path.join(viewDir, name);
|
||
if (full.endsWith('.vue') && full !== indexFile) files.push(full);
|
||
}
|
||
}
|
||
return files;
|
||
}
|
||
|
||
function parseExistingCatalog() {
|
||
const text = fs.readFileSync(catalogPath, 'utf8');
|
||
const parents = new Set();
|
||
const parentRe = /B\('([^']+)'/g;
|
||
let m;
|
||
while ((m = parentRe.exec(text))) parents.add(m[1]);
|
||
|
||
const buttonNames = new Set();
|
||
const nameRe = /name:\s*'([^']+)'/g;
|
||
while ((m = nameRe.exec(text))) buttonNames.add(m[1]);
|
||
|
||
return { parents, buttonNames };
|
||
}
|
||
|
||
const localeMap = loadLocaleMap();
|
||
const existing = parseExistingCatalog();
|
||
|
||
const viewFiles = walk(path.join(srcDir, 'views'), '.vue').filter((f) => {
|
||
const normalized = f.replace(/\\/g, '/');
|
||
if (!normalized.endsWith('/index.vue')) return false;
|
||
const key = viewKey(f);
|
||
if (SKIP_VIEWS.has(key)) return false;
|
||
if (SKIP_VIEWS.has(key.split('/')[0])) return false;
|
||
return true;
|
||
});
|
||
|
||
const report = [];
|
||
|
||
for (const file of viewFiles.sort()) {
|
||
const key = viewKey(file);
|
||
if (SKIP_VIEWS.has(key)) continue;
|
||
|
||
const parent = VIEW_TO_MENU_PARENT[key] || null;
|
||
const vueFiles = collectViewVueFiles(key);
|
||
const allButtons = new Set();
|
||
for (const vf of vueFiles) {
|
||
const text = fs.readFileSync(vf, 'utf8');
|
||
extractButtonsFromVue(text, localeMap).forEach((b) => allButtons.add(b));
|
||
}
|
||
const buttons = [...allButtons].sort();
|
||
|
||
const inCatalog = parent && existing.parents.has(parent);
|
||
const missingButtons = buttons.filter((b) => !existing.buttonNames.has(b));
|
||
|
||
if (!parent || !inCatalog || missingButtons.length > 0) {
|
||
report.push({ view: key, parent, inCatalog: !!inCatalog, buttons, missingButtons });
|
||
}
|
||
}
|
||
|
||
const lines = [];
|
||
lines.push('# 深度扫描报告');
|
||
lines.push('');
|
||
lines.push(`已收录 parent 数: ${existing.parents.size}`);
|
||
lines.push(`扫描页面数: ${viewFiles.length}`);
|
||
lines.push(`需补充/遗漏: ${report.length}`);
|
||
lines.push('');
|
||
|
||
for (const row of report) {
|
||
lines.push(`## ${row.view}`);
|
||
lines.push(` parent: ${row.parent || '(未映射)'}`);
|
||
lines.push(` inCatalog: ${row.inCatalog}`);
|
||
lines.push(` 页面按钮: ${row.buttons.join(' | ') || '(无)'}`);
|
||
if (row.missingButtons.length) {
|
||
lines.push(` 目录缺失: ${row.missingButtons.join(' | ')}`);
|
||
}
|
||
lines.push('');
|
||
}
|
||
|
||
const missingParents = [...new Set(report.filter((r) => !r.inCatalog && r.parent).map((r) => r.parent))].sort();
|
||
lines.push('# 完全未收录的 parentPermissionCode');
|
||
missingParents.forEach((p) => lines.push(` - ${p}`));
|
||
|
||
const output = lines.join('\n');
|
||
fs.writeFileSync(reportPath, `\uFEFF${output}`, 'utf8');
|
||
console.log(output);
|