1092 lines
48 KiB
HTML
1092 lines
48 KiB
HTML
<!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>
|