Files
ERP/ofbiz-framework/plugins/modern-ui/app/scripts/verify-admin-site.mjs
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

902 lines
58 KiB
JavaScript

#!/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, /<ErpAppShell[\s\S]*v-else/, 'authenticated product UI must render inside the ERP application shell after the login gate')
assert.match(appVue, /if \(sessionData\.authenticated\) \{\s*await loadAuthenticatedData\(\)/, 'authenticated session refresh must load administrator navigation and inventory only after login')
assert.match(appVue, /loadAuthenticatedData[\s\S]*getNavigation\(\)[\s\S]*getInventory\(\)/, 'post-login product load must hydrate backend navigation and generated business page definitions')
assertRouteMapsToView('#/business', 'BusinessCenterView', 'post-login product must include a business center route')
// BusinessPageView no longer takes :page-id/:inventory props; it reads the
// pageId from the route and the inventory from inject(). The same wiring is
// preserved here: the router maps the generated business-page path to
// BusinessPageView, and BusinessPageView sources pageId from the route and
// inventory from the injected app-level state.
assert.match(routerTs, /path:\s*'\/pages\/:pageId\(\.\*\)'[\s\S]*?import\('\.\.\/views\/BusinessPageView\.vue'\)/, 'post-login product must render generated OFBiz business pages through BusinessPageView')
assert.match(businessPageVue, /route\.params\.pageId/, 'BusinessPageView must read its pageId from the route')
assert.match(businessPageVue, /inject\(InventoryKey/, 'BusinessPageView must read its inventory from injected app state')
assert.match(shellVue + dashboardVue + businessCenterVue, /#\/business/, 'post-login product chrome must expose the global business center anchor')
for (const [route, activeView, component, sourceName, sourceText, labels, entities] of postLoginCoreModuleAnchors) {
assert.match(routerTs, new RegExp(component), `${sourceName} must be mounted in the authenticated administrator app`)
assertRouteMapsToView(route, component, `${route} must resolve to ${activeView} after login`)
assert.match(
shellVue + dashboardVue + moduleCatalog,
new RegExp(escapeRegExp(route)),
`${route} must be reachable from post-login shell, dashboard, or module catalog anchors`
)
for (const label of labels) {
assert.match(sourceText, new RegExp(escapeRegExp(label)), `${sourceName} must expose product UI anchor: ${label}`)
}
for (const entityName of entities) {
assertEntitySource(sourceText, entityName, `${sourceName} must load real ${entityName} rows for the product anchor`)
}
}
}
assertPostLoginProductAnchors()
assertNoVisibleUiLeakage([
['LoginView.vue', loginVue],
['App.vue', appVue],
['ErpAppShell.vue', shellVue],
['DashboardView.vue', dashboardVue],
['ModuleWorkspaceView.vue', moduleVue],
['BusinessCenterView.vue', businessCenterVue],
['BusinessPageView.vue', businessPageVue],
['SystemToolsView.vue', systemVue],
['SecurityAdminView.vue', securityAdminVue],
['SystemOperationsView.vue', systemOperationsVue],
['ProductAdminView.vue', productAdminVue],
['OrderAdminView.vue', orderAdminVue],
['PartyAdminView.vue', partyAdminVue],
['AccountingAdminView.vue', accountingAdminVue],
['InventoryAdminView.vue', inventoryAdminVue],
['ManufacturingAdminView.vue', manufacturingAdminVue],
['HumanResAdminView.vue', humanResAdminVue],
['SalesAdminView.vue', salesAdminVue],
['ProcurementAdminView.vue', procurementAdminVue],
['ScrumAdminView.vue', scrumAdminVue],
['OperationsAdminView.vue', operationsAdminVue],
['ContentAdminView.vue', contentAdminVue],
['MarketingAdminView.vue', marketingAdminVue],
['CommerceAdminView.vue', commerceAdminVue],
['PosAdminView.vue', posAdminVue],
['MarketplaceAdminView.vue', marketplaceAdminVue],
['AnalyticsAdminView.vue', analyticsAdminVue],
['ErpDomainAdminView.vue', domainAdminVue]
])
assert.match(routerTs, /DashboardView/, 'the authenticated default route should land on the ERP administrator workbench')
assert.match(routerTs, /path:\s*'\/'[\s\S]*?import\('\.\.\/views\/DashboardView\.vue'\)/, 'only the authenticated root route should land on the ERP administrator workbench')
assert.match(routerTs, /path:\s*'\/:pathMatch\(\.\*\)\*'[\s\S]*?import\('\.\.\/views\/NotFoundView\.vue'\)/, 'unknown authenticated routes should render a clear unavailable state')
assert.match(notFoundVue, /页面不可用/, 'the unavailable route state must clearly tell the operator the page is unavailable')
assert.doesNotMatch(routerTs, /path:\s*'\/:pathMatch\(\.\*\)\*'[\s\S]*?DashboardView/, 'unknown authenticated routes must not silently fall back to the administrator workbench')
assert.doesNotMatch(routerTs, /path:\s*'\/components'|path:\s*'\/patterns'|PatternsView/, 'component and pattern pages must not be top-level authenticated routes')
assert.doesNotMatch(
routerTs,
/LegacyRedirectView|DeliveryView|ParityView|ComponentsView|PatternsView|InventoryView/,
'production administrator site should not mount legacy bridge, delivery, parity, component, pattern, or inventory engineering workbenches'
)
assert.doesNotMatch(routerTs, /path:\s*'\/legacy'|path:\s*'\/delivery'|path:\s*'\/parity'|path:\s*'\/components'|path:\s*'\/patterns'|path:\s*'\/inventory'/, 'support workbenches and legacy bridge pages should not ship as top-level administrator routes')
assert.doesNotMatch(
loginVue,
/inventory\.counts|个业务入口|业务化后台界面/,
'login door should read like an ERP administrator login, not a route or inventory catalog'
)
assert.doesNotMatch(
appVue,
/getSession\(\),\s*\n\s*getNavigation\(\),\s*\n\s*getInventory\(\)/,
'page inventory should load after OFBiz session authentication, not before the login gate'
)
for (const text of ['ERP 管理员工作台', '管理员首页', '经营数据', '今日运营', '待办队列', '最近记录', '快捷动作', '系统健康与权限', '快速检索', '订单运营', '系统维护', '当班状态', '可处理范围']) {
assert.match(dashboardVue, new RegExp(text), `administrator dashboard must prioritize business operation: ${text}`)
}
for (const text of ['kpiRows', 'priorityWorkRows', 'workbenchStatusRows', 'continueWorkRows', 'recentDocumentRows', 'businessActionGroups', 'businessSearchRows', 'pendingWorkRows']) {
assert.match(dashboardVue, new RegExp(text), `administrator dashboard should be organized around daily ERP work: ${text}`)
}
for (const text of ['getAdminSummary', 'businessSummary', 'realEntityMetrics', '经营数据', '最近单据']) {
assert.match(dashboardVue, new RegExp(text), `administrator dashboard must use real OFBiz business data signals: ${text}`)
}
assert.match(apiTs, /export async function getAdminSummary/, 'API client should expose a business summary loader for the administrator dashboard')
for (const entityName of ['OrderHeader', 'Product', 'Party', 'Invoice', 'InventoryItem']) {
assert.match(apiTs, new RegExp(`entityName: '${entityName}'`), `business summary should include ${entityName}`)
}
assert.match(apiTs, /adminSummarySources[\s\S]*getEntityRows\(source\.entityName/, 'business summary should query configured core OFBiz entities')
assert.match(apiTypes, /export type AdminSummary/, 'API types should document the administrator summary contract')
assert.doesNotMatch(
dashboardVue,
/props\.inventory|UiInventory|ParityPage|parityManifest|routeManifest|pageSource|pageList|highRiskPages|businessParityStatus|parityStatus|risk ===|page\.risk|value:\s*moduleStats\.value\.[\w?]+\.pages|const moduleStats|item\.highRiskCount\s*\|\|\s*item\.pageCount|运营入口台|核心工作区|核心处理入口|全局业务命令/,
'dashboard must not use route inventory, parity, or generated risk data as operator metrics'
)
assert.doesNotMatch(
dashboardVue,
/常用操作|数据来源|空数据源|OFBiz Entity|实体可读/,
'dashboard should read like an operator workbench instead of a data-source diagnostic page'
)
assert.doesNotMatch(
dashboardVue,
/后端授权|后端连接|待登录授权|已授权业务|条处理路径|entryCount:\s*module\.quickPages\.length|actions:\s*module\.quickPages\.length/,
'dashboard should not expose backend-authorization wording or quick-page counts as business operation language'
)
for (const text of ['primaryModules', 'secondaryModules', '核心业务', '渠道与扩展', '运营工作台', '系统治理']) {
assert.match(shellVue, new RegExp(text), `application shell must separate business navigation from governance: ${text}`)
}
for (const text of ['#/business', '业务处理中心', '统一业务中心']) {
assert.match(shellVue + appVue + dashboardVue + businessCenterVue + systemVue, new RegExp(text), `administrator product should expose a business center route instead of a page catalog route: ${text}`)
}
assertRouteMapsToView('#/business', 'BusinessCenterView', 'business center should be mounted as the administrator business route')
assert.doesNotMatch(
shellVue,
/const adminSupportTools = \[[^\]]*#\/pages[^\]]*\]/,
'global administrator support navigation should not send operators to the generated page route; use #/business for the business center'
)
assert.doesNotMatch(
shellVue,
/label: '组件规范'|label: '排版规则'|label: '全量清单'/,
'component and inventory pages must not be permanent shell navigation items'
)
assert.match(shellVue, /quickQueues[\s\S]*进入模块工作台/, 'application shell should expose operator work queues')
assert.doesNotMatch(
shellVue,
/`\$\{module\.navLabel\}入口`|>\s*入口\s*</,
'application shell should not describe core operator surfaces as generic entries'
)
assert.doesNotMatch(
shellVue,
/authorizedApplications\s*=\s*computed|后端授权/,
'application shell should turn backend navigation into authorized business domains, not an app-directory panel'
)
for (const text of ['统一运营管理平台', '运营工作台', '核心业务', 'navGroups', 'quickQueues', 'breadcrumbs']) {
assert.match(shellVue, new RegExp(text), `application shell should expose administrator work context: ${text}`)
}
assert.doesNotMatch(
shellVue,
/count:\s*module\.quickPages\.length/,
'topbar work queues should not use quick action counts as queue counts'
)
assert.match(
shellVue,
/const navGroups = computed\(\(\) => \[[\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))