20260818220849
This commit is contained in:
1 parent
f565437125
commit
f01153e9fe
3 files changed
+3375
-913
No files matched your search
+533
-913
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,655 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FMS 新系统 · 模块与菜单设计 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--text: #1f2328;
|
||||
--text-2: #57606a;
|
||||
--text-3: #8b949e;
|
||||
--border: #d0d7de;
|
||||
--bg: #ffffff;
|
||||
--bg-2: #f6f8fa;
|
||||
--accent: #0969da;
|
||||
--accent-bg: #ddf4ff;
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text); background: var(--bg); font-size: 14px; line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* 顶栏 */
|
||||
.topbar {
|
||||
height: 52px; background: var(--bg); border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; padding: 0 24px; gap: 12px;
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 20;
|
||||
}
|
||||
.logo-title { font-weight: 600; font-size: 14px; }
|
||||
.topbar .divider { width: 1px; height: 18px; background: var(--border); }
|
||||
.breadcrumb { color: var(--text-3); font-size: 13px; display: flex; align-items: center; gap: 6px; }
|
||||
.breadcrumb .cur { color: var(--text); }
|
||||
.topbar-right { margin-left: auto; color: var(--text-2); font-size: 13px; }
|
||||
|
||||
/* 布局 */
|
||||
.layout { display: flex; padding-top: 52px; height: 100vh; }
|
||||
.sidebar {
|
||||
width: 240px; flex: 0 0 240px; border-right: 1px solid var(--border);
|
||||
overflow-y: auto; padding: 14px 10px 30px;
|
||||
}
|
||||
.main { flex: 1; overflow-y: auto; padding: 28px 40px 80px; max-width: 1080px; }
|
||||
|
||||
/* 侧边栏 */
|
||||
.side-module { margin-bottom: 2px; }
|
||||
.side-module-head {
|
||||
display: flex; align-items: center; gap: 8px; padding: 7px 10px;
|
||||
border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 13.5px;
|
||||
color: var(--text-2); user-select: none;
|
||||
}
|
||||
.side-module-head:hover { background: var(--bg-2); }
|
||||
.side-module-head.active { background: var(--accent-bg); color: var(--accent); }
|
||||
.side-module-head .deps { font-size: 10.5px; color: var(--text-3); font-weight: 400; margin-left: auto; }
|
||||
.side-menu { list-style: none; display: none; margin: 1px 0 6px; }
|
||||
.side-menu.open { display: block; }
|
||||
.side-menu-item {
|
||||
display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 24px;
|
||||
border-radius: 6px; cursor: pointer; color: var(--text-2); font-size: 13px;
|
||||
}
|
||||
.side-menu-item:hover { background: var(--bg-2); color: var(--text); }
|
||||
.side-menu-item.active { background: var(--accent-bg); color: var(--accent); }
|
||||
.side-menu-item .mi { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.side-menu-item .tk { font-size: 10px; color: var(--text-3); }
|
||||
.side-menu-item .mark { width: 5px; height: 5px; border-radius: 50%; flex: 0 0 5px; }
|
||||
.side-menu-item .mark.m-data { background: var(--accent); }
|
||||
.side-menu-item .mark.m-dir, .side-menu-item .mark.m-dcat { background: transparent; border: 1px solid var(--text-3); }
|
||||
.side-menu-item .mark.m-page { background: var(--text-2); }
|
||||
.side-menu-item .mark.m-ext { background: transparent; border: 1px solid var(--text-3); border-style: dashed; }
|
||||
|
||||
/* 页面头部 */
|
||||
.page-head { margin-bottom: 24px; }
|
||||
.ph-title { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
|
||||
.ph-title .tag { font-size: 11px; color: var(--text-3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 7px; font-weight: 400; }
|
||||
.ph-desc { color: var(--text-2); font-size: 13px; margin-top: 6px; max-width: 720px; }
|
||||
.ph-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 10px; font-family: var(--mono); font-size: 12px; color: var(--text-3); }
|
||||
|
||||
/* 区块 */
|
||||
.section { margin-bottom: 32px; }
|
||||
.section-title {
|
||||
font-size: 13px; font-weight: 600; color: var(--text-2);
|
||||
padding-bottom: 6px; border-bottom: 1px solid var(--border); margin-bottom: 12px;
|
||||
}
|
||||
.section-title .hint { float: right; font-weight: 400; color: var(--text-3); font-family: var(--mono); font-size: 11.5px; }
|
||||
|
||||
/* 表格 */
|
||||
table.grid { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
table.grid th {
|
||||
text-align: left; color: var(--text-2); font-weight: 500;
|
||||
padding: 7px 10px; font-size: 12px; border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.grid td { padding: 7px 10px; border-bottom: 1px solid var(--bg-2); color: var(--text-2); vertical-align: middle; }
|
||||
table.grid tr:last-child td { border-bottom: none; }
|
||||
table.grid tr.row-click { cursor: pointer; }
|
||||
table.grid tr.row-click:hover td { background: var(--bg-2); }
|
||||
table.grid .mono { font-family: var(--mono); color: var(--text); font-size: 12.5px; }
|
||||
table.grid .dim { color: var(--text-3); }
|
||||
|
||||
/* 键值 */
|
||||
.kv-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px 24px; }
|
||||
.kv .k { font-size: 11px; color: var(--text-3); }
|
||||
.kv .v { font-size: 13px; font-family: var(--mono); }
|
||||
|
||||
/* 依赖 */
|
||||
.dep-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; font-family: var(--mono); font-size: 12.5px; color: var(--text-2); }
|
||||
.dep-line .arr { color: var(--text-3); }
|
||||
|
||||
/* 树预览 */
|
||||
.tree { font-family: var(--mono); font-size: 12.5px; }
|
||||
.tree .tr { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
||||
.tree .tr .name { color: var(--text); }
|
||||
.tree .tr .type { font-size: 10.5px; color: var(--text-3); border: 1px solid var(--border); border-radius: 3px; padding: 0 5px; }
|
||||
.tree .tr .route { color: var(--text-3); font-size: 11px; }
|
||||
|
||||
/* 类型说明 */
|
||||
.type-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 0 32px; }
|
||||
.type-item { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--bg-2); }
|
||||
.type-item .tn { width: 90px; flex: 0 0 90px; font-family: var(--mono); font-size: 12.5px; color: var(--text); }
|
||||
.type-item .tn .cn { display: block; font-family: inherit; color: var(--text-3); font-size: 11px; }
|
||||
.type-item .td { font-size: 12.5px; color: var(--text-2); }
|
||||
.type-item .td .req { color: var(--text-3); font-size: 11.5px; }
|
||||
|
||||
.empty { color: var(--text-3); font-size: 13px; padding: 18px 0; }
|
||||
.link { color: var(--accent); cursor: pointer; text-decoration: none; }
|
||||
.link:hover { text-decoration: underline; }
|
||||
.wm-tag { font-family: var(--mono); font-size: 11px; color: var(--text-2); background: var(--bg-2); border: 1px solid var(--border); padding: 1px 6px; border-radius: 3px; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="topbar">
|
||||
<span class="logo-title">FMS 新系统 · 模块与菜单设计</span>
|
||||
<span class="divider"></span>
|
||||
<div class="breadcrumb" id="breadcrumb"></div>
|
||||
<div class="topbar-right">orgId = SH01 · 售后技术 · 张工</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main" id="main"></main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ============ 数据:与《FMS新系统核心表结构设计》对应 ============ */
|
||||
|
||||
const MENU_TYPES = {
|
||||
directory: { cn: '目录', desc: '纯导航节点,不绑页面', req: '不可填实体/视图/表单/外链路由' },
|
||||
data_category: { cn: '数据目录', desc: '数据模块的分组目录', req: '不直接绑定实体,仅组织子节点' },
|
||||
data: { cn: '数据', desc: '通用列表+表单维护的数据模块', req: '必填 entity_key + view_key' },
|
||||
page: { cn: '页面', desc: '固定 Vue 组件页面', req: '必填 route,绑定 view/form' },
|
||||
external: { cn: '外链', desc: '跳转外部 URL', req: '必填 route + permission_key' },
|
||||
};
|
||||
|
||||
const MODULES = [
|
||||
{
|
||||
key: 'platform', name: '平台', origin: 'platform', sortNo: 0,
|
||||
desc: '平台基础能力:用户与组织身份、文件、审计、工作流、报表。其他模块都直接或间接依赖它。',
|
||||
deps: [],
|
||||
entities: [
|
||||
{ key: 'platform.user', table: 'core_user', policy: 'restrictWhenReferenced', desc: '系统用户与账号' },
|
||||
{ key: 'platform.org', table: 'core_org', policy: 'restrictWhenReferenced', desc: '组织与部门' },
|
||||
{ key: 'platform.file', table: 'core_file', policy: 'hardDelete', desc: '文件元数据' },
|
||||
],
|
||||
menus: [
|
||||
{ key: 'platform.system', name: '系统管理', type: 'directory', children: [
|
||||
{ key: 'platform.user.list', name: '用户管理', type: 'data', entity: 'platform.user', route: '/platform/users', permission: 'platform.user.view' },
|
||||
{ key: 'platform.role.page', name: '角色权限', type: 'page', route: '/platform/roles', permission: 'platform.role.view' },
|
||||
{ key: 'platform.audit.data', name: '审计日志', type: 'data', entity: 'platform.audit', route: '/platform/audit', permission: 'platform.audit.view' },
|
||||
]},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'master-data', name: '基础数据', origin: 'platform', sortNo: 10,
|
||||
desc: '客户、联系人、港口、币种、船名航次等共享主数据。被海运、空运、财务、合同等业务模块引用。',
|
||||
deps: ['platform'],
|
||||
entities: [
|
||||
{ key: 'master.company', table: 'master_company', policy: 'restrictWhenReferenced', desc: '客户/供应商/代理统一主数据' },
|
||||
{ key: 'master.contact', table: 'master_contact', policy: 'restrictWhenReferenced', desc: '联系人' },
|
||||
{ key: 'master.port', table: 'master_port', policy: 'restrictWhenReferenced', desc: '港口' },
|
||||
{ key: 'master.currency',table: 'master_currency',policy: 'restrictWhenReferenced', desc: '币种' },
|
||||
{ key: 'master.vessel', table: 'master_vessel', policy: 'restrictWhenReferenced', desc: '船名航次' },
|
||||
],
|
||||
menus: [
|
||||
{ key: 'master.company.list', name: '公司管理', type: 'data', entity: 'master.company', route: '/master/company', permission: 'master.company.view' },
|
||||
{ key: 'master.contact.list', name: '联系人', type: 'data', entity: 'master.contact', route: '/master/contact', permission: 'master.contact.view' },
|
||||
{ key: 'master.port.list', name: '港口', type: 'data', entity: 'master.port', route: '/master/port', permission: 'master.port.view' },
|
||||
{ key: 'master.currency.list',name: '币种', type: 'data', entity: 'master.currency', route: '/master/currency', permission: 'master.currency.view' },
|
||||
{ key: 'master.vessel.list', name: '船名航次', type: 'data', entity: 'master.vessel', route: '/master/vessel', permission: 'master.vessel.view' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'ocean', name: '海运', origin: 'platform', sortNo: 20,
|
||||
desc: '海运出口全流程:订舱、海运单、集装箱、提单、船期。典型的"一模块多实体多菜单"形态。',
|
||||
deps: ['platform', 'master-data'],
|
||||
entities: [
|
||||
{ key: 'ocean.booking', table: 'ocean_booking', policy: 'lifecycle', desc: '订舱主单' },
|
||||
{ key: 'ocean.container', table: 'ocean_container', policy: 'cascadeOwned', desc: '箱信息(订舱的子表)' },
|
||||
{ key: 'ocean.shipment', table: 'ocean_shipment', policy: 'lifecycle', desc: '海运单' },
|
||||
{ key: 'ocean.bill_of_lading', table: 'ocean_bill_of_lading', policy: 'lifecycle', desc: '提单' },
|
||||
{ key: 'ocean.sailing', table: 'ocean_sailing', policy: 'hardDelete', desc: '船期' },
|
||||
],
|
||||
menus: [
|
||||
{ key: 'ocean.booking.cat', name: '订舱管理', type: 'data_category', children: [
|
||||
{ key: 'ocean.booking.list', name: '订舱列表', type: 'data', entity: 'ocean.booking', view: 'ocean.booking.default-list', form: 'ocean.booking.default-form', route: '/ocean/bookings', permission: 'ocean.booking.view' },
|
||||
]},
|
||||
{ key: 'ocean.shipment.cat', name: '海运单', type: 'data_category', children: [
|
||||
{ key: 'ocean.shipment.list', name: '海运单列表', type: 'data', entity: 'ocean.shipment', route: '/ocean/shipments', permission: 'ocean.shipment.view' },
|
||||
]},
|
||||
{ key: 'ocean.sailing.cat', name: '船期', type: 'data_category', children: [
|
||||
{ key: 'ocean.sailing.list', name: '船期列表', type: 'data', entity: 'ocean.sailing', route: '/ocean/sailings', permission: 'ocean.sailing.view' },
|
||||
]},
|
||||
{ key: 'ocean.workbench', name: '提单工作台', type: 'page', route: '/ocean/bl-workbench', permission: 'ocean.bl.workbench', special: true },
|
||||
{ key: 'ocean.carrier.edi', name: '船公司订舱 EDI', type: 'external', route: 'https://edi.carrier.com', permission: 'ocean.edi.view' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'air', name: '空运', origin: 'platform', sortNo: 30,
|
||||
desc: '空运出口:订舱、空运单、主单/分单、航班。',
|
||||
deps: ['platform', 'master-data'],
|
||||
entities: [
|
||||
{ key: 'air.booking', table: 'air_booking', policy: 'lifecycle', desc: '空运订舱' },
|
||||
{ key: 'air.shipment',table: 'air_shipment',policy: 'lifecycle', desc: '空运单' },
|
||||
{ key: 'air.mawb', table: 'air_mawb', policy: 'lifecycle', desc: '主单' },
|
||||
{ key: 'air.hawb', table: 'air_hawb', policy: 'lifecycle', desc: '分单' },
|
||||
],
|
||||
menus: [
|
||||
{ key: 'air.booking.cat', name: '空运订舱', type: 'data_category', children: [
|
||||
{ key: 'air.booking.list', name: '订舱列表', type: 'data', entity: 'air.booking', route: '/air/bookings', permission: 'air.booking.view' },
|
||||
]},
|
||||
{ key: 'air.doc.cat', name: '单证管理', type: 'data_category', children: [
|
||||
{ key: 'air.shipment.list', name: '空运单', type: 'data', entity: 'air.shipment', route: '/air/shipments', permission: 'air.shipment.view' },
|
||||
{ key: 'air.mawb.list', name: '主单', type: 'data', entity: 'air.mawb', route: '/air/mawbs', permission: 'air.mawb.view' },
|
||||
{ key: 'air.hawb.list', name: '分单', type: 'data', entity: 'air.hawb', route: '/air/hawbs', permission: 'air.hawb.view' },
|
||||
]},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'finance', name: '财务', origin: 'platform', sortNo: 40,
|
||||
desc: '应收应付、发票、结算。依赖业务模块产生费用,属于典型的跨模块依赖。',
|
||||
deps: ['platform', 'master-data', 'ocean', 'air'],
|
||||
entities: [
|
||||
{ key: 'finance.invoice', table: 'finance_invoice', policy: 'lifecycle', desc: '发票' },
|
||||
{ key: 'finance.bill', table: 'finance_bill', policy: 'lifecycle', desc: '收付款单' },
|
||||
{ key: 'finance.settlement', table: 'finance_settlement', policy: 'lifecycle', desc: '结算单' },
|
||||
],
|
||||
menus: [
|
||||
{ key: 'finance.receivable', name: '应收管理', type: 'data_category', children: [
|
||||
{ key: 'finance.invoice.list', name: '发票', type: 'data', entity: 'finance.invoice', route: '/finance/invoices', permission: 'finance.invoice.view' },
|
||||
{ key: 'finance.bill.list', name: '收款单', type: 'data', entity: 'finance.bill', route: '/finance/bills', permission: 'finance.bill.view' },
|
||||
]},
|
||||
{ key: 'finance.settle', name: '结算管理', type: 'data_category', children: [
|
||||
{ key: 'finance.settlement.list', name: '结算单', type: 'data', entity: 'finance.settlement', route: '/finance/settlements', permission: 'finance.settlement.view' },
|
||||
]},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contract', name: '合同', origin: 'platform', sortNo: 50,
|
||||
desc: '报价单与客户合同。报价单通常由客户合同拆分而来。',
|
||||
deps: ['platform', 'master-data'],
|
||||
entities: [
|
||||
{ key: 'contract.pricelist', table: 'contract_pricelist', policy: 'lifecycle', desc: '报价单' },
|
||||
{ key: 'contract.pricelist_item', table: 'contract_pricelist_item', policy: 'cascadeOwned', desc: '报价明细(子表)' },
|
||||
{ key: 'contract.customer_contract', table: 'contract_customer_contract', policy: 'lifecycle', desc: '客户合同' },
|
||||
],
|
||||
menus: [
|
||||
{ key: 'contract.pricelist.list', name: '报价单', type: 'data', entity: 'contract.pricelist', route: '/contract/pricelists', permission: 'contract.pricelist.view' },
|
||||
{ key: 'contract.ct.list', name: '客户合同', type: 'data', entity: 'contract.customer_contract', route: '/contract/contracts', permission: 'contract.contract.view' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const ENTITY_FIELDS = {
|
||||
'ocean.booking': [
|
||||
{ key: 'booking_no', label: '订舱号', type: 'varchar(50)', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'customer_id', label: '客户', type: 'bigint', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'carrier_id', label: '船公司', type: 'bigint', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'etd', label: '预计离港日', type: 'date', write: 'direct', req: 0, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'load_port', label: '起运港', type: 'bigint', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'discharge_port', label: '目的港', type: 'bigint', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'freight_payment',label: '运费支付', type: 'varchar(32)', write: 'direct', req: 0, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'gross_weight', label: '毛重', type: 'decimal(18,3)', write: 'direct', req: 0, qry: 0, list: 1, edit: 1 },
|
||||
{ key: 'volume', label: '体积', type: 'decimal(18,3)', write: 'direct', req: 0, qry: 0, list: 1, edit: 1 },
|
||||
{ key: 'container_count',label: '箱量', type: 'int', write: 'computed', req: 0, qry: 0, list: 1, edit: 0 },
|
||||
{ key: 'status', label: '状态', type: 'varchar(32)', write: 'actionOnly', req: 0, qry: 1, list: 1, edit: 0 },
|
||||
{ key: 'remark', label: '备注', type: 'nvarchar(1000)', write: 'direct', req: 0, qry: 0, list: 0, edit: 1 },
|
||||
{ key: 'created_by', label: '创建人', type: 'nvarchar(128)', write: 'system', req: 0, qry: 0, list: 0, edit: 0 },
|
||||
{ key: 'version', label: '版本', type: 'int', write: 'system', req: 0, qry: 0, list: 0, edit: 0 },
|
||||
],
|
||||
'ocean.container': [
|
||||
{ key: 'con_no', label: '箱号', type: 'varchar(50)', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'seal_no', label: '封号', type: 'varchar(50)', write: 'direct', req: 0, qry: 0, list: 1, edit: 1 },
|
||||
{ key: 'con_size', label: '箱型', type: 'varchar(32)', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'gross_weight', label: '毛重', type: 'decimal(18,3)', write: 'direct', req: 0, qry: 0, list: 1, edit: 1 },
|
||||
{ key: 'vgm', label: 'VGM', type: 'decimal(18,3)', write: 'direct', req: 0, qry: 0, list: 1, edit: 1 },
|
||||
],
|
||||
'finance.invoice': [
|
||||
{ key: 'invoice_no', label: '发票号', type: 'varchar(50)', write: 'direct', req: 1, qry: 1, list: 1, edit: 0 },
|
||||
{ key: 'direction', label: '方向', type: 'varchar(16)', write: 'createOnly', req: 1, qry: 1, list: 1, edit: 0 },
|
||||
{ key: 'customer_id', label: '往来单位', type: 'bigint', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'amount', label: '含税金额', type: 'decimal(18,2)', write: 'computed', req: 0, qry: 0, list: 1, edit: 0 },
|
||||
{ key: 'currency', label: '币种', type: 'varchar(8)', write: 'direct', req: 1, qry: 1, list: 1, edit: 1 },
|
||||
{ key: 'status', label: '状态', type: 'varchar(32)', write: 'actionOnly', req: 0, qry: 1, list: 1, edit: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
const ENTITY_RELATIONS = {
|
||||
'ocean.booking': [
|
||||
{ key: 'ocean.booking.containers', type: 'one-to-many', target: 'ocean.container', ownership: 'composition', delete: 'cascade', save: 'with-parent' },
|
||||
{ key: 'ocean.booking.customer', type: 'many-to-one', target: 'master.company', ownership: 'reference', delete: 'restrict', save: 'separate' },
|
||||
{ key: 'ocean.booking.carrier', type: 'many-to-one', target: 'master.company', ownership: 'reference', delete: 'restrict', save: 'separate' },
|
||||
],
|
||||
};
|
||||
|
||||
const ENTITY_VIEWS = {
|
||||
'ocean.booking': [
|
||||
{ key: 'ocean.booking.default-list', queryable: 1, exportable: 1, pageSize: 50 },
|
||||
{ key: 'ocean.booking.query-simple', queryable: 1, exportable: 1, pageSize: 20 },
|
||||
],
|
||||
};
|
||||
const ENTITY_FORMS = {
|
||||
'ocean.booking': [
|
||||
{ key: 'ocean.booking.default-form', type: 'default', relations: ['ocean.booking.containers'] },
|
||||
{ key: 'ocean.booking.readonly-form', type: 'readonly', relations: [] },
|
||||
],
|
||||
};
|
||||
|
||||
/* ============ 状态与工具 ============ */
|
||||
const state = { selectedModule: null, selectedMenu: null, selectedEntity: null, openModules: new Set(['ocean']) };
|
||||
|
||||
function findMenuByKey(k) {
|
||||
for (const m of MODULES) for (const menu of m.menus) {
|
||||
if (menu.key === k) return menu;
|
||||
if (menu.children) { const c = menu.children.find(x => x.key === k); if (c) return c; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function menuOwner(k) {
|
||||
return MODULES.find(m => m.menus.some(mm => mm.key === k || (mm.children && mm.children.some(c => c.key === k))));
|
||||
}
|
||||
function entityTable(ek) {
|
||||
for (const m of MODULES) { const e = m.entities.find(x => x.key === ek); if (e) return e.table; }
|
||||
return '—';
|
||||
}
|
||||
function entityOwner(ek) {
|
||||
for (const m of MODULES) { const e = m.entities.find(x => x.key === ek); if (e) return { mod: m, ent: e }; }
|
||||
return null;
|
||||
}
|
||||
function countMenus(m) {
|
||||
let n = 0;
|
||||
for (const menu of m.menus) n += menu.children ? menu.children.length : 1;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ============ 渲染 ============ */
|
||||
function render() {
|
||||
const bc = ['配置中心'];
|
||||
if (state.selectedModule) {
|
||||
const m = MODULES.find(x => x.key === state.selectedModule);
|
||||
if (m) bc.push(m.name + ' 模块');
|
||||
}
|
||||
if (state.selectedMenu) { const menu = findMenuByKey(state.selectedMenu); if (menu) bc.push(menu.name); }
|
||||
if (state.selectedEntity) bc.push(state.selectedEntity);
|
||||
document.getElementById('breadcrumb').innerHTML = bc.map((s, i) => i === bc.length - 1
|
||||
? `<span class="cur">${s}</span>` : `<span>${s}</span><span> / </span>`).join('');
|
||||
|
||||
renderSidebar();
|
||||
const main = document.getElementById('main');
|
||||
if (state.selectedEntity) main.innerHTML = renderEntity();
|
||||
else if (state.selectedMenu) main.innerHTML = renderMenu();
|
||||
else if (state.selectedModule) main.innerHTML = renderModule();
|
||||
else main.innerHTML = renderOverview();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function renderSidebar() {
|
||||
const el = document.getElementById('sidebar');
|
||||
let html = '';
|
||||
for (const m of MODULES) {
|
||||
const open = state.openModules.has(m.key);
|
||||
const active = state.selectedModule === m.key;
|
||||
html += `<div class="side-module">
|
||||
<div class="side-module-head ${open ? '' : ''} ${active ? 'active' : ''}" data-mod="${m.key}">
|
||||
<span>${m.name}</span>
|
||||
${m.deps.length ? `<span class="deps">依赖 ${m.deps.length}</span>` : ''}
|
||||
</div>
|
||||
<ul class="side-menu ${open ? 'open' : ''}">`;
|
||||
for (const menu of m.menus) html += renderMenuRow(menu, 0);
|
||||
html += `</ul></div>`;
|
||||
}
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderMenuRow(menu, depth) {
|
||||
const t = MENU_TYPES[menu.type];
|
||||
const active = state.selectedMenu === menu.key;
|
||||
const pad = depth * 14;
|
||||
if (menu.children) {
|
||||
return `<li class="side-menu-item ${active ? 'active' : ''}" data-menu="${menu.key}" style="padding-left:${10 + pad}px">
|
||||
<span class="mark m-${menu.type}"></span><span class="mi">${menu.name}</span><span class="tk">${t.cn}</span>
|
||||
</li>` + menu.children.map(c => renderMenuRow(c, depth + 1)).join('');
|
||||
}
|
||||
return `<li class="side-menu-item ${active ? 'active' : ''}" data-menu="${menu.key}" style="padding-left:${10 + pad}px">
|
||||
<span class="mark m-${menu.type}"></span><span class="mi">${menu.name}</span><span class="tk">${t.cn}</span>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
/* --- 总览 --- */
|
||||
function renderOverview() {
|
||||
return `
|
||||
<div class="page-head">
|
||||
<div class="ph-title">模块与菜单设计总览</div>
|
||||
<div class="ph-desc">依据《FMS新系统核心表结构设计》实现。左侧为 s_module_menu 导航树(五种菜单类型),点击查看模块、菜单与实体的元数据设计。</div>
|
||||
<div class="ph-meta">s_module · s_module_dependency · s_module_entity · s_module_field · s_module_menu</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">菜单类型(s_module_menu.b_menu_type)</div>
|
||||
<div class="type-list">
|
||||
${Object.entries(MENU_TYPES).map(([k, v]) => `
|
||||
<div class="type-item">
|
||||
<div class="tn">${k}<span class="cn">${v.cn}</span></div>
|
||||
<div class="td">${v.desc}<div class="req">${v.req}</div></div>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">模块清单(s_module)<span class="hint">点击模块查看详情</span></div>
|
||||
<table class="grid">
|
||||
<thead><tr><th>模块</th><th>模块 key</th><th>依赖</th><th>实体</th><th>菜单</th><th>来源</th></tr></thead>
|
||||
<tbody>
|
||||
${MODULES.map(m => `
|
||||
<tr class="row-click" data-mod="${m.key}">
|
||||
<td>${m.name}</td>
|
||||
<td class="mono">${m.key}</td>
|
||||
<td class="mono dim">${m.deps.length ? m.deps.join(', ') : '—'}</td>
|
||||
<td>${m.entities.length}</td>
|
||||
<td>${countMenus(m)}</td>
|
||||
<td class="dim">${m.origin}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* --- 模块详情 --- */
|
||||
function renderModule() {
|
||||
const m = MODULES.find(x => x.key === state.selectedModule);
|
||||
if (!m) return renderOverview();
|
||||
const reverseDeps = MODULES.filter(x => x.deps.includes(m.key));
|
||||
|
||||
return `
|
||||
<div class="page-head">
|
||||
<div class="ph-title">${m.name} 模块 <span class="tag">${m.origin}</span></div>
|
||||
<div class="ph-desc">${m.desc}</div>
|
||||
<div class="ph-meta">key: ${m.key} · name_key: module.${m.key} · sort_no: ${m.sortNo}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">模块依赖(s_module_dependency)<span class="hint">有向无环图</span></div>
|
||||
<div class="dep-line">
|
||||
${m.deps.length
|
||||
? m.deps.map(d => `<span>${d}</span>`).join('<span class="arr">←</span>')
|
||||
: '<span class="dim">无依赖(平台根基)</span>'}
|
||||
</div>
|
||||
${reverseDeps.length ? `<div style="margin-top:8px;font-size:12.5px;color:var(--text-3)">被依赖:${reverseDeps.map(x => x.name).join('、')}</div>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">包含实体(s_module_entity)<span class="hint">点击实体查看字段元数据</span></div>
|
||||
<table class="grid">
|
||||
<thead><tr><th>实体 key</th><th>物理表</th><th>删除策略</th><th>描述</th><th>字段数</th></tr></thead>
|
||||
<tbody>
|
||||
${m.entities.map(e => `
|
||||
<tr class="row-click" data-entity="${e.key}">
|
||||
<td class="mono">${e.key}</td>
|
||||
<td class="mono dim">${e.table}</td>
|
||||
<td class="dim">${e.policy}</td>
|
||||
<td>${e.desc}</td>
|
||||
<td>${ENTITY_FIELDS[e.key] ? ENTITY_FIELDS[e.key].length : 0}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">菜单结构(s_module_menu)<span class="hint">一条记录 = 导航节点 + 页面入口</span></div>
|
||||
${renderTree(m.menus)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTree(items, depth = 0) {
|
||||
let html = '<div class="tree">';
|
||||
const walk = (list, d) => {
|
||||
for (const item of list) {
|
||||
const t = MENU_TYPES[item.type];
|
||||
html += `<div class="tr" data-menu="${item.key}" style="cursor:pointer;padding-left:${d * 22}px">
|
||||
<span class="type">${t.cn}</span><span class="name">${item.name}</span>
|
||||
${item.route ? `<span class="route">${item.route}</span>` : ''}
|
||||
</div>`;
|
||||
if (item.children) walk(item.children, d + 1);
|
||||
}
|
||||
};
|
||||
walk(items, depth);
|
||||
return html + '</div>';
|
||||
}
|
||||
|
||||
/* --- 菜单详情 --- */
|
||||
function renderMenu() {
|
||||
const menu = findMenuByKey(state.selectedMenu);
|
||||
if (!menu) { state.selectedMenu = null; return renderMainFallback(); }
|
||||
const t = MENU_TYPES[menu.type];
|
||||
const owner = menuOwner(menu.key);
|
||||
const entity = menu.entity ? {
|
||||
key: menu.entity, table: entityTable(menu.entity),
|
||||
fields: ENTITY_FIELDS[menu.entity] ? ENTITY_FIELDS[menu.entity].length : 0,
|
||||
views: ENTITY_VIEWS[menu.entity] ? ENTITY_VIEWS[menu.entity].length : 0,
|
||||
forms: ENTITY_FORMS[menu.entity] ? ENTITY_FORMS[menu.entity].length : 0,
|
||||
} : null;
|
||||
|
||||
return `
|
||||
<div class="page-head">
|
||||
<div class="ph-title">${menu.name} <span class="tag">${t.cn} · ${menu.type}</span></div>
|
||||
<div class="ph-desc">${t.desc}</div>
|
||||
<div class="ph-meta">menu_key: ${menu.key} · module: ${owner ? owner.key : '—'}${menu.permission ? ' · permission: ' + menu.permission : ''}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">节点约束(元数据校验器)<span class="hint">b_menu_type</span></div>
|
||||
<div class="kv-grid">
|
||||
<div class="kv"><div class="k">类型</div><div class="v">${t.cn} (${menu.type})</div></div>
|
||||
<div class="kv"><div class="k">路由 route</div><div class="v">${menu.route || '—'}</div></div>
|
||||
<div class="kv"><div class="k">权限 permission_key</div><div class="v">${menu.permission || '—'}</div></div>
|
||||
<div class="kv"><div class="k">校验要求</div><div class="v">${t.req}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${entity ? `
|
||||
<div class="section">
|
||||
<div class="section-title">绑定的实体 / 视图 / 表单 <span class="hint">data 类型</span></div>
|
||||
<div class="kv-grid">
|
||||
<div class="kv"><div class="k">实体 entity_key</div><div class="v"><a class="link" data-entity="${entity.key}">${entity.key} ›</a></div></div>
|
||||
<div class="kv"><div class="k">物理表</div><div class="v">${entity.table}</div></div>
|
||||
<div class="kv"><div class="k">字段 / 视图 / 表单</div><div class="v">${entity.fields} / ${entity.views} / ${entity.forms}</div></div>
|
||||
<div class="kv"><div class="k">视图 key</div><div class="v">${menu.view || '—'}</div></div>
|
||||
<div class="kv"><div class="k">表单 key</div><div class="v">${menu.form || '—'}</div></div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
${menu.children ? `
|
||||
<div class="section">
|
||||
<div class="section-title">子节点</div>
|
||||
${renderTree([menu])}
|
||||
</div>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderMainFallback() { state.selectedModule = null; state.selectedMenu = null; state.selectedEntity = null; return renderOverview(); }
|
||||
|
||||
/* --- 实体详情 --- */
|
||||
function renderEntity() {
|
||||
const { mod, ent } = entityOwner(state.selectedEntity) || {};
|
||||
if (!ent) { state.selectedEntity = null; return render(); }
|
||||
const ek = ent.key;
|
||||
const fields = ENTITY_FIELDS[ek] || [];
|
||||
const rels = ENTITY_RELATIONS[ek] || [];
|
||||
const views = ENTITY_VIEWS[ek] || [];
|
||||
const forms = ENTITY_FORMS[ek] || [];
|
||||
|
||||
return `
|
||||
<div class="page-head">
|
||||
<div class="ph-title">${ent.desc} <span class="tag">${ent.policy}</span></div>
|
||||
<div class="ph-desc">实体是数据和字段的所有者,字段挂实体(s_module_field.b_entity_key)。返回<a class="link" data-back="mod">${mod.name} 模块</a>。</div>
|
||||
<div class="ph-meta">entity_key: ${ek} · 物理表: ${ent.table} · module: ${mod.key}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">字段元数据(s_module_field)<span class="hint">写入模式 / 必填 / 查询 / 列表 / 编辑</span></div>
|
||||
<table class="grid">
|
||||
<thead><tr><th>字段 key</th><th>标签</th><th>数据类型</th><th>写入模式</th><th>必填</th><th>查询</th><th>列表</th><th>编辑</th></tr></thead>
|
||||
<tbody>
|
||||
${fields.map(f => `<tr>
|
||||
<td class="mono">${f.key}</td>
|
||||
<td>${f.label}</td>
|
||||
<td class="mono dim">${f.type}</td>
|
||||
<td><span class="wm-tag">${f.write}</span></td>
|
||||
<td>${f.req ? '必填' : '<span class="dim">—</span>'}</td>
|
||||
<td>${f.qry ? '✓' : ''}</td>
|
||||
<td>${f.list ? '✓' : ''}</td>
|
||||
<td>${f.edit ? '✓' : ''}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:8px;font-size:12px;color:var(--text-3)">
|
||||
direct 普通保存 · createOnly 仅创建 · readOnly 只读 · computed 计算 · actionOnly 仅动作 · system 系统维护
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">关系(s_module_relation)<span class="hint">主子表 / 引用</span></div>
|
||||
${rels.length ? `<table class="grid">
|
||||
<thead><tr><th>关系 key</th><th>类型</th><th>目标实体</th><th>所有权</th><th>删除</th><th>保存</th></tr></thead>
|
||||
<tbody>${rels.map(r => `<tr>
|
||||
<td class="mono">${r.key}</td>
|
||||
<td class="mono">${r.type}</td>
|
||||
<td class="mono">${r.target}</td>
|
||||
<td>${r.ownership}</td>
|
||||
<td>${r.delete}</td>
|
||||
<td>${r.save}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>` : '<div class="empty">无</div>'}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">视图与表单 <span class="hint">一个实体可有多视图/多表单</span></div>
|
||||
<div class="kv-grid">
|
||||
<div class="kv"><div class="k">视图 s_module_view</div><div class="v">${views.map(v => v.key).join('、') || '—'}</div></div>
|
||||
<div class="kv"><div class="k">表单 s_module_form</div><div class="v">${forms.map(v => v.key).join('、') || '—'}</div></div>
|
||||
</div>
|
||||
${forms.some(f => f.relations.length) ? `<div style="margin-top:10px;font-size:12.5px;color:var(--text-3)">主子表区域(s_module_form_relation):${forms.find(f => f.relations.length).relations.join('、')} 以 table 模式内嵌于表单</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ============ 事件 ============ */
|
||||
document.addEventListener('click', (e) => {
|
||||
const modEl = e.target.closest('[data-mod]');
|
||||
if (modEl) {
|
||||
const k = modEl.getAttribute('data-mod');
|
||||
state.selectedModule = k; state.selectedMenu = null; state.selectedEntity = null;
|
||||
state.openModules.add(k); render(); return;
|
||||
}
|
||||
const menuEl = e.target.closest('[data-menu]');
|
||||
if (menuEl) {
|
||||
const k = menuEl.getAttribute('data-menu');
|
||||
state.selectedMenu = k; state.selectedEntity = null;
|
||||
const owner = menuOwner(k);
|
||||
if (owner) { state.selectedModule = owner.key; state.openModules.add(owner.key); }
|
||||
render(); return;
|
||||
}
|
||||
const entEl = e.target.closest('[data-entity]');
|
||||
if (entEl) { state.selectedEntity = entEl.getAttribute('data-entity'); state.selectedMenu = null; render(); return; }
|
||||
const backEl = e.target.closest('[data-back]');
|
||||
if (backEl) { state.selectedEntity = null; render(); return; }
|
||||
});
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user