Files
workspace/code/fms/module-management-demo.html
T
2026-08-04 22:31:52 +08:00

376 lines
20 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>模块管理原型</title>
<style>
:root {
--bg: #f4f6f8;
--surface: #ffffff;
--surface-muted: #f8fafb;
--line: #e3e8ed;
--line-strong: #cbd5df;
--text: #1f2a37;
--muted: #6b7785;
--primary: #2166d1;
--primary-soft: #eaf2ff;
--green: #198754;
--amber: #b7791f;
--shadow: 0 8px 24px rgba(29, 43, 58, 0.06);
}
* { box-sizing: border-box; }
body {
margin: 0;
color: var(--text);
background: var(--bg);
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
}
button, input, select { font: inherit; }
button { cursor: pointer; }
.app-shell { min-height: 100vh; padding: 20px; }
.app-bar {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1440px;
margin: 0 auto 14px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: grid;
width: 34px;
height: 34px;
place-items: center;
border-radius: 8px;
color: #fff;
background: #174a96;
font-weight: 700;
}
.brand-title { font-size: 18px; font-weight: 650; }
.brand-subtitle { margin-left: 8px; color: var(--muted); }
.app-actions { display: flex; align-items: center; gap: 8px; }
.user-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface);
color: var(--muted);
}
.avatar {
display: grid;
width: 24px;
height: 24px;
place-items: center;
border-radius: 50%;
color: #174a96;
background: #dbe9ff;
font-size: 12px;
font-weight: 700;
}
.workspace {
display: grid;
grid-template-columns: 330px minmax(0, 1fr);
max-width: 1440px;
min-height: 720px;
margin: 0 auto;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 9px;
background: var(--surface);
box-shadow: var(--shadow);
}
.sidebar { display: flex; min-width: 0; flex-direction: column; border-right: 1px solid var(--line); }
.sidebar-head { padding: 18px 16px 12px; border-bottom: 1px solid var(--line); }
.sidebar-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.sidebar-title strong { font-size: 15px; }
.count { color: var(--muted); font-size: 12px; }
.search { position: relative; }
.search input {
width: 100%;
height: 34px;
padding: 0 10px 0 32px;
border: 1px solid var(--line-strong);
border-radius: 6px;
outline: none;
color: var(--text);
}
.search input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(33, 102, 209, 0.11); }
.search-icon { position: absolute; top: 6px; left: 11px; color: var(--muted); font-size: 16px; }
.tree { flex: 1; overflow: auto; padding: 10px 8px 16px; }
.tree-group { margin-bottom: 16px; }
.tree-label {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
color: var(--muted);
font-size: 12px;
font-weight: 650;
letter-spacing: 0.02em;
}
.tree-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-height: 36px;
padding: 7px 10px 7px 18px;
border: 0;
border-radius: 6px;
color: var(--text);
background: transparent;
text-align: left;
}
.tree-row:hover { background: var(--surface-muted); }
.tree-row.active { color: #174a96; background: var(--primary-soft); font-weight: 600; }
.tree-row.child { padding-left: 38px; }
.tree-row.grandchild { padding-left: 62px; }
.node-icon { width: 17px; color: var(--muted); text-align: center; font-size: 13px; }
.tree-row.active .node-icon { color: var(--primary); }
.node-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.node-code { margin-left: auto; color: #98a3ae; font-size: 11px; font-weight: 400; }
.sidebar-foot { padding: 11px 16px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; }
.content { min-width: 0; overflow: auto; background: #fbfcfd; }
.content-head { padding: 22px 28px 0; background: var(--surface); border-bottom: 1px solid var(--line); }
.breadcrumb { color: var(--muted); font-size: 12px; }
.title-line { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-top: 8px; }
h1 { margin: 0; font-size: 22px; line-height: 1.3; font-weight: 650; }
.title-meta { display: flex; align-items: center; gap: 8px; margin-top: 6px; color: var(--muted); }
.tag { display: inline-flex; align-items: center; height: 23px; padding: 0 8px; border-radius: 5px; color: #315a8b; background: #edf4fc; font-size: 12px; }
.tag.green { color: #176b47; background: #e8f6ef; }
.tag.amber { color: #8d641e; background: #fff6dc; }
.head-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 34px;
padding: 0 12px;
border: 1px solid var(--line-strong);
border-radius: 6px;
color: var(--text);
background: var(--surface);
}
.btn:hover { border-color: var(--primary); color: var(--primary); }
.btn.primary { border-color: var(--primary); color: #fff; background: var(--primary); }
.btn.primary:hover { background: #1956b5; }
.btn.ghost { border-color: transparent; color: var(--muted); }
.btn.danger { color: #b42318; }
.tabs { display: flex; gap: 24px; margin-top: 18px; }
.tab {
position: relative;
padding: 10px 2px 12px;
border: 0;
color: var(--muted);
background: transparent;
}
.tab.active { color: var(--primary); font-weight: 600; }
.tab.active::after { position: absolute; right: 0; bottom: -1px; left: 0; height: 2px; background: var(--primary); content: ""; }
.content-body { max-width: 1020px; padding: 24px 28px 36px; }
.notice { display: flex; align-items: center; gap: 8px; margin-bottom: 18px; padding: 10px 12px; border: 1px solid #cfe0f8; border-radius: 6px; color: #315a8b; background: #f2f7ff; }
.section { margin-bottom: 22px; }
.section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.section-head h2 { margin: 0; font-size: 15px; font-weight: 650; }
.section-head span { color: var(--muted); font-size: 12px; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 18px; padding: 18px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface); }
.field.full { grid-column: 1 / -1; }
label { display: block; margin-bottom: 6px; color: #4f5d6b; font-size: 12px; font-weight: 600; }
.input, .select {
width: 100%;
height: 34px;
padding: 0 10px;
border: 1px solid var(--line-strong);
border-radius: 6px;
outline: none;
color: var(--text);
background: #fff;
}
.input:focus, .select:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(33, 102, 209, 0.11); }
.switch-line { display: flex; align-items: center; justify-content: space-between; min-height: 34px; padding: 0 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-muted); }
.switch { position: relative; width: 34px; height: 20px; border: 0; border-radius: 12px; background: #b7c1cb; }
.switch::after { position: absolute; top: 3px; left: 3px; width: 14px; height: 14px; border-radius: 50%; background: #fff; content: ""; transition: transform .16s; }
.switch.on { background: var(--primary); }
.switch.on::after { transform: translateX(14px); }
.data-table { width: 100%; border-collapse: collapse; overflow: hidden; border: 1px solid var(--line); border-radius: 7px; background: var(--surface); }
.data-table th, .data-table td { padding: 11px 13px; border-bottom: 1px solid var(--line); text-align: left; }
.data-table th { color: var(--muted); background: var(--surface-muted); font-size: 12px; font-weight: 600; }
.data-table tr:last-child td { border-bottom: 0; }
.data-table .muted { color: var(--muted); }
.drag { color: #9aa6b2; }
.check { color: var(--green); font-weight: 650; }
.bottom-note { margin-top: 20px; color: var(--muted); font-size: 12px; }
.toast { position: fixed; right: 24px; bottom: 24px; display: none; padding: 10px 14px; border: 1px solid #cfe0f8; border-radius: 6px; color: #315a8b; background: #f2f7ff; box-shadow: var(--shadow); }
.toast.show { display: block; }
@media (max-width: 900px) {
.app-shell { padding: 10px; }
.workspace { grid-template-columns: 260px minmax(0, 1fr); min-height: 680px; }
.content-head, .content-body { padding-right: 18px; padding-left: 18px; }
.title-line { flex-direction: column; }
.head-actions { justify-content: flex-start; }
}
@media (max-width: 680px) {
.app-bar { align-items: flex-start; gap: 12px; flex-direction: column; }
.workspace { display: block; }
.sidebar { min-height: 280px; border-right: 0; border-bottom: 1px solid var(--line); }
.form-grid { grid-template-columns: 1fr; }
.field.full { grid-column: auto; }
.content { min-height: 600px; }
}
</style>
</head>
<body>
<div class="app-shell">
<header class="app-bar">
<div class="brand">
<div class="brand-mark">F</div>
<div><span class="brand-title">模块管理</span><span class="brand-subtitle">统一维护目录、页面和数据模块</span></div>
</div>
<div class="app-actions">
<button class="btn ghost" type="button" data-action="refresh">↻ 刷新</button>
<div class="user-chip"><span class="avatar">管</span><span>系统管理员</span><span class="tag green">可维护</span></div>
</div>
</header>
<main class="workspace">
<aside class="sidebar">
<div class="sidebar-head">
<div class="sidebar-title"><strong>业务结构</strong><span class="count">12 个资源</span></div>
<div class="search"><span class="search-icon">⌕</span><input id="tree-search" type="search" placeholder="搜索目录、页面或模块" /></div>
</div>
<div class="tree" id="tree">
<div class="tree-group">
<div class="tree-label">⌄ 业务目录</div>
<button class="tree-row" data-node="directory"><span class="node-icon">□</span><span class="node-text">基础资料</span><span class="node-code">base</span></button>
<button class="tree-row child" data-node="page"><span class="node-icon">▣</span><span class="node-text">往来单位</span><span class="node-code">contact</span></button>
<button class="tree-row grandchild active" data-node="data"><span class="node-icon">▤</span><span class="node-text">客户联系人</span><span class="node-code">contact_person</span></button>
<button class="tree-row child" data-node="page"><span class="node-icon">▣</span><span class="node-text">其他公司</span><span class="node-code">othercompany</span></button>
<button class="tree-row grandchild" data-node="data"><span class="node-icon">▤</span><span class="node-text">公司联系人</span><span class="node-code">company_contact</span></button>
<button class="tree-row" data-node="directory"><span class="node-icon">□</span><span class="node-text">系统管理</span><span class="node-code">system</span></button>
<button class="tree-row child" data-node="page"><span class="node-icon">▣</span><span class="node-text">用户管理</span><span class="node-code">user</span></button>
<button class="tree-row grandchild" data-node="data"><span class="node-icon">▤</span><span class="node-text">用户列表</span><span class="node-code">sys_user</span></button>
</div>
<div class="tree-group">
<div class="tree-label">⌄ 未挂载资源</div>
<button class="tree-row child" data-node="orphan-data"><span class="node-icon">▤</span><span class="node-text">登录日志</span><span class="node-code">login_log</span></button>
<button class="tree-row child" data-node="orphan-page"><span class="node-icon">▣</span><span class="node-text">多语言类型</span><span class="node-code">i18n_type</span></button>
</div>
</div>
<div class="sidebar-foot">拖动节点可以调整目录和页面顺序</div>
</aside>
<section class="content">
<div class="content-head">
<div class="breadcrumb">基础资料 / 往来单位 / 数据模块</div>
<div class="title-line">
<div>
<h1 id="page-title">客户联系人</h1>
<div class="title-meta"><span class="tag green">数据模块</span><span>contact_person</span><span>已启用</span></div>
</div>
<div class="head-actions">
<button class="btn" type="button" data-action="link">关联页面</button>
<button class="btn danger" type="button" data-action="delete">删除</button>
<button class="btn primary" type="button" data-action="save">保存更改</button>
</div>
</div>
<nav class="tabs" aria-label="当前对象配置">
<button class="tab active" data-tab="overview">基本信息</button>
<button class="tab" data-tab="fields">字段</button>
<button class="tab" data-tab="list">列表</button>
<button class="tab" data-tab="form">表单</button>
<button class="tab" data-tab="query">查询</button>
<button class="tab" data-tab="actions">操作权限</button>
</nav>
</div>
<div class="content-body" id="panel-overview">
<div class="notice"><span>✓</span><span>当前使用方式:默认。显示名称支持多语言,未翻译时自动回退到中文名称。</span><button class="btn ghost" type="button" data-action="i18n">管理多语言</button></div>
<section class="section">
<div class="section-head"><h2>模块基本信息</h2><span>保存后立即对运行页面生效</span></div>
<div class="form-grid">
<div class="field"><label>技术编码</label><input class="input" value="contact_person" disabled /></div>
<div class="field"><label>显示名称</label><input class="input" value="客户联系人" /></div>
<div class="field"><label>查询对象</label><input class="input" value="v_contact_person" /></div>
<div class="field"><label>保存对象</label><input class="input" value="b_contact_person" /></div>
<div class="field"><label>主键字段</label><input class="input" value="b_id" /></div>
<div class="field"><label>默认排序</label><input class="input" value="b_xh ASC, b_id DESC" /></div>
<div class="field"><label>默认使用方式</label><select class="select"><option>默认(default)</option><option>只读(readonly)</option></select></div>
<div class="field"><label>启用状态</label><div class="switch-line"><span>允许页面使用</span><button class="switch on" type="button" aria-label="切换启用状态"></button></div></div>
</div>
</section>
<section class="section">
<div class="section-head"><h2>字段概览</h2><button class="btn" type="button" data-tab-jump="fields">查看全部字段</button></div>
<table class="data-table">
<thead><tr><th>字段</th><th>名称</th><th>类型</th><th>列表</th><th>表单</th><th>状态</th></tr></thead>
<tbody>
<tr><td><strong>b_name</strong></td><td>姓名</td><td class="muted">varchar</td><td class="check">显示</td><td class="check">编辑</td><td><span class="tag green">启用</span></td></tr>
<tr><td><strong>b_mobile</strong></td><td>手机</td><td class="muted">varchar</td><td class="check">显示</td><td class="check">编辑</td><td><span class="tag green">启用</span></td></tr>
<tr><td><strong>b_remark</strong></td><td>备注</td><td class="muted">nvarchar</td><td class="muted">隐藏</td><td class="check">编辑</td><td><span class="tag green">启用</span></td></tr>
</tbody>
</table>
</section>
<div class="bottom-note">权限范围:当前账号可以编辑模块配置、同步字段和管理操作权限。</div>
</div>
</section>
</main>
</div>
<div class="toast" id="toast" role="status"></div>
<script>
const toast = document.getElementById("toast");
const title = document.getElementById("page-title");
const search = document.getElementById("tree-search");
const rows = [...document.querySelectorAll(".tree-row")];
let toastTimer;
function showToast(text) {
toast.textContent = text;
toast.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toast.classList.remove("show"), 2200);
}
rows.forEach((row) => row.addEventListener("click", () => {
rows.forEach((item) => item.classList.remove("active"));
row.classList.add("active");
const text = row.querySelector(".node-text").textContent;
title.textContent = text;
showToast(`已切换到「${text}」`);
}));
document.querySelectorAll(".tab").forEach((tab) => tab.addEventListener("click", () => {
document.querySelectorAll(".tab").forEach((item) => item.classList.remove("active"));
tab.classList.add("active");
showToast(`打开${tab.textContent}配置`);
}));
document.querySelectorAll("[data-tab-jump]").forEach((button) => button.addEventListener("click", () => {
document.querySelector(`[data-tab='${button.dataset.tabJump}']`).click();
}));
document.querySelectorAll("[data-action]").forEach((button) => button.addEventListener("click", () => {
const messages = { save: "更改已保存", refresh: "配置已刷新", link: "打开关联页面窗口", delete: "删除操作需要再次确认", i18n: "打开多语言管理" };
showToast(messages[button.dataset.action]);
}));
document.querySelectorAll(".switch").forEach((button) => button.addEventListener("click", () => button.classList.toggle("on")));
search.addEventListener("input", () => {
const keyword = search.value.trim().toLowerCase();
rows.forEach((row) => {
row.hidden = keyword && !row.textContent.toLowerCase().includes(keyword);
});
});
</script>
</body>
</html>