front/scripts/scan-page-buttons-deep.mjs
zzh f1d219ae48 feat: ship billing UI for additional services, conditions, and fee insights
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>
2026-08-01 18:09:22 +08:00

257 lines
8.8 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.

/**
* 深度扫描前端页面按钮(含同目录 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', '<', '×', '脳', '&lt;',
]);
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);