#!/usr/bin/env node /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import assert from 'node:assert/strict' import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' const scriptPath = fileURLToPath(import.meta.url) const appRoot = path.resolve(path.dirname(scriptPath), '..') async function source(file) { return readFile(path.join(appRoot, file), 'utf8') } async function sourceFiles(dir) { const entries = await readdir(dir, { withFileTypes: true }) const files = await Promise.all(entries.map(async (entry) => { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) return sourceFiles(fullPath) return entry.name.endsWith('.vue') ? [fullPath] : [] })) return files.flat() } const appVue = await source('src/App.vue') const routerTs = await source('src/router/index.ts') const shellVue = await source('src/components/erp/ErpAppShell.vue') const dashboardVue = await source('src/views/DashboardView.vue') const loginVue = await source('src/views/LoginView.vue') const moduleVue = await source('src/views/ModuleWorkspaceView.vue') const businessPageVue = await source('src/views/BusinessPageView.vue') const notFoundVue = await source('src/views/NotFoundView.vue') const productAdminVue = await source('src/views/ProductAdminView.vue') const orderAdminVue = await source('src/views/OrderAdminView.vue') const partyAdminVue = await source('src/views/PartyAdminView.vue') const accountingAdminVue = await source('src/views/AccountingAdminView.vue') const inventoryAdminVue = await source('src/views/InventoryAdminView.vue') const manufacturingAdminVue = await source('src/views/ManufacturingAdminView.vue') const humanResAdminVue = await source('src/views/HumanResAdminView.vue') const salesAdminVue = await source('src/views/SalesAdminView.vue') const procurementAdminVue = await source('src/views/ProcurementAdminView.vue') const scrumAdminVue = await source('src/views/ScrumAdminView.vue') const operationsAdminVue = await source('src/views/OperationsAdminView.vue') const contentAdminVue = await source('src/views/ContentAdminView.vue') const marketingAdminVue = await source('src/views/MarketingAdminView.vue') const commerceAdminVue = await source('src/views/CommerceAdminView.vue') const posAdminVue = await source('src/views/PosAdminView.vue') const marketplaceAdminVue = await source('src/views/MarketplaceAdminView.vue') const analyticsAdminVue = await source('src/views/AnalyticsAdminView.vue') const systemVue = await source('src/views/SystemToolsView.vue') const securityAdminVue = await source('src/views/SecurityAdminView.vue') const systemOperationsVue = await source('src/views/SystemOperationsView.vue') const businessCenterVue = await source('src/views/BusinessCenterView.vue') const moduleCatalog = await source('src/data/moduleCatalog.ts') const modernCss = await source('src/styles/modern.css') const apiTs = await source('src/services/api.ts') const apiTypes = await source('src/types/api.ts') const domainAdminVue = await source('src/components/erp/ErpDomainAdminView.vue') const adminSurfaceVueFiles = [ ...(await sourceFiles(path.join(appRoot, 'src/views'))), ...(await sourceFiles(path.join(appRoot, 'src/components/erp'))) ] const directElementTabs = [] for (const file of adminSurfaceVueFiles) { const relativeFile = path.relative(appRoot, file) if (relativeFile === 'src/components/erp/ErpTabbedDataPanel.vue') continue const content = await readFile(file, 'utf8') if (/<(?:el-tabs|el-tab-pane|ElTabs|ElTabPane)\b/.test(content)) { directElementTabs.push(relativeFile) } } assert.deepEqual( directElementTabs.sort(), [], 'business views and ERP workspaces must use ErpTabbedDataPanel instead of direct Element Plus tabs' ) async function collectDirectElementUsage(pattern, allowedWrapperFiles = []) { const allowedFiles = new Set(allowedWrapperFiles) const usage = {} for (const file of adminSurfaceVueFiles) { const relativeFile = path.relative(appRoot, file) if (allowedFiles.has(relativeFile)) continue const content = await readFile(file, 'utf8') const count = content.match(pattern)?.length || 0 if (count > 0) usage[relativeFile] = count } return Object.fromEntries(Object.entries(usage).sort(([left], [right]) => left.localeCompare(right))) } function assertNoExpandedDirectElementUsage(actualUsage, allowedBaseline, message) { const unexpectedFiles = Object.keys(actualUsage).filter((file) => !(file in allowedBaseline)) const expandedFiles = Object.entries(actualUsage) .filter(([file, count]) => count > allowedBaseline[file]) .map(([file, count]) => `${file} (${count} > ${allowedBaseline[file]})`) assert.deepEqual(unexpectedFiles, [], `${message}: unexpected direct Element Plus files`) assert.deepEqual(expandedFiles, [], `${message}: direct Element Plus baseline expanded`) } const directElementTables = await collectDirectElementUsage( /<(?:el-table|el-table-column|ElTable|ElTableColumn)\b/g, ['src/components/erp/ErpDataTable.vue'] ) const directElementTableBaseline = { 'src/components/erp/ErpFinanceOperationsWorkspace.vue': 23, 'src/components/erp/ErpOrderWorkspace.vue': 28, 'src/views/BusinessCenterView.vue': 8, 'src/views/BusinessPageView.vue': 34 } assertNoExpandedDirectElementUsage( directElementTables, directElementTableBaseline, 'business views and ERP workspaces should move direct Element Plus tables into ErpDataTable' ) const directElementForms = await collectDirectElementUsage( /<(?:el-form|el-form-item|ElForm|ElFormItem)\b/g, ['src/components/erp/ErpEntityForm.vue', 'src/components/erp/ErpSearchForm.vue'] ) assertNoExpandedDirectElementUsage( directElementForms, {}, 'business views and ERP workspaces must use ERP form wrappers instead of direct Element Plus forms' ) function assertEntitySource(content, entityName, message) { const directEntityLoad = new RegExp(`getEntityRows\\('${entityName}'`) const wrapperEntitySource = new RegExp(`entityName: '${entityName}'`) assert.ok(directEntityLoad.test(content) || wrapperEntitySource.test(content), message) } function escapeRegExp(text) { return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } // Routing moved out of App.vue's hand-rolled hash dispatch into the central // vue-router table (src/router/index.ts). Route paths there have no leading // '#'. This asserts that the same `#/X` deep link still resolves to the same // view component, now via the router record instead of the old activeView chain. function assertRouteMapsToView(route, component, message) { const routePath = escapeRegExp(route.replace(/^#/, '')) const pattern = new RegExp(`path:\\s*'${routePath}'[\\s\\S]*?import\\('\\.\\./views/${component}\\.vue'\\)`) assert.match(routerTs, pattern, message) } const visibleUiLeakageTerms = [ ['component gallery copy', /组件展厅|组件规范|排版规则|全量清单/], ['preview copy', /技术预览|>\s*预览\s*<|title="预览"|预览模式|页面预览/], ['migration copy', /迁移|自动迁移|旧入口|旧 URL|旧页面|历史地址|旧屏幕来源|旧 widget/i], ['parity copy', /业务等价|功能等价|旧新报表比对|逐页比对|真实输出比对/], ['acceptance copy', /验收|待验收|验收清单|业务回归|待业务回归/], ['inventory checklist copy', /页面清单|覆盖率|覆盖台账|待补齐|待输出比对/], ['engineering metadata copy', /页面定义|页面结构|动作契约|控制规则|模板适配|适配器已覆盖|前端已重写/] ] function assertNoVisibleUiLeakage(surfaces) { for (const [fileName, content] of surfaces) { for (const [label, pattern] of visibleUiLeakageTerms) { assert.doesNotMatch( content, pattern, `${fileName} must not expose ${label} in visible administrator product UI` ) } } } const postLoginCoreModuleAnchors = [ ['#/orders', 'order-admin', 'OrderAdminView', 'OrderAdminView.vue', orderAdminVue, ['订单运营台', '订单业务数据'], ['OrderHeader', 'OrderItem', 'Shipment']], ['#/catalog/products', 'product-admin', 'ProductAdminView', 'ProductAdminView.vue', productAdminVue, ['商品管理台', '商品业务数据'], ['Product', 'ProductCategory', 'ProductPrice']], ['#/parties', 'party-admin', 'PartyAdminView', 'PartyAdminView.vue', partyAdminVue, ['客户与组织管理台', '客户业务数据'], ['Party', 'Person', 'PartyRole']], ['#/sales', 'sales-admin', 'SalesAdminView', 'SalesAdminView.vue', salesAdminVue, ['销售管理台', '销售业务数据'], ['SalesOpportunity', 'SalesForecast', 'CommunicationEvent']], ['#/procurement', 'procurement-admin', 'ProcurementAdminView', 'ProcurementAdminView.vue', procurementAdminVue, ['采购管理台', '采购业务数据'], ['Requirement', 'SupplierProduct', 'Vendor']], ['#/accounting', 'accounting-admin', 'AccountingAdminView', 'AccountingAdminView.vue', accountingAdminVue, ['财务管理台', '财务业务数据'], ['Invoice', 'Payment', 'AcctgTrans']], ['#/facility', 'inventory-admin', 'InventoryAdminView', 'InventoryAdminView.vue', inventoryAdminVue, ['库存管理台', '库存业务数据'], ['InventoryItem', 'Facility', 'Shipment']], ['#/manufacturing', 'manufacturing-admin', 'ManufacturingAdminView', 'ManufacturingAdminView.vue', manufacturingAdminVue, ['生产管理台', '生产业务数据'], ['WorkEffort', 'Requirement', 'CostComponent']], ['#/humanres', 'humanres-admin', 'HumanResAdminView', 'HumanResAdminView.vue', humanResAdminVue, ['人事管理台', '人事业务数据'], ['Person', 'Employment', 'EmplPosition']] ] function assertPostLoginProductAnchors() { assert.match(appVue, /\s*入口\s* \[[\s\S]*modules: primaryBusinessModules\.value[\s\S]*modules: secondaryBusinessModules\.value/, 'authorized business navigation must be built from the moduleCatalog primary/secondary groups' ) assert.match( shellVue, /moduleItemIndex\(module, toPath\(moduleLandingPath\(module\)\)\)/, 'authorized business-domain links must use dedicated administrator routes (moduleLandingPath), not backend modernPath or generated page-shell anchors' ) assert.doesNotMatch( shellVue, /const path = matchedItems\[0\] \? normalizedModernPath\(matchedItems\[0\]\) : moduleLandingPath\(module\)/, 'authorized business-domain links must not prefer backend navigation paths over real administrator routes' ) assert.doesNotMatch( shellVue, /#\/pages\/\$\{item\.id\}__main|function normalizedModernPath/, 'application shell must not generate backend page-shell anchors for menu navigation' ) for (const [pattern, message] of [ [ /v-for="module in group\.modules"[\s\S]*:index="moduleItemIndex\(module, toPath\(moduleLandingPath\(module\)\)\)"/, 'sidebar module menu landing items should use module landing routes' ], [ /v-for="page in module\.quickPages"[\s\S]*:index="moduleItemIndex\(module, `\/pages\/\$\{page\.pageId\}`\)"/, 'sidebar module quick-page items should link to their generated business pages' ], [ /path: toPath\(moduleLandingPath\(module\)\)/, 'topbar work-queue links should use module landing routes' ], [ /v-for="group in navGroups"[\s\S]*:title="group\.title"/, 'authorized business-domain navigation should render each moduleCatalog group through its dedicated nav field' ], [ /trail\.push\(\{ label: module\.navLabel, path: route\.path === landing \? undefined : landing \}\)/, 'module breadcrumb links should return to the dedicated administrator route' ] ]) { assert.match(shellVue, pattern, message) } assert.match( shellVue, /const secondaryBusinessModules = computed\(\(\) => secondaryModules\.map\(\(id\) => moduleConfigMap\[id\]\)\.filter\(Boolean\)\)/, 'secondary business navigation should be driven by the moduleCatalog secondary group, including the standalone system entry under governance' ) for (const text of ['getModuleSummary', 'moduleSummary', 'moduleEntityMetrics', 'pendingQueues', 'processingLanes', '业务处理中枢', '业务队列', '处理流', '交接记录', '执行动作', '处理事项', '运行状态', '异常交接']) { assert.match(moduleVue, new RegExp(text), `module workspace must behave like an ERP operating console: ${text}`) } assert.doesNotMatch( moduleVue, /props\.inventory|parityManifest|ParityPage|modulePages|filteredPages|workflowRows|scenarioColumns|businessParityStatus|parityStatus|frontendRewriteStatus|uiRewriteStatus|scenarioStatus|pending-business-e2e|ready-for-business-e2e|risk ===|page\.risk|highRiskPages|highRisk|高风险|待生成/, 'module workspaces must not expose migration, parity, or generated-risk semantics' ) assert.doesNotMatch( moduleVue, /搜索当前模块页面、流程、旧入口|模块页面|页面标识|旧入口|直接进入真实 OFBiz 业务页面|组件展示页|打开常用入口|常用操作|最近记录|真实 OFBiz 模块数据|模块数据源|模块数据|数据源|没有匹配的常用操作/, 'module workspaces must not read like route inventories or migration catalogs' ) assert.match( moduleCatalog, /queueLabels|scenarioColumnLabels|primaryModules|secondaryModules/, 'module catalog should provide business navigation groupings and queue labels' ) assert.match( moduleCatalog, /id: 'extensions'[\s\S]*landingPath: '#\/extensions'/, 'configuration module should land on the dedicated administrator page, not the generic module workspace' ) assert.match(routerTs, /ProductAdminView/, 'administrator site should include a dedicated product administration view') assertRouteMapsToView('#/catalog/products', 'ProductAdminView', 'product administration should have a dedicated catalog route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/catalog\/products/, 'product administration should be reachable from the shell, dashboard, or catalog module') for (const text of ['商品管理台', '商品资料', '目录分类', '价格规则', '促销活动', '库存状态', '商品业务数据']) { assert.match(productAdminVue, new RegExp(text), `product administration page should expose administrator operation: ${text}`) } for (const entityName of ['Product', 'ProductCategory', 'ProductPrice', 'ProductPromo', 'InventoryItem']) { assertEntitySource(productAdminVue, entityName, `product administration page should load real ${entityName} rows`) } assert.doesNotMatch( productAdminVue, /Demo[A-Za-z0-9_]*|SKU-\d+|PROD-\d+|RetailStore|BigSupplier|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'product administration page should not expose fake product rows or engineering language' ) assert.match(routerTs, /OrderAdminView/, 'administrator site should include a dedicated order operations view') assertRouteMapsToView('#/orders', 'OrderAdminView', 'order operations should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/orders/, 'order operations should be reachable from the shell, dashboard, or order module') for (const text of ['订单运营台', '订单执行台', '待审核订单', '待履约订单', '付款关注', '退货风险', '今日处理流', '交接记录', '订单队列', '订单明细', '履约与发运', '退货授权', '订单业务数据']) { assert.match(orderAdminVue, new RegExp(text), `order operations page should expose administrator operation: ${text}`) } for (const text of ['orderExecutionRows', 'orderFlowRows', 'orderHandoffRows', 'orderRiskRows', 'order-admin-execution', 'order-admin-flow-row', 'order-admin-handoff-row', 'order-admin-risk-row']) { assert.match(orderAdminVue + modernCss, new RegExp(text), `order operations page should expose executable order work surfaces: ${text}`) } for (const text of ['ErpTabbedDataPanel', 'orderDataTabs', 'data-modern="order-admin-tabbed-data"', '订单业务数据']) { assert.match(orderAdminVue, new RegExp(text), `order operations page should use the ERP tabbed data panel for real order partitions: ${text}`) } for (const entityName of ['OrderHeader', 'OrderItem', 'OrderStatus', 'Shipment', 'ReturnHeader']) { assert.match(orderAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `order operations page should load real ${entityName} rows`) } assert.doesNotMatch( orderAdminVue, /Demo[A-Za-z0-9_]*|ORD-\d+|SHP-\d+|RET-\d+|SampleCustomer|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'order operations page should not expose fake order rows or engineering language' ) assert.match(routerTs, /PartyAdminView/, 'administrator site should include a dedicated party administration view') assertRouteMapsToView('#/parties', 'PartyAdminView', 'party administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/parties/, 'party administration should be reachable from the shell, dashboard, or party module') for (const text of ['客户与组织管理台', '客户主体', '个人档案', '组织档案', '角色关系', '联系方式', '沟通记录', '客户业务数据']) { assert.match(partyAdminVue, new RegExp(text), `party administration page should expose administrator operation: ${text}`) } for (const entityName of ['Party', 'Person', 'PartyGroup', 'PartyRole', 'ContactMech', 'CommunicationEvent']) { assertEntitySource(partyAdminVue, entityName, `party administration page should load real ${entityName} rows`) } assert.doesNotMatch( partyAdminVue, /Demo[A-Za-z0-9_]*|PARTY-\d+|CUST-\d+|ORG-\d+|Person-\d+|SampleCustomer|example\.com|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'party administration page should not expose fake party rows or engineering language' ) assert.match(routerTs, /AccountingAdminView/, 'administrator site should include a dedicated accounting administration view') assertRouteMapsToView('#/accounting', 'AccountingAdminView', 'accounting administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/accounting/, 'accounting administration should be reachable from the shell, dashboard, or accounting module') for (const text of ['财务管理台', '财务结账台', '应收跟进', '应付安排', '收付款匹配', '核销队列', '现金流关注', '财务异常', '本期结账', '发票队列', '付款收款', '会计凭证', '凭证明细', '总账科目', '核销处理', '账龄风险', '财务业务数据']) { assert.match(accountingAdminVue, new RegExp(text), `accounting administration page should expose administrator operation: ${text}`) } for (const text of ['financeCloseRows', 'receivableRows', 'payableRows', 'reconciliationRows', 'cashAttentionRows', 'financeExceptionRows', 'accounting-admin-close-grid', 'accounting-admin-work-row', 'accounting-admin-exception-row']) { assert.match(accountingAdminVue + modernCss, new RegExp(text), `accounting administration page should expose finance operator work surfaces: ${text}`) } for (const entityName of ['Invoice', 'Payment', 'AcctgTrans', 'AcctgTransEntry', 'GlAccount']) { assert.match(accountingAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `accounting administration page should load real ${entityName} rows`) } assert.doesNotMatch( accountingAdminVue, /Demo[A-Za-z0-9_]*|INV-\d+|PAY-\d+|GL-\d+|ACCT-\d+|SampleCustomer|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'accounting administration page should not expose fake finance rows or engineering language' ) assert.match(routerTs, /InventoryAdminView/, 'administrator site should include a dedicated inventory administration view') assertRouteMapsToView('#/facility', 'InventoryAdminView', 'inventory administration should have a dedicated facility route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/facility/, 'inventory administration should be reachable from the shell, dashboard, or facility module') for (const text of ['库存管理台', '库存执行台', '收货上架', '可用量关注', '调拨跟进', '盘点差异', '库存异常', '库位交接', '库存项', '设施库位', '收货发运', '库存明细', '库存调拨', '盘点调整', '库存风险', '库存业务数据']) { assert.match(inventoryAdminVue, new RegExp(text), `inventory administration page should expose administrator operation: ${text}`) } for (const text of ['inventoryExecutionRows', 'receivingWorkRows', 'availabilityRows', 'transferWorkRows', 'countVarianceRows', 'inventoryExceptionRows', 'inventory-admin-execution-grid', 'inventory-admin-work-row', 'inventory-admin-risk-row']) { assert.match(inventoryAdminVue + modernCss, new RegExp(text), `inventory administration page should expose warehouse operator work surfaces: ${text}`) } for (const entityName of ['InventoryItem', 'Facility', 'FacilityLocation', 'Shipment', 'InventoryItemDetail', 'InventoryTransfer', 'PhysicalInventory']) { assert.match(inventoryAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `inventory administration page should load real ${entityName} rows`) } assert.doesNotMatch( inventoryAdminVue, /Demo[A-Za-z0-9_]*|INV-\d+|FAC-\d+|SHP-\d+|LOC-\d+|TRF-\d+|SampleWarehouse|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'inventory administration page should not expose fake inventory rows or engineering language' ) assert.match(routerTs, /ManufacturingAdminView/, 'administrator site should include a dedicated manufacturing administration view') assertRouteMapsToView('#/manufacturing', 'ManufacturingAdminView', 'manufacturing administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/manufacturing/, 'manufacturing administration should be reachable from the shell, dashboard, or manufacturing module') for (const text of ['生产管理台', '生产调度台', '排产跟进', '物料缺口', '工序准备', '发料领用', '成本关注', '生产异常', '生产运行', '物料需求', 'BOM 工艺', '工序任务', '成本构成', '生产风险', '生产业务数据']) { assert.match(manufacturingAdminVue, new RegExp(text), `manufacturing administration page should expose administrator operation: ${text}`) } for (const text of ['manufacturingScheduleRows', 'materialShortageRows', 'routingPreparationRows', 'issuanceWorkRows', 'manufacturingExceptionRows', 'manufacturing-admin-execution-grid', 'manufacturing-admin-work-row', 'manufacturing-admin-risk-row']) { assert.match(manufacturingAdminVue + modernCss, new RegExp(text), `manufacturing administration page should expose production operator work surfaces: ${text}`) } for (const entityName of ['WorkEffort', 'Requirement', 'ProductAssoc', 'WorkEffortGoodStandard', 'CostComponent', 'ItemIssuance']) { assert.match(manufacturingAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `manufacturing administration page should load real ${entityName} rows`) } assert.doesNotMatch( manufacturingAdminVue, /Demo[A-Za-z0-9_]*|MFG-\d+|RUN-\d+|BOM-\d+|MAT-\d+|WORK-\d+|SampleFactory|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'manufacturing administration page should not expose fake manufacturing rows or engineering language' ) assert.match(routerTs, /HumanResAdminView/, 'administrator site should include a dedicated human resources administration view') assertRouteMapsToView('#/humanres', 'HumanResAdminView', 'human resources administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/humanres/, 'human resources administration should be reachable from the shell, dashboard, or humanres module') for (const text of ['人事管理台', '人事执行台', '入转离跟进', '岗位空缺', '招聘处理', '绩效复核', '技能资质', '人事异常', '任职交接', '员工档案', '雇佣关系', '岗位编制', '招聘申请', '人事风险', '人事业务数据']) { assert.match(humanResAdminVue, new RegExp(text), `human resources administration page should expose administrator operation: ${text}`) } for (const text of ['humanResExecutionRows', 'employmentLifecycleRows', 'positionVacancyRows', 'recruitingWorkRows', 'performanceReviewRows', 'skillCredentialRows', 'humanResExceptionRows', 'humanres-admin-execution-grid', 'humanres-admin-work-row', 'humanres-admin-risk-row']) { assert.match(humanResAdminVue + modernCss, new RegExp(text), `human resources administration page should expose HR operator work surfaces: ${text}`) } for (const entityName of ['Person', 'Employment', 'EmplPosition', 'EmplPositionFulfillment', 'EmploymentApp', 'JobRequisition', 'PerfReview', 'PartySkill']) { assert.match(humanResAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `human resources administration page should load real ${entityName} rows`) } assert.doesNotMatch( humanResAdminVue, /Demo[A-Za-z0-9_]*|EMP-\d+|HR-\d+|POS-\d+|APP-\d+|SampleEmployee|SamplePosition|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'human resources administration page should not expose fake human resources rows or engineering language' ) assert.match(routerTs, /SalesAdminView/, 'administrator site should include a dedicated sales administration view') assertRouteMapsToView('#/sales', 'SalesAdminView', 'sales administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/sales/, 'sales administration should be reachable from the shell, dashboard, or sales module') for (const text of ['销售管理台', '销售执行台', '线索承接', '机会推进', '预测复核', '跟进交接', '客户角色', '销售异常', '销售线索', '机会管道', '销售预测', '跟进事件', '销售风险', '销售业务数据']) { assert.match(salesAdminVue, new RegExp(text), `sales administration page should expose administrator operation: ${text}`) } for (const text of ['salesExecutionRows', 'leadHandoffRows', 'opportunityPipelineRows', 'forecastReviewRows', 'communicationFollowupRows', 'salesExceptionRows', 'sales-admin-execution-grid', 'sales-admin-work-row', 'sales-admin-risk-row']) { assert.match(salesAdminVue + modernCss, new RegExp(text), `sales administration page should expose sales operator work surfaces: ${text}`) } for (const entityName of ['SalesOpportunity', 'SalesOpportunityStage', 'SalesOpportunityRole', 'SalesForecast', 'SalesForecastDetail', 'PartyRole', 'CommunicationEvent']) { assert.match(salesAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `sales administration page should load real ${entityName} rows`) } assert.doesNotMatch( salesAdminVue, /Demo[A-Za-z0-9_]*|LEAD-\d+|SFA-\d+|OPP-\d+|FORECAST-\d+|SampleLead|SampleOpportunity|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'sales administration page should not expose fake sales rows or engineering language' ) assert.match(routerTs, /ProcurementAdminView/, 'administrator site should include a dedicated procurement administration view') assertRouteMapsToView('#/procurement', 'ProcurementAdminView', 'procurement administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/procurement/, 'procurement administration should be reachable from the shell, dashboard, or procurement module') for (const text of ['采购管理台', '采购执行台', '需求审批', '询价比价', '供应商风险', '到货接收', '补货交接', '采购异常', '供应商协同', '采购需求', '供应商', '供应商品', '供应商报价', '采购风险', '采购业务数据']) { assert.match(procurementAdminVue, new RegExp(text), `procurement administration page should expose administrator operation: ${text}`) } for (const text of ['procurementExecutionRows', 'approvalWorkRows', 'quoteComparisonRows', 'supplierRiskRows', 'receivingWorkRows', 'procurementHandoffRows', 'procurement-admin-execution-grid', 'procurement-admin-work-row', 'procurement-admin-risk-row']) { assert.match(procurementAdminVue + modernCss, new RegExp(text), `procurement administration page should expose procurement operator work surfaces: ${text}`) } for (const entityName of ['Requirement', 'SupplierProduct', 'Vendor', 'Quote', 'CustRequest', 'Shipment', 'InventoryItem']) { assert.match(procurementAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `procurement administration page should load real ${entityName} rows`) } assert.doesNotMatch( procurementAdminVue, /Demo[A-Za-z0-9_]*|REQ-\d+|PO-\d+|SUP-\d+|VEN-\d+|SampleVendor|SampleSupplier|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'procurement administration page should not expose fake procurement rows or engineering language' ) assert.match(routerTs, /ScrumAdminView/, 'administrator site should include a dedicated Scrum administration view') assertRouteMapsToView('#/scrum', 'ScrumAdminView', 'Scrum administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/scrum/, 'Scrum administration should be reachable from the shell, dashboard, or Scrum module') for (const text of ['敏捷交付管理台', '敏捷执行台', 'Backlog 承接', 'Sprint 推进', '任务交接', '工时复核', '资源协调', '交付异常', '产品 Backlog', 'Sprint 排程', '任务板', '团队资源', '工时确认', '交付风险', '敏捷业务数据']) { assert.match(scrumAdminVue, new RegExp(text), `Scrum administration page should expose administrator operation: ${text}`) } for (const text of ['scrumExecutionRows', 'backlogHandoffRows', 'sprintDeliveryRows', 'sprintTaskHandoffRows', 'timeReviewRows', 'resourceCoordinationRows', 'scrumExceptionRows', 'scrum-admin-execution', 'scrum-admin-work-row', 'scrum-admin-risk-row']) { assert.match(scrumAdminVue + modernCss, new RegExp(text), `Scrum administration page should expose executable delivery work surfaces: ${text}`) } for (const entityName of ['ProductBacklog', 'ProjectSprint', 'ProjectSprintBacklogAndTask', 'Timesheet', 'TimeEntry', 'WorkEffortPartyAssignment', 'CustRequest']) { assert.match(scrumAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `Scrum administration page should load real ${entityName} rows`) } assert.doesNotMatch( scrumAdminVue, /Demo[A-Za-z0-9_]*|SCRUM-\d+|SPRINT-\d+|TASK-\d+|BACKLOG-\d+|SampleSprint|SampleBacklog|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'Scrum administration page should not expose fake agile delivery rows or engineering language' ) assert.match(routerTs, /OperationsAdminView/, 'administrator site should include a dedicated operations administration view') assertRouteMapsToView('#/operations', 'OperationsAdminView', 'operations administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/operations/, 'operations administration should be reachable from the shell, dashboard, or operations module') for (const text of ['运营任务管理台', '运营执行台', '任务承接', '排程依赖', '人员分配', '工时复核', '请求跟进', '沟通交接', '运营异常', '任务排程', '工作分配', '工时表', '请求协同', '沟通记录', '运营风险', '运营业务数据']) { assert.match(operationsAdminVue, new RegExp(text), `operations administration page should expose administrator operation: ${text}`) } for (const text of ['operationsExecutionRows', 'taskHandoffRows', 'dependencyRows', 'assignmentDispatchRows', 'operationsTimeReviewRows', 'requestFollowupRows', 'communicationHandoffRows', 'operationsExceptionRows', 'operations-admin-execution', 'operations-admin-work-row', 'operations-admin-risk-row']) { assert.match(operationsAdminVue + modernCss, new RegExp(text), `operations administration page should expose executable operations work surfaces: ${text}`) } for (const entityName of ['WorkEffort', 'WorkEffortAssoc', 'WorkEffortPartyAssignment', 'Timesheet', 'TimeEntry', 'CustRequest', 'CommunicationEvent']) { assert.match(operationsAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `operations administration page should load real ${entityName} rows`) } assert.doesNotMatch( operationsAdminVue, /Demo[A-Za-z0-9_]*|OPS-\d+|WORK-\d+|TASK-\d+|SampleTask|SampleOperation|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'operations administration page should not expose fake operations rows or engineering language' ) const convertedTabbedAdminExpectations = [ { name: 'inventory administration page', content: inventoryAdminVue, tokens: ['ErpTabbedDataPanel', 'inventoryDataTabs', 'data-modern="inventory-admin-tabbed-data"', '库存业务数据'] }, { name: 'operations administration page', content: operationsAdminVue, tokens: ['ErpTabbedDataPanel', 'operationsDataTabs', 'data-modern="operations-admin-tabbed-data"', '运营业务数据'] }, { name: 'sales administration page', content: salesAdminVue, tokens: ['ErpTabbedDataPanel', 'salesDataTabs', 'data-modern="sales-admin-tabbed-data"', '销售业务数据'] }, { name: 'procurement administration page', content: procurementAdminVue, tokens: ['ErpTabbedDataPanel', 'procurementDataTabs', 'data-modern="procurement-admin-tabbed-data"', '采购业务数据'] }, { name: 'human resources administration page', content: humanResAdminVue, tokens: ['ErpTabbedDataPanel', 'humanResDataTabs', 'data-modern="humanres-admin-tabbed-data"', '人事业务数据'] }, { name: 'Scrum administration page', content: scrumAdminVue, tokens: ['ErpTabbedDataPanel', 'scrumDataTabs', 'data-modern="scrum-admin-tabbed-data"', '敏捷业务数据'] } ] for (const adminPage of convertedTabbedAdminExpectations) { for (const text of adminPage.tokens) { assert.match(adminPage.content, new RegExp(text), `${adminPage.name} should use ErpTabbedDataPanel for real business data partitions: ${text}`) } } const secondaryAdminExpectations = [ { viewName: 'ContentAdminView', content: contentAdminVue, route: '#/content', activeView: 'content-admin', labels: ['内容管理台', '内容资源', '站点管理', 'CMS 树', '论坛消息', '博客文章', '发布风险', '内容业务数据'], executionLabels: ['内容执行台', '处理队列', '复核交接', '风险关注'], entities: ['Content', 'DataResource', 'WebSite', 'WebPage', 'ElectronicText', 'CommunicationEvent'] }, { viewName: 'MarketingAdminView', content: marketingAdminVue, route: '#/marketing', activeView: 'marketing-admin', labels: ['营销管理台', '营销活动', '联系名单', '追踪码', '细分群组', '活动统计', '营销风险', '营销业务数据'], executionLabels: ['营销执行台', '处理队列', '复核交接', '风险关注'], entities: ['MarketingCampaign', 'ContactList', 'TrackingCode', 'SegmentGroup', 'CommunicationEvent', 'PartyRole'] }, { viewName: 'CommerceAdminView', content: commerceAdminVue, route: '#/commerce', activeView: 'commerce-admin', labels: ['电商管理台', '购物车', '会员订单', '商品浏览', '退货请求', '客户资料', '电商风险', '电商业务数据'], executionLabels: ['电商执行台', '处理队列', '复核交接', '风险关注'], entities: ['ShoppingList', 'OrderHeader', 'Product', 'ReturnHeader', 'Party', 'ProductStore'] }, { viewName: 'PosAdminView', content: posAdminVue, route: '#/pos', activeView: 'pos-admin', labels: ['POS 管理台', '门店购物车', '收银订单', '支付记录', '经理授权', '门店库存', '收银风险', 'POS 业务数据'], executionLabels: ['POS 执行台', '处理队列', '复核交接', '风险关注'], entities: ['ShoppingList', 'OrderHeader', 'Payment', 'UserLogin', 'Facility', 'InventoryItem'] }, { viewName: 'MarketplaceAdminView', content: marketplaceAdminVue, route: '#/marketplace', activeView: 'marketplace-admin', labels: ['店铺运营管理台', '店铺配置', '物流方式', '库存同步', '活动刊登', '店铺商品', '店铺风险', '店铺业务数据'], executionLabels: ['店铺执行台', '处理队列', '复核交接', '风险关注'], entities: ['EbayConfig', 'EbayShippingMethod', 'EbayProductStoreInventory', 'EbayProductListing', 'ProductStore', 'Product'] }, { viewName: 'AnalyticsAdminView', content: analyticsAdminVue, route: '#/analytics', activeView: 'analytics-admin', labels: ['报表分析管理台', '数据维度', '事实数据', '报表资源', '报表发布', '输出队列', '报表风险', '报表业务数据'], executionLabels: ['报表执行台', '处理队列', '复核交接', '风险关注'], entities: ['DateDimension', 'ProductDimension', 'SalesOrderItemFact', 'SalesInvoiceItemFact', 'DataResource', 'Enumeration'] } ] for (const adminPage of secondaryAdminExpectations) { assert.match(routerTs, new RegExp(`${adminPage.viewName}`), `${adminPage.viewName} should be mounted as a dedicated administrator page`) assertRouteMapsToView(adminPage.route, adminPage.viewName, `${adminPage.route} should resolve to ${adminPage.activeView}`) assert.match(shellVue + dashboardVue + moduleCatalog, new RegExp(adminPage.route.replace('/', '\\/')), `${adminPage.viewName} should be reachable from administrator navigation`) assert.match(adminPage.content, /ErpDomainAdminView/, `${adminPage.viewName} should use the shared domain admin renderer that loads entity sources`) for (const text of adminPage.labels) { assert.match(adminPage.content, new RegExp(text), `${adminPage.viewName} should expose ERP operation label: ${text}`) } for (const text of adminPage.executionLabels) { assert.match(adminPage.content, new RegExp(text), `${adminPage.viewName} should expose execution-desk label: ${text}`) } for (const entityName of adminPage.entities) { assert.match(adminPage.content, new RegExp(`entityName: '${entityName}'`), `${adminPage.viewName} should declare real ${entityName} rows for the domain renderer`) } assert.doesNotMatch( adminPage.content, /Demo[A-Za-z0-9_]*|Sample[A-Za-z0-9_]*|MOCK-\d+|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, `${adminPage.viewName} should not expose fake rows or engineering language` ) } for (const text of ['executionDeskRows', 'reviewHandoffRows', 'riskFocusRows', 'domainWorkQueueRows', 'recentBusinessRows', 'handoffRows', 'businessActionRows', '业务执行台', '处理队列', '复核交接', '风险关注', '业务队列', '业务记录', '快捷处理', '异常交接', '处理流', '进入处理']) { assert.match(domainAdminVue, new RegExp(text), `shared domain administrator renderer must render a complete ERP operation console: ${text}`) } for (const selector of ['domain-admin-execution-desk', 'domain-admin-execution-row', 'domain-admin-review-list', 'domain-admin-risk-strip']) { assert.match(domainAdminVue + modernCss, new RegExp(selector), `shared domain administrator renderer should provide quiet execution desk layout: ${selector}`) } const domainAdminCssStart = modernCss.indexOf('.domain-admin-hero') const domainAdminCssEnd = modernCss.indexOf('.modern-table-search', domainAdminCssStart) const domainAdminCss = domainAdminCssStart === -1 ? '' : modernCss.slice(domainAdminCssStart, domainAdminCssEnd === -1 ? undefined : domainAdminCssEnd) assert.doesNotMatch( domainAdminCss, /border-left/, 'shared domain administrator layout should use quiet 1px borders or top rules, not colored left bars' ) assert.doesNotMatch( domainAdminVue, /组件展厅|页面清单|业务等价|待验收|迁移|技术预览|预览/, 'shared domain administrator renderer must not expose preview, migration, parity, or component-gallery language' ) assert.match( systemVue, /安全与会话|运行任务|运行日志|缓存维护|导入导出|用户与权限|权限与业务域/, 'system maintenance should expose administrator operations instead of design or validation workbenches' ) assert.doesNotMatch(systemVue, /应用权限|system-app-list|navigation\.slice/, 'system maintenance must organize permissions by ERP business domain, not by the old application list') assert.match(routerTs, /SecurityAdminView/, 'administrator site should include a dedicated security administration view') assertRouteMapsToView('#/system/security', 'SecurityAdminView', 'security administration should have a dedicated system route') assert.match(systemVue, /#\/system\/security/, 'system maintenance should link to the dedicated security administration page') for (const text of ['用户与权限管理', '账号与登录', '安全组', '授权关系', '权限业务数据']) { assert.match(securityAdminVue, new RegExp(text), `security administration page should expose administrator operation: ${text}`) } for (const entityName of ['UserLogin', 'SecurityGroup', 'UserLoginSecurityGroup']) { assertEntitySource(securityAdminVue, entityName, `security administration page should load real ${entityName} rows`) } assert.match(routerTs, /SystemOperationsView/, 'administrator site should include a dedicated system operations view') assertRouteMapsToView('#/system/operations', 'SystemOperationsView', 'system operations should have a dedicated system route') assert.match(systemVue + shellVue, /#\/system\/operations/, 'system maintenance and shell should link to the dedicated system operations page') for (const text of ['系统运行管理', '计划任务', '缓存维护', '导入导出', '运行资源', '运行业务数据']) { assert.match(systemOperationsVue, new RegExp(text), `system operations page should expose administrator operation: ${text}`) } for (const entityName of ['JobSandbox', 'SystemProperty', 'DataResource', 'ExcelImportHistory']) { assert.match(systemOperationsVue, new RegExp(`getEntityRows\\('${entityName}'`), `system operations page should load real ${entityName} rows`) } assert.doesNotMatch( systemVue + systemOperationsVue, /props\.inventory|UiInventory|routeManifest|counts\?\.routes|counts\?\.actions|counts\?\.services|业务回归记录|界面组件规范|排版规则|覆盖台账|现代页面|动作契约|#\/system\/delivery|#\/system\/parity|#\/system\/components|#\/system\/patterns|#\/system\/inventory|待补齐事项|pendingE2ePages|customParityPages|missingRoutes|missingActions/, 'system maintenance should not foreground validation, component documentation, inventory counts, or engineering workbenches' ) for (const text of ['运行日志', '缓存维护', '定时任务', '导入导出']) { assert.match(systemVue, new RegExp(text), `system maintenance should expose administrator operation: ${text}`) } assert.doesNotMatch( systemVue + moduleCatalog, /快速进入安全、服务|WebTools|OFBiz 扩展|OFBiz 管理|接口|后端|后台|实体对象|业务对象|业务实体|待处理对象|可处理对象/, 'system maintenance and module catalog copy must use administrator-facing task, record, and connection language' ) for (const text of ['统一业务中心', '业务受理台', '待办事项', '当前班次', '当前办理口径', '今日承接', '优先处理', '异常队列', '续办记录', '待办队列', '快捷处理', '处理建议', '进入处理']) { assert.match(businessCenterVue, new RegExp(text), `global business center must use operator-facing language: ${text}`) } assert.doesNotMatch( businessCenterVue + moduleVue, /待登录授权|待授权|需授权|实体可读|可读实体|数据连接|数据状态|业务深链|处理路径/, 'business center and module workspaces must not use catalog, authorization-diagnostic, or route-path wording as primary product language' ) for (const text of ['business-center-workbench', 'business-command-grid', 'business-shift-board', 'business-priority-board', 'business-exception-board', 'business-continue-board', 'business-center-queue-card', 'business-command-lane', 'business-priority-row', '业务受理台', '办理口径', '待办总量']) { assert.match(businessCenterVue, new RegExp(text), `global business center must render a real administrator workbench: ${text}`) } for (const text of ['operatorEntryGroups', 'business-center-entry-grid', 'business-center-entry-card', '处理动作', '订单接收', '客户建档', '财务复核', '库存接收', '系统运维']) { assert.match(businessCenterVue + modernCss, new RegExp(text), `global business center must provide fixed administrator processing entries: ${text}`) } for (const text of ['getGlobalBusinessSearch', 'businessSearch', 'businessRecords', '待办事项', '当前班次']) { assert.match(businessCenterVue, new RegExp(text), `global business center must search real business records: ${text}`) } assert.match(apiTs, /export async function getGlobalBusinessSearch/, 'API client should expose a real business search loader') assert.match(apiTypes, /export type GlobalBusinessSearch/, 'API types should document the global business search contract') for (const entityName of ['OrderHeader', 'Product', 'Party', 'Invoice', 'Payment', 'InventoryItem', 'Shipment']) { assert.match(apiTs, new RegExp(`entityName: '${entityName}'`), `global business search should include ${entityName}`) } assert.doesNotMatch( businessCenterVue, /props\.inventory\.routeManifest|routeManifest|label="页面标识"|label="适配"|label="验收清单"|label="业务等价"|label="控制规则"|label="旧入口"|旧 URL|旧入口|历史地址|待补齐事项|missingRoutes|missingActions|generated-renderable|generated-adapter-renderable|generated-needs-custom-vue|risk ===|row\.risk/, 'global business center should not use route inventory or migration columns as operator data' ) for (const text of ['ErpTabbedDataPanel', 'businessDataTabs', 'data-modern="business-page-tabbed-data"', '处理台', '资料明细', '处理动作', '操作记录', '当前单据', '业务资料', '后续处理', '流转记录', 'businessDocumentRows', 'businessActionRows', 'businessFlowRows', 'businessRecordRows']) { assert.match(businessPageVue, new RegExp(text), `business pages must behave like ERP document pages: ${text}`) } assert.doesNotMatch( businessPageVue, /label="工作面"|label="数据区"|label="执行区"|label="审批流"|后端服务协同|Service Dispatcher|统一 Lookup|业务工作面|页面结构|动作编码|当前页面|页面动作|动作清单|页面定义|旧入口|旧 URL|适配/, 'business pages should not read like an engineering console' ) for (const [fileName, content] of [ ['ErpAppShell.vue', shellVue], ['DashboardView.vue', dashboardVue], ['BusinessPageView.vue', businessPageVue], ['BusinessCenterView.vue', businessCenterVue], ['SystemToolsView.vue', systemVue], ['SecurityAdminView.vue', securityAdminVue], ['ModuleWorkspaceView.vue', moduleVue] ]) { assert.doesNotMatch( content, /组件展厅|验收|迁移|业务等价|覆盖率|覆盖台账|待补齐|旧入口|旧 URL|旧页面|技术预览|页面清单|自动迁移/, `${fileName} must not expose preview, migration, parity, or component-gallery language in the administrator product` ) } for (const selector of [ 'modern-side-nav-group', 'module-board-grid', 'module-queue-card', 'module-record-list', 'admin-backend-row' ]) { assert.match(modernCss, new RegExp(selector), `modern CSS should style the administrator site pattern: ${selector}`) } console.log(JSON.stringify({ status: 'passed', checked: [ 'App.vue', 'ErpAppShell.vue', 'DashboardView.vue', 'ModuleWorkspaceView.vue', 'SystemToolsView.vue', 'SecurityAdminView.vue', 'OrderAdminView.vue', 'PartyAdminView.vue', 'ProductAdminView.vue', 'InventoryAdminView.vue', 'ManufacturingAdminView.vue', 'AccountingAdminView.vue', 'HumanResAdminView.vue', 'SalesAdminView.vue', 'ProcurementAdminView.vue', 'ScrumAdminView.vue', 'OperationsAdminView.vue', 'ContentAdminView.vue', 'MarketingAdminView.vue', 'CommerceAdminView.vue', 'PosAdminView.vue', 'MarketplaceAdminView.vue', 'AnalyticsAdminView.vue', 'SystemOperationsView.vue', 'BusinessCenterView.vue', 'ErpDomainAdminView.vue', 'converted tabbed admin pages', 'secondary domain admin pages', 'moduleCatalog.ts', 'modern.css', 'api.ts', 'api.ts types' ] }, null, 2))