Files
workspace/code/fms/erp-layout-patterns-demo.html
T
2026-08-09 22:34:28 +08:00

1092 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>国外 ERP 布局模式 Demo — 分类树 + 内容区</title>
<style>
/* ============================================================
DESIGN TOKENS — 模仿 SAP Fiori / Salesforce Lightning 的视觉体系
============================================================ */
:root {
--canvas: #f2f4f7;
--surface: #ffffff;
--surface-hover: #f7f8fa;
--surface-active: #ecf1f7;
--border: #e2e6ed;
--border-strong: #cfd5de;
--text: #1d232b;
--text-secondary: #5b6577;
--text-muted: #9099a9;
--text-placeholder:#b0b8c4;
--accent: #1b6ef3;
--accent-hover: #1559d6;
--accent-soft: #e8f1fd;
--accent-text: #ffffff;
--success: #12854a;
--success-soft: #e7f5ec;
--warning: #b8680a;
--warning-soft: #fef4e4;
--danger: #d9353b;
--danger-soft: #fde9ea;
--info: #1b6ef3;
--info-soft: #e8f1fd;
--sidebar-width: 272px;
--header-height: 56px;
--title-height: 96px;
--tab-height: 44px;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--shadow-sm: 0 1px 2px rgba(0,0,0,.04);
--shadow-md: 0 2px 8px rgba(0,0,0,.06);
--shadow-lg: 0 4px 16px rgba(0,0,0,.08);
--transition: 0.15s ease;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font: 13px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", "Noto Sans SC", "Microsoft YaHei", sans-serif;
color: var(--text);
background: var(--canvas);
-webkit-font-smoothing: antialiased;
}
/* ---- Top Bar (模拟 ERP 全局导航) ---- */
.global-bar {
height: var(--header-height);
background: var(--surface);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 20px;
gap: 16px;
position: relative;
z-index: 10;
}
.global-bar .logo {
font-size: 16px;
font-weight: 700;
color: var(--accent);
letter-spacing: -.02em;
}
.global-bar .divider { width: 1px; height: 24px; background: var(--border); }
.global-bar .app-name { font-size: 14px; font-weight: 600; color: var(--text-secondary); }
.global-bar .global-actions { margin-left: auto; display: flex; gap: 6px; }
.global-bar .global-actions button {
height: 34px; padding: 0 12px; border: 0; border-radius: var(--radius-sm);
background: transparent; color: var(--text-secondary); font-size: 12px; cursor: pointer;
}
.global-bar .global-actions button:hover { background: var(--surface-hover); color: var(--text); }
/* ---- Switch between patterns ---- */
.pattern-tabs {
display: flex; gap: 0; padding: 0 20px;
background: var(--surface); border-bottom: 1px solid var(--border);
height: 36px; align-items: center;
}
.pattern-tabs button {
height: 36px; padding: 0 16px; border: 0; border-bottom: 2px solid transparent;
background: transparent; color: var(--text-secondary); font-size: 12px; font-weight: 500; cursor: pointer;
transition: var(--transition);
}
.pattern-tabs button:hover { color: var(--text); }
.pattern-tabs button.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
/* ---- Shell — the main layout container ---- */
.shell {
display: flex; height: calc(100% - var(--header-height) - 36px);
}
.shell.full { height: calc(100% - var(--header-height)); }
/* ============================================================
SIDEBAR — 分类树左侧栏
============================================================ */
.sidebar {
width: var(--sidebar-width); min-width: var(--sidebar-width);
background: var(--surface); border-right: 1px solid var(--border);
display: flex; flex-direction: column; overflow: hidden;
transition: width .2s ease;
}
.sidebar.collapsed { width: 52px; min-width: 52px; }
.sidebar-head {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 16px 10px; gap: 8px;
}
.sidebar-head h3 {
font-size: 13px; font-weight: 600; color: var(--text);
white-space: nowrap; overflow: hidden;
}
.sidebar.collapsed .sidebar-head h3 { display: none; }
.sidebar-head .collapse-btn {
width: 28px; height: 28px; border: 0; border-radius: var(--radius-sm);
background: transparent; color: var(--text-muted); cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 16px; transition: var(--transition);
}
.sidebar-head .collapse-btn:hover { background: var(--surface-hover); color: var(--text); }
.sidebar-search {
padding: 0 12px 8px;
position: relative;
}
.sidebar.collapsed .sidebar-search { display: none; }
.sidebar-search input {
width: 100%; height: 32px; padding: 0 10px 0 30px;
border: 1px solid var(--border); border-radius: var(--radius-md);
background: var(--surface-hover); color: var(--text); outline: none;
font-size: 12px; transition: var(--transition);
}
.sidebar-search input:focus { border-color: var(--accent); background: var(--surface); box-shadow: 0 0 0 3px var(--accent-soft); }
.sidebar-search::before {
content: "⌕"; position: absolute; left: 22px; top: 5px;
font-size: 16px; color: var(--text-muted);
}
.tree-wrap { flex: 1; overflow-y: auto; padding: 0 8px 10px; }
.sidebar.collapsed .tree-wrap { padding: 0 6px; }
/* Tree nodes */
.tree-group-label {
padding: 12px 12px 4px; font-size: 10px; font-weight: 700;
color: var(--text-muted); letter-spacing: .06em;
text-transform: uppercase; white-space: nowrap; overflow: hidden;
}
.sidebar.collapsed .tree-group-label { display: none; }
.tree-node {
display: flex; align-items: center; gap: 8px;
width: 100%; height: 36px; padding: 0 10px;
border: 0; border-radius: var(--radius-md);
background: transparent; color: var(--text-secondary);
font-size: 13px; cursor: pointer; text-align: left;
transition: var(--transition); white-space: nowrap; overflow: hidden;
}
.sidebar.collapsed .tree-node {
justify-content: center; padding: 0;
width: 36px; height: 36px; margin: 2px auto;
}
.tree-node:hover { background: var(--surface-hover); color: var(--text); }
.tree-node.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.tree-node .node-icon { font-size: 14px; width: 18px; text-align: center; flex-shrink: 0; }
.tree-node .node-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.sidebar.collapsed .tree-node .node-label { display: none; }
.tree-node .node-badge {
font-size: 10px; padding: 1px 6px; border-radius: 10px;
background: var(--border); color: var(--text-muted); font-weight: 600;
}
.sidebar.collapsed .tree-node .node-badge { display: none; }
.tree-node.child { padding-left: 32px; }
.sidebar.collapsed .tree-node.child { padding-left: 0; }
.tree-node .chevron { width: 12px; font-size: 10px; color: var(--text-muted); flex-shrink: 0; text-align: center; }
.sidebar.collapsed .tree-node .chevron { display: none; }
.sidebar-footer {
padding: 12px 16px; border-top: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
.sidebar.collapsed .sidebar-footer { justify-content: center; padding: 12px 6px; }
.sidebar-footer .add-btn {
flex: 1; height: 32px; border: 1px dashed var(--border-strong);
border-radius: var(--radius-md); background: transparent;
color: var(--text-muted); font-size: 12px; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 4px;
transition: var(--transition);
}
.sidebar-footer .add-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.sidebar.collapsed .sidebar-footer .add-btn span { display: none; }
.sidebar.collapsed .sidebar-footer .add-btn { flex: 0; width: 32px; }
/* ============================================================
MAIN CONTENT AREA
============================================================ */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
/* ---- Object Header ---- */
.object-header {
padding: 16px 28px 0;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.breadcrumb {
display: flex; align-items: center; gap: 6px;
font-size: 12px; color: var(--text-muted); margin-bottom: 4px;
}
.breadcrumb span { color: var(--text-muted); }
.breadcrumb .current { color: var(--text-secondary); font-weight: 500; }
.title-row {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 16px; padding-bottom: 10px;
}
.title-left { min-width: 0; }
.title-left h1 { font-size: 22px; font-weight: 700; letter-spacing: -.01em; line-height: 1.3; }
.title-meta {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
margin-top: 4px; font-size: 12px; color: var(--text-muted);
}
.title-meta .dot { color: var(--border-strong); }
.title-actions {
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
}
/* Badges */
.badge {
display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
border-radius: 11px; font-size: 11px; font-weight: 600; white-space: nowrap;
}
.badge.success { background: var(--success-soft); color: var(--success); }
.badge.warning { background: var(--warning-soft); color: var(--warning); }
.badge.info { background: var(--info-soft); color: var(--info); }
.badge.danger { background: var(--danger-soft); color: var(--danger); }
.badge.neutral { background: var(--surface-hover); color: var(--text-secondary); }
/* Buttons */
.btn {
display: inline-flex; align-items: center; gap: 6px;
height: 34px; padding: 0 14px; border: 1px solid var(--border-strong);
border-radius: var(--radius-md); background: var(--surface);
color: var(--text); font-size: 13px; font-weight: 500; cursor: pointer;
white-space: nowrap; transition: var(--transition);
}
.btn:hover { background: var(--surface-hover); border-color: var(--text-muted); }
.btn.primary { border-color: var(--accent); background: var(--accent); color: var(--accent-text); }
.btn.primary:hover { background: var(--accent-hover); }
.btn.ghost { border-color: transparent; background: transparent; color: var(--text-secondary); }
.btn.ghost:hover { background: var(--surface-hover); color: var(--text); }
.btn.danger { border-color: var(--danger); color: var(--danger); }
.btn.danger:hover { background: var(--danger-soft); }
.btn .btn-icon { font-size: 15px; line-height: 1; }
.btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
/* ---- Tab Navigation ---- */
.tab-nav {
display: flex; gap: 0;
padding: 0 10px;
}
.tab-nav button {
position: relative; height: var(--tab-height); padding: 0 16px;
border: 0; background: transparent; color: var(--text-secondary);
font-size: 13px; font-weight: 500; cursor: pointer; transition: var(--transition);
}
.tab-nav button:hover { color: var(--text); }
.tab-nav button.active { color: var(--accent); font-weight: 600; }
.tab-nav button.active::after {
content: ""; position: absolute; bottom: 0; left: 16px; right: 16px;
height: 3px; background: var(--accent); border-radius: 3px 3px 0 0;
}
/* ---- Content Area ---- */
.content {
flex: 1; overflow-y: auto; background: var(--canvas);
}
.content-inner {
max-width: 960px; padding: 24px 28px 80px;
}
.content-inner.wide { max-width: 1140px; }
/* ---- Tab Panels ---- */
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ---- Card Section ---- */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-lg); margin-bottom: 20px;
overflow: hidden;
}
.card-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; gap: 16px;
border-bottom: 1px solid var(--border);
}
.card-header h2 {
font-size: 14px; font-weight: 650; color: var(--text);
}
.card-header .card-hint { font-size: 12px; color: var(--text-muted); }
.card-body { padding: 20px; }
/* No-border card variant — just sections */
.section { margin-bottom: 28px; }
.section-header {
display: flex; align-items: baseline; justify-content: space-between;
gap: 16px; margin-bottom: 12px;
}
.section-header h3 {
font-size: 14px; font-weight: 650; color: var(--text);
}
/* ---- Form Grid ---- */
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px 28px; }
.form-grid.col-3 { grid-template-columns: repeat(3, 1fr); }
.form-grid.col-1 { grid-template-columns: 1fr; }
.form-grid.span-2 { /* handled by field.span-2 */ }
.field { display: flex; flex-direction: column; gap: 4px; }
.field.span-2 { grid-column: 1 / -1; }
.field.span-3 { grid-column: 1 / -1; }
.field label {
font-size: 12px; font-weight: 550; color: var(--text-secondary);
display: flex; align-items: center; gap: 4px;
}
.field label .req { color: var(--danger); font-weight: 400; }
.field .hint { font-size: 11px; color: var(--text-muted); font-weight: 400; }
.form-control {
width: 100%; height: 36px; padding: 0 12px;
border: 1px solid var(--border); border-radius: var(--radius-md);
background: var(--surface); color: var(--text); font-size: 13px;
outline: none; transition: var(--transition);
}
.form-control:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.form-control.readonly { background: var(--surface-hover); color: var(--text-secondary); }
textarea.form-control { height: 80px; padding: 8px 12px; resize: vertical; }
select.form-control { appearance: none; background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%239099a9' fill='none' stroke-width='1.5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px; }
/* Inline field groups */
.field-group {
display: flex; gap: 12px; align-items: flex-start;
}
.field-group .field { flex: 1; }
/* Switch */
.switch-row { display: flex; align-items: center; gap: 10px; height: 36px; }
.switch-row .switch-label { font-size: 13px; color: var(--text); }
.toggle {
position: relative; width: 36px; height: 20px; flex-shrink: 0;
}
.toggle input { width: 0; height: 0; opacity: 0; position: absolute; }
.toggle .track {
position: absolute; inset: 0; border-radius: 10px;
background: var(--border-strong); transition: var(--transition); cursor: pointer;
}
.toggle .track::after {
content: ""; position: absolute; top: 2px; left: 2px;
width: 16px; height: 16px; border-radius: 50%;
background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.15); transition: var(--transition);
}
.toggle input:checked + .track { background: var(--accent); }
.toggle input:checked + .track::after { transform: translateX(16px); }
/* ---- Table ---- */
.table-wrap {
border: 1px solid var(--border); border-radius: var(--radius-md);
overflow: hidden;
}
table { width: 100%; border-collapse: collapse; }
thead th {
height: 38px; padding: 0 14px; text-align: left;
font-size: 11px; font-weight: 650; color: var(--text-secondary);
background: var(--surface-hover); border-bottom: 1px solid var(--border);
white-space: nowrap;
}
tbody td {
height: 42px; padding: 0 14px; font-size: 13px;
border-bottom: 1px solid var(--border); color: var(--text);
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: var(--surface-hover); }
td.muted { color: var(--text-muted); }
.row-actions { display: flex; justify-content: flex-end; gap: 2px; }
.row-btn {
height: 28px; padding: 0 8px; border: 0; border-radius: var(--radius-sm);
background: transparent; color: var(--text-muted); font-size: 12px; cursor: pointer;
}
.row-btn:hover { background: var(--surface-hover); color: var(--text); }
/* ---- Sticky Bottom Action Bar (关键模式!) ---- */
.sticky-bar {
position: sticky; bottom: 0; left: 0; right: 0;
background: var(--surface); border-top: 1px solid var(--border);
padding: 12px 28px; display: flex; align-items: center; justify-content: flex-end;
gap: 10px; z-index: 5;
box-shadow: 0 -2px 8px rgba(0,0,0,.04);
margin-top: auto;
}
/* ---- Callout / Info Box ---- */
.callout {
display: flex; gap: 10px; padding: 14px 16px;
border-radius: var(--radius-md); font-size: 12px; line-height: 1.5;
margin-bottom: 20px;
}
.callout.info { background: var(--info-soft); border: 1px solid #c6dbfb; color: #2e5a9e; }
.callout.warning { background: var(--warning-soft); border: 1px solid #fad6a0; color: #8a5610; }
.callout.success { background: var(--success-soft); border: 1px solid #b6ddc4; color: #1a5c32; }
.callout .callout-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
/* ---- Drag handle mock ---- */
.drag-handle { color: var(--text-muted); cursor: grab; font-size: 14px; }
/* ---- Empty State ---- */
.empty {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 48px 20px; color: var(--text-muted); text-align: center; gap: 12px;
}
.empty-icon { font-size: 36px; opacity: .5; }
.empty-text { font-size: 14px; }
.empty-action { font-size: 12px; color: var(--accent); cursor: pointer; }
/* ---- Chip / Tag ---- */
.chip {
display: inline-flex; align-items: center; height: 24px; padding: 0 10px; gap: 4px;
border-radius: 12px; background: var(--surface-hover); border: 1px solid var(--border);
font-size: 12px; color: var(--text-secondary);
}
/* ---- Sub-tabs within a card (secondary navigation) ---- */
.sub-tabs {
display: flex; gap: 2px; padding: 3px;
background: var(--surface-hover); border-radius: var(--radius-md);
margin-bottom: 16px; width: fit-content;
}
.sub-tabs button {
height: 30px; padding: 0 14px; border: 0; border-radius: var(--radius-sm);
background: transparent; color: var(--text-secondary); font-size: 12px; font-weight: 500;
cursor: pointer; transition: var(--transition);
}
.sub-tabs button.active { background: var(--surface); color: var(--text); font-weight: 600; box-shadow: var(--shadow-sm); }
/* ---- Inline Edit (双击编辑模式) ---- */
.inline-edit {
border: 0; background: transparent; font: inherit; color: inherit; padding: 0;
outline: none; width: 100%;
}
.inline-edit:focus { border-bottom: 1px dashed var(--accent); }
/* ---- Annotation / 设计标注 ---- */
.annotation-toggle {
position: fixed; bottom: 20px; right: 20px; z-index: 100;
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--border-strong);
background: var(--surface); color: var(--text-muted); font-size: 18px; cursor: pointer;
box-shadow: var(--shadow-md); display: flex; align-items: center; justify-content: center;
transition: var(--transition);
}
.annotation-toggle:hover { color: var(--accent); border-color: var(--accent); }
.annotation-toggle.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.annotation {
display: none; position: absolute; z-index: 50;
background: #1e293b; color: #e2e8f0; font-size: 11px; line-height: 1.5;
padding: 8px 12px; border-radius: var(--radius-md); max-width: 200px;
pointer-events: none; box-shadow: 0 4px 12px rgba(0,0,0,.25);
}
.annotation::after {
content: ""; position: absolute;
border: 6px solid transparent;
}
.annotation.top::after { top: 100%; left: 20px; border-top-color: #1e293b; }
.annotation.bottom::after { bottom: 100%; left: 20px; border-bottom-color: #1e293b; }
.annotation.right::after { right: 100%; top: 12px; border-right-color: #1e293b; }
.annotation.left::after { left: 100%; top: 12px; border-left-color: #1e293b; }
body.show-annotations .annotation { display: block; }
/* ---- Tag indicator on sections ---- */
.annotated {
position: relative;
}
.annotated::before {
content: ""; display: none;
position: absolute; inset: -2px; border: 2px dashed var(--accent);
border-radius: inherit; pointer-events: none; z-index: 1;
}
body.show-annotations .annotated::before { display: block; }
/* ---- Responsive ---- */
@media (max-width: 900px) {
.sidebar { width: 220px; min-width: 220px; }
.content-inner { padding: 16px 20px 60px; }
.object-header { padding: 12px 20px 0; }
.form-grid { grid-template-columns: 1fr; }
.form-grid.col-3 { grid-template-columns: 1fr 1fr; }
.title-row { flex-direction: column; gap: 8px; }
.sticky-bar { padding: 10px 20px; }
}
</style>
</head>
<body>
<!-- Global navigation bar -->
<div class="global-bar">
<span class="logo">◆ FMS</span>
<span class="divider"></span>
<span class="app-name">模块管理</span>
<div class="global-actions">
<button>帮助</button>
<button>⚙</button>
<button style="font-weight:600">管理员</button>
</div>
</div>
<!-- Pattern switcher -->
<div class="pattern-tabs" id="patternTabs">
<button class="active" data-pattern="master-detail">布局一:Master-Detail(SAP Fiori 风格)</button>
<button data-pattern="salesforce">布局二:Record Page(Salesforce 风格)</button>
<button data-pattern="odoo">布局三:Notebook(Odoo 风格)</button>
</div>
<!-- Main shell -->
<div class="shell" id="shell">
<!-- ===== SIDEBAR ===== -->
<aside class="sidebar annotated" id="sidebar"
data-anno="左侧分类树宽度 240–320px 为黄金区间:太窄层级看不清,太宽挤占内容空间。SAP Fiori 和 Salesforce 均采用此范围。"
data-anno-pos="right">
<div class="sidebar-head">
<h3>分类导航</h3>
<button class="collapse-btn" id="collapseBtn" title="收起侧栏">◀</button>
</div>
<div class="sidebar-search"><input type="search" placeholder="搜索模块..." id="treeSearch"></div>
<nav class="tree-wrap" id="tree">
<!-- populated by JS -->
</nav>
<div class="sidebar-footer">
<button class="add-btn" title="新建模块"><span style="font-size:16px">+</span> <span>新建</span></button>
</div>
</aside>
<!-- ===== MAIN ===== -->
<main class="main" id="mainContent">
<header class="object-header annotated" id="objectHeader"
data-anno="对象头部是 ERP 页面最重要的区域之一:面包屑帮助定位,标题告知当前对象,标签页组织多维信息。标题行右侧放置全局操作(如保存、更多)。"
data-anno-pos="bottom">
<div class="breadcrumb">
<span>系统设置</span> <span style="color:var(--border-strong)">/</span>
<span>基础数据</span> <span style="color:var(--border-strong)">/</span>
<span class="current" id="bcCurrent">合作伙伴</span>
</div>
<div class="title-row">
<div class="title-left">
<h1 id="pageTitle">合作伙伴</h1>
<div class="title-meta">
<span id="pageType">数据模块</span>
<span class="dot">·</span>
<code style="font-size:11px;color:var(--text-muted)" id="pageCode">v_b_othercompany</code>
<span class="badge success" id="pageStatus">已启用</span>
</div>
</div>
<div class="title-actions annotated"
data-anno="主要操作放在标题行右侧。国外 ERP 的黄金法则:Save 在右、Delete/More 在左(或收起在下拉菜单中)。主按钮用高亮色、次按钮用描边。"
data-anno-pos="bottom">
<button class="btn btn-sm" id="moreBtn">•••</button>
<button class="btn primary" id="saveBtn"><span class="btn-icon">✓</span> 保存</button>
</div>
</div>
<nav class="tab-nav annotated" id="tabNav"
data-anno="Tab 导航承载一个对象的不同维度(基本信息/字段/权限/历史)。每个 Tab 是独立的内容区,避免把所有信息堆在一页。这是信息架构的核心手段。"
data-anno-pos="bottom">
<button class="active" data-tab="basic">基本信息</button>
<button data-tab="fields">字段定义</button>
<button data-tab="permissions">权限配置</button>
<button data-tab="history">操作日志</button>
</nav>
</header>
<!-- ===== CONTENT ===== -->
<div class="content" id="contentArea">
<!-- Pattern 1: MASTER-DETAIL (default) -->
<div class="tab-panel active" id="panel-basic" data-pattern="master-detail">
<!-- Basic info -->
<div class="content-inner">
<!-- Card: Identity -->
<div class="card annotated"
data-anno="卡片式分区(Card)是国外 ERP 最常用的信息组织方式。每个 Card 承载一组相关的字段,有明确的标题。相比无边框分区,视觉上更清晰。"
data-anno-pos="top">
<div class="card-header">
<div>
<h2>身份信息</h2>
<span class="card-hint">定义模块在系统中的基本身份标识</span>
</div>
</div>
<div class="card-body">
<div class="form-grid">
<div class="field"><label>模块名称 <span class="req">*</span></label><input class="form-control" value="合作伙伴" id="fModuleName"></div>
<div class="field"><label>模块编码 <span class="req">*</span></label><input class="form-control" value="v_b_othercompany"></div>
<div class="field"><label>模块类型 <span class="req">*</span></label>
<select class="form-control"><option selected>数据模块</option><option>页面</option><option>目录</option></select>
</div>
<div class="field"><label>上级模块</label><input class="form-control readonly" value="基础数据" readonly></div>
<div class="field"><label>菜单图标</label>
<select class="form-control"><option selected>📋 列表</option><option>📁 文件夹</option><option>📄 页面</option></select>
</div>
<div class="field"><label>排序号</label><input class="form-control" type="number" value="10"></div>
</div>
</div>
</div>
<!-- Card: Data Source -->
<div class="card annotated"
data-anno="将低频配置放在独立的 Card 中。如果有大量字段,使用折叠(collapse)进一步减少视觉噪音——只展开用户当前关注的部分。"
data-anno-pos="top">
<div class="card-header">
<div>
<h2>数据源配置 <span class="card-hint">数据模块从查询视图读取</span></h2>
</div>
<button class="btn btn-sm">测试连接</button>
</div>
<div class="card-body">
<div class="form-grid col-3">
<div class="field"><label>查询视图 <span class="req">*</span></label><input class="form-control" value="v_b_othercompany"></div>
<div class="field"><label>保存表 <span class="req">*</span></label><input class="form-control" value="b_othercompany"></div>
<div class="field"><label>主键字段</label><input class="form-control" value="b_id"></div>
<div class="field"><label>默认排序</label><input class="form-control" value="b_xh ASC, b_id DESC"></div>
<div class="field"><label>最大行数</label><input class="form-control" type="number" value="5000"></div>
<div class="field"><label>缓存时间(秒)</label><input class="form-control" type="number" value="300"></div>
</div>
</div>
</div>
<!-- Card: Switches -->
<div class="card annotated"
data-anno="开关型配置使用 Toggle 而非下拉框。一个 Toggle = 一个明确的 Boolean,比「是/否」下拉更直观。多个 Toggle 之间留够间距。"
data-anno-pos="top">
<div class="card-header"><h2>功能开关</h2></div>
<div class="card-body">
<div class="form-grid">
<div class="switch-row">
<label class="toggle"><input type="checkbox" checked id="swEnabled"><span class="track"></span></label>
<span class="switch-label">启用模块</span>
</div>
<div class="switch-row">
<label class="toggle"><input type="checkbox" checked><span class="track"></span></label>
<span class="switch-label">在菜单中显示</span>
</div>
<div class="switch-row">
<label class="toggle"><input type="checkbox"><span class="track"></span></label>
<span class="switch-label">允许导出</span>
</div>
<div class="switch-row">
<label class="toggle"><input type="checkbox"><span class="track"></span></label>
<span class="switch-label">启用审计日志</span>
</div>
</div>
</div>
</div>
<!-- Sticky action bar -->
<div class="sticky-bar annotated"
data-anno="关键模式:保存按钮固定在底部。用户滚动到任何位置都能保存,无需回到顶部。SAP Fiori 的 Footer Bar、Salesforce 的 Sticky Footer 都采用此模式。"
data-anno-pos="top">
<button class="btn">取消</button>
<button class="btn primary" id="stickySave">保存</button>
</div>
</div>
</div>
<!-- Tab: Fields -->
<div class="tab-panel" id="panel-fields" data-pattern="master-detail">
<div class="content-inner wide">
<div class="card annotated"
data-anno="表格中的操作按钮放在行末(Actions Column)。不同于国内常见的『操作』列标题,国外 ERP 更多用图标按钮或文字按钮,省略列标题。每行操作不超过 3 个,多余收入下拉菜单。"
data-anno-pos="top">
<div class="card-header">
<div><h2>字段定义</h2><span class="card-hint">从视图同步字段,调整显示属性</span></div>
<button class="btn btn-sm"><span class="btn-icon">↻</span> 同步字段</button>
</div>
<div class="card-body" style="padding:0">
<div class="table-wrap" style="border:0">
<table>
<thead>
<tr><th style="width:40px">序</th><th>字段名</th><th>显示名称</th><th>类型</th><th>宽度</th><th>状态</th><th style="width:80px"></th></tr>
</thead>
<tbody>
<tr>
<td><span class="drag-handle">⋮⋮</span></td>
<td><code>b_id</code></td><td>编号</td><td class="muted">数字</td><td>120</td>
<td><span class="badge success">启用</span></td>
<td class="row-actions"><button class="row-btn">编辑</button><button class="row-btn">•••</button></td>
</tr>
<tr>
<td><span class="drag-handle">⋮⋮</span></td>
<td><code>b_name</code></td><td>名称</td><td class="muted">文本</td><td>200</td>
<td><span class="badge success">启用</span></td>
<td class="row-actions"><button class="row-btn">编辑</button><button class="row-btn">•••</button></td>
</tr>
<tr>
<td><span class="drag-handle">⋮⋮</span></td>
<td><code>b_shortname</code></td><td>简称</td><td class="muted">文本</td><td>140</td>
<td><span class="badge success">启用</span></td>
<td class="row-actions"><button class="row-btn">编辑</button><button class="row-btn">•••</button></td>
</tr>
<tr>
<td><span class="drag-handle">⋮⋮</span></td>
<td><code>b_type</code></td><td>类型</td><td class="muted">选择</td><td>150</td>
<td><span class="badge success">启用</span></td>
<td class="row-actions"><button class="row-btn">编辑</button><button class="row-btn">•••</button></td>
</tr>
<tr>
<td><span class="drag-handle">⋮⋮</span></td>
<td><code>b_tel</code></td><td>电话</td><td class="muted">文本</td><td>130</td>
<td><span class="badge neutral">停用</span></td>
<td class="row-actions"><button class="row-btn">编辑</button><button class="row-btn">•••</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Permissions -->
<div class="tab-panel" id="panel-permissions" data-pattern="master-detail">
<div class="content-inner wide">
<div class="callout info">
<span class="callout-icon">ⓘ</span>
<div><strong>权限控制目前仅在前端生效。</strong>后端拦截尚未实现,建议在后续版本中增加 API 层面的权限校验。</div>
</div>
<div class="card">
<div class="card-header">
<div><h2>操作权限</h2><span class="card-hint">控制用户可执行的操作</span></div>
<button class="btn btn-sm"><span class="btn-icon">+</span> 添加</button>
</div>
<div class="card-body" style="padding:0">
<div class="table-wrap" style="border:0"><table>
<thead><tr><th>操作名称</th><th>操作编码</th><th>类型</th><th>状态</th><th style="width:80px"></th></tr></thead>
<tbody>
<tr><td>新增</td><td><code>add</code></td><td class="muted">数据操作</td><td><span class="badge success">启用</span></td><td class="row-actions"><button class="row-btn">编辑</button></td></tr>
<tr><td>编辑</td><td><code>edit</code></td><td class="muted">数据操作</td><td><span class="badge success">启用</span></td><td class="row-actions"><button class="row-btn">编辑</button></td></tr>
<tr><td>删除</td><td><code>delete</code></td><td class="muted">数据操作</td><td><span class="badge warning">受限</span></td><td class="row-actions"><button class="row-btn">编辑</button></td></tr>
<tr><td>导出</td><td><code>export</code></td><td class="muted">批量操作</td><td><span class="badge neutral">停用</span></td><td class="row-actions"><button class="row-btn">编辑</button></td></tr>
</tbody>
</table></div>
</div>
</div>
</div>
</div>
<!-- Tab: History -->
<div class="tab-panel" id="panel-history" data-pattern="master-detail">
<div class="content-inner wide">
<div class="card">
<div class="card-header"><h2>操作日志</h2></div>
<div class="card-body" style="padding:0">
<div class="table-wrap" style="border:0"><table>
<thead><tr><th>时间</th><th>操作</th><th>操作人</th><th>详情</th></tr></thead>
<tbody>
<tr><td class="muted">2026-08-08 15:32</td><td>修改</td><td>管理员</td><td>更新了字段「简称」的显示名称</td></tr>
<tr><td class="muted">2026-08-07 09:15</td><td>新增</td><td>张三</td><td>新增了操作权限「导出」</td></tr>
<tr><td class="muted">2026-08-05 17:48</td><td>同步</td><td>系统</td><td>从视图同步了 12 个字段</td></tr>
<tr><td class="muted">2026-08-01 10:00</td><td>创建</td><td>管理员</td><td>创建模块「合作伙伴」</td></tr>
</tbody>
</table></div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Annotation toggle button -->
<button class="annotation-toggle" id="annotationToggle" title="显示/隐藏设计标注">💡</button>
<script>
// ============================================================
// Tree data
// ============================================================
const treeData = [
{ type: 'group', label: '基础数据' },
{ id: 'partners', label: '合作伙伴', icon: '📋', active: true, children: [
{ id: 'contacts', label: '联系人', icon: '👤', count: 24 },
{ id: 'suppliers', label: '供应商', icon: '🏭', count: 18 },
]},
{ id: 'products', label: '产品管理', icon: '📦', count: 156 },
{ type: 'group', label: '业务管理' },
{ id: 'sea', label: '海运业务', icon: '🚢', children: [
{ id: 'shipment', label: '出运单', icon: '📄', count: 89 },
{ id: 'container', label: '集装箱', icon: '📦', count: 45 },
{ id: 'bill', label: '提单管理', icon: '📋', count: 67 },
]},
{ id: 'customs', label: '报关管理', icon: '🛃', count: 34 },
{ type: 'group', label: '系统管理' },
{ id: 'users', label: '用户管理', icon: '👥', children: [
{ id: 'roles', label: '角色管理', icon: '🔑', count: 8 },
{ id: 'depts', label: '部门管理', icon: '🏢', count: 12 },
]},
{ id: 'i18n', label: '多语言', icon: '🌐', count: 3 },
{ id: 'audit', label: '审计日志', icon: '📝' },
];
// ============================================================
// Module data for page content
// ============================================================
const moduleData = {
partners: { title: '合作伙伴', type: '数据模块', code: 'v_b_othercompany', status: 'success', statusText: '已启用' },
contacts: { title: '联系人', type: '数据模块', code: 'v_b_contact', status: 'success', statusText: '已启用' },
suppliers: { title: '供应商', type: '数据模块', code: 'v_b_supplier', status: 'success', statusText: '已启用' },
products: { title: '产品管理', type: '数据模块', code: 'v_b_product', status: 'success', statusText: '已启用' },
sea: { title: '海运业务', type: '页面', code: 'm_sea', status: 'success', statusText: '已启用' },
shipment: { title: '出运单', type: '数据模块', code: 'd_shipment', status: 'success', statusText: '已启用' },
container: { title: '集装箱', type: '数据模块', code: 'd_container', status: 'warning', statusText: '草稿' },
bill: { title: '提单管理', type: '数据模块', code: 'd_bill', status: 'success', statusText: '已启用' },
customs: { title: '报关管理', type: '数据模块', code: 'd_customs', status: 'success', statusText: '已启用' },
users: { title: '用户管理', type: '页面', code: 'b_user', status: 'success', statusText: '已启用' },
roles: { title: '角色管理', type: '页面', code: 'b_role', status: 'success', statusText: '已启用' },
depts: { title: '部门管理', type: '页面', code: 'b_dept', status: 'success', statusText: '已启用' },
i18n: { title: '多语言', type: '页面', code: 'b_i18n', status: 'success', statusText: '已启用' },
audit: { title: '审计日志', type: '页面', code: 'b_audit_log', status: 'success', statusText: '已启用' },
};
// ============================================================
// Build tree
// ============================================================
function buildTree() {
const tree = document.getElementById('tree');
tree.innerHTML = '';
treeData.forEach(item => {
if (item.type === 'group') {
const g = document.createElement('div');
g.className = 'tree-group-label';
g.textContent = item.label;
tree.appendChild(g);
} else {
const btn = document.createElement('button');
btn.className = 'tree-node' + (item.active ? ' active' : '');
btn.dataset.moduleId = item.id;
btn.innerHTML = (item.children ? '<span class="chevron">▶</span>' : '<span class="chevron"></span>') +
'<span class="node-icon">' + (item.icon || '') + '</span>' +
'<span class="node-label">' + item.label + '</span>' +
(item.count ? '<span class="node-badge">' + item.count + '</span>' : '');
btn.addEventListener('click', () => selectModule(item.id, btn));
tree.appendChild(btn);
// Children
if (item.children) {
item.children.forEach(child => {
const cbtn = document.createElement('button');
cbtn.className = 'tree-node child' + (child.active ? ' active' : '');
cbtn.dataset.moduleId = child.id;
cbtn.dataset.parentId = item.id;
cbtn.innerHTML = '<span class="chevron"></span>' +
'<span class="node-icon">' + (child.icon || '') + '</span>' +
'<span class="node-label">' + child.label + '</span>' +
(child.count ? '<span class="node-badge">' + child.count + '</span>' : '');
cbtn.addEventListener('click', () => selectModule(child.id, cbtn));
tree.appendChild(cbtn);
cbtn.style.display = item.active ? '' : 'none';
});
}
}
});
}
let currentParent = null;
function selectModule(id, btn) {
document.querySelectorAll('.tree-node').forEach(n => n.classList.remove('active'));
btn.classList.add('active');
// Collapse/expand parent
if (btn.dataset.parentId) {
currentParent = btn.dataset.parentId;
} else if (treeData.find(t => t.id === id && t.children)) {
// Toggle children
const isOpen = btn.querySelector('.chevron').textContent === '▼';
btn.querySelector('.chevron').textContent = isOpen ? '▶' : '▼';
document.querySelectorAll('.tree-node.child[data-parent-id="' + id + '"]').forEach(c => {
c.style.display = isOpen ? 'none' : '';
});
currentParent = null;
}
const data = moduleData[id];
if (!data) return;
document.getElementById('pageTitle').textContent = data.title;
document.getElementById('pageType').textContent = data.type;
document.getElementById('pageCode').textContent = data.code;
document.getElementById('bcCurrent').textContent = data.title;
const statusEl = document.getElementById('pageStatus');
statusEl.textContent = data.statusText;
statusEl.className = 'badge ' + data.status;
document.getElementById('fModuleName').value = data.title;
}
// ============================================================
// Tabs
// ============================================================
document.querySelectorAll('#tabNav button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#tabNav button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
});
});
// ============================================================
// Pattern switching
// ============================================================
document.querySelectorAll('#patternTabs button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#patternTabs button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const pattern = btn.dataset.pattern;
const content = document.getElementById('contentArea');
const shell = document.getElementById('shell');
// Remove all pattern classes
content.classList.remove('pattern-salesforce', 'pattern-odoo');
shell.classList.remove('pattern-salesforce', 'pattern-odoo');
if (pattern === 'salesforce') {
content.classList.add('pattern-salesforce');
applySalesforceLayout();
} else if (pattern === 'odoo') {
content.classList.add('pattern-odoo');
applyOdooLayout();
} else {
applyMasterDetailLayout();
}
});
});
function applyMasterDetailLayout() {
// Reset to default — cards + sticky bar
document.querySelectorAll('.content-inner').forEach(el => {
el.style.maxWidth = '960px';
});
// Show sticky bar
document.querySelectorAll('.sticky-bar').forEach(el => el.style.display = 'flex');
// Standard card style
document.querySelectorAll('.card').forEach(el => {
el.style.borderRadius = 'var(--radius-lg)';
el.style.boxShadow = 'none';
});
}
function applySalesforceLayout() {
// Salesforce style: full-width, section-based (no card borders), related lists at bottom
document.querySelectorAll('.content-inner').forEach(el => {
el.style.maxWidth = '100%';
el.style.padding = '0';
});
// Convert cards to borderless sections with left accent
document.querySelectorAll('.card').forEach((el, i) => {
el.style.borderRadius = '0';
el.style.border = 'none';
el.style.borderBottom = '1px solid var(--border)';
el.style.marginBottom = '0';
el.style.boxShadow = 'none';
// Add Salesforce-style left accent on hover
el.style.borderLeft = '3px solid transparent';
el.addEventListener('mouseenter', () => el.style.borderLeftColor = 'var(--accent)');
el.addEventListener('mouseleave', () => el.style.borderLeftColor = 'transparent');
});
// Hide sticky bar (Salesforce uses top save)
document.querySelectorAll('.sticky-bar').forEach(el => el.style.display = 'none');
// Show annotation
const callout = document.querySelector('.callout');
if (callout) callout.style.borderRadius = '0';
}
function applyOdooLayout() {
// Odoo style: sheet-like centered form (like a piece of paper), notebook tabs inline
document.querySelectorAll('.content-inner').forEach(el => {
el.style.maxWidth = '100%';
el.style.padding = '0';
el.style.display = 'flex';
el.style.justifyContent = 'center';
});
// Create a "sheet" wrapper effect
document.querySelectorAll('.content-inner > *').forEach(el => {
if (el.classList.contains('card')) {
el.style.maxWidth = '900px';
el.style.width = '100%';
el.style.borderRadius = '0';
el.style.border = '1px solid var(--border)';
el.style.boxShadow = 'var(--shadow-sm)';
el.style.marginBottom = '1px';
}
});
// Show sticky bar
document.querySelectorAll('.sticky-bar').forEach(el => {
el.style.display = 'flex';
el.style.justifyContent = 'center';
el.querySelectorAll('.btn').forEach(b => b.style.minWidth = '120px');
});
}
// ============================================================
// Annotations toggle
// ============================================================
const annoToggle = document.getElementById('annotationToggle');
annoToggle.addEventListener('click', () => {
document.body.classList.toggle('show-annotations');
annoToggle.classList.toggle('active');
});
// ============================================================
// Sidebar collapse
// ============================================================
document.getElementById('collapseBtn').addEventListener('click', () => {
document.getElementById('sidebar').classList.toggle('collapsed');
const btn = document.getElementById('collapseBtn');
btn.textContent = document.getElementById('sidebar').classList.contains('collapsed') ? '▶' : '◀';
});
// ============================================================
// Search
// ============================================================
document.getElementById('treeSearch').addEventListener('input', (e) => {
const q = e.target.value.trim().toLowerCase();
document.querySelectorAll('.tree-node').forEach(n => {
const label = n.querySelector('.node-label');
if (!label) return;
n.style.display = q && !label.textContent.toLowerCase().includes(q) ? 'none' : '';
});
document.querySelectorAll('.tree-group-label').forEach(g => {
g.style.display = q ? 'none' : '';
});
});
// ============================================================
// Save
// ============================================================
function showSaved() {
const badge = document.getElementById('pageStatus');
badge.textContent = '已保存';
badge.className = 'badge success';
setTimeout(() => {
badge.textContent = moduleData[currentId]?.statusText || '已启用';
badge.className = 'badge ' + (moduleData[currentId]?.status || 'success');
}, 1500);
}
document.getElementById('saveBtn').addEventListener('click', showSaved);
document.getElementById('stickySave').addEventListener('click', showSaved);
// ============================================================
// Init
// ============================================================
let currentId = 'partners';
buildTree();
// Handle window resize for responsive annotations
window.addEventListener('resize', updateAnnotationPositions);
</script>
</body>
</html>