fix: add tabbed layout to super admin console
This commit is contained in:
parent
168cdf6470
commit
3a9bf9aeca
|
|
@ -89,6 +89,46 @@
|
|||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
border: none;
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
padding: 10px 18px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background: #c7d2fe;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: linear-gradient(135deg, #4f46e5, #6366f1);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.25);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
|
|
@ -158,6 +198,281 @@
|
|||
color: #4f46e5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.org-chart-card h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.org-chart-controls {
|
||||
background: #f8f9ff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0 24px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.control-group .search-input {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.control-group .search-input:focus {
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
|
||||
}
|
||||
|
||||
.control-group .search-btn,
|
||||
.control-group .reset-btn {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.control-group .search-btn {
|
||||
background: linear-gradient(135deg, #4f46e5, #6366f1);
|
||||
color: #fff;
|
||||
box-shadow: 0 6px 16px rgba(79, 70, 229, 0.35);
|
||||
}
|
||||
|
||||
.control-group .reset-btn {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-bar strong {
|
||||
color: #4f46e5;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.org-chart-container {
|
||||
min-height: 260px;
|
||||
position: relative;
|
||||
padding: 10px 0 30px;
|
||||
}
|
||||
|
||||
.org-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin: 10px 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.tree-node-box {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
|
||||
min-width: 200px;
|
||||
max-width: 280px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-node-box:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 28px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.tree-node.root-node > .tree-node-box {
|
||||
background: linear-gradient(135deg, #4338ca 0%, #6d28d9 100%);
|
||||
border: 2px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.tree-node-box .node-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tree-node-box .node-code {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tree-node-box .node-region {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tree-level {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 28px;
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tree-level.root-level {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.tree-level:not(.root-level) {
|
||||
margin-top: 40px;
|
||||
padding-top: 26px;
|
||||
}
|
||||
|
||||
.tree-level:not(.root-level)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #94a3b8;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tree-level.single-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree-children {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.node-children {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 20px 14px 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.node-children::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: #94a3b8;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tree-node.has-children > .tree-node-box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: #94a3b8;
|
||||
transform: translateX(-50%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
color: white;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tooltip.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tree-node.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: #fef08a;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #6b7280;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.no-results-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
padding: 28px 36px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 36px rgba(0, 0, 0, 0.12);
|
||||
z-index: 10;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.overlay-close-btn {
|
||||
margin-top: 14px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.action {
|
||||
border: none;
|
||||
background: none;
|
||||
|
|
@ -220,135 +535,174 @@
|
|||
|
||||
<div class="message" id="messageBar"></div>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>用户管理</h2>
|
||||
<div class="section">
|
||||
<h3>用户添加</h3>
|
||||
<form id="userCreateForm">
|
||||
<label>用户名
|
||||
<input type="text" name="username" required placeholder="请输入登录账号">
|
||||
<div class="tabs-container">
|
||||
<div class="tab-nav">
|
||||
<button class="tab-button active" data-tab="users-tab">👤 用户管理</button>
|
||||
<button class="tab-button" data-tab="departments-tab">🏢 服务部门</button>
|
||||
<button class="tab-button" data-tab="themes-tab">📋 主题管理</button>
|
||||
<button class="tab-button" data-tab="templates-tab">📄 模板管理</button>
|
||||
<button class="tab-button" data-tab="org-chart-tab">🌳 组织架构</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="users-tab">
|
||||
<section class="card">
|
||||
<h2>用户管理</h2>
|
||||
<div class="section">
|
||||
<h3>用户添加</h3>
|
||||
<form id="userCreateForm">
|
||||
<label>用户名
|
||||
<input type="text" name="username" required placeholder="请输入登录账号">
|
||||
</label>
|
||||
<label>初始密码
|
||||
<input type="password" name="password" required placeholder="设置登录密码">
|
||||
</label>
|
||||
<label>显示名称
|
||||
<input type="text" name="display_name" placeholder="可选">
|
||||
</label>
|
||||
<label>绑定服务部门
|
||||
<select name="service_department_id" id="userCreateDept">
|
||||
<option value="">暂不绑定</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary" type="submit">创建用户</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>密码 / 部门调整</h3>
|
||||
<form id="userUpdateForm">
|
||||
<label>选择用户
|
||||
<select name="user_id" id="userUpdateSelect" required></select>
|
||||
</label>
|
||||
<label>新密码(留空表示不修改)
|
||||
<input type="password" name="password" placeholder="可选">
|
||||
</label>
|
||||
<label>新的服务部门
|
||||
<select name="service_department_id" id="userUpdateDept">
|
||||
<option value="">保持不变 / 清除绑定</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary" type="submit">保存调整</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>用户列表</h3>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>显示名</th>
|
||||
<th>部门</th>
|
||||
<th>角色</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="departments-tab">
|
||||
<section class="card">
|
||||
<h2>服务部门管理</h2>
|
||||
<form id="deptCreateForm">
|
||||
<label>部门名称
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label>初始密码
|
||||
<input type="password" name="password" required placeholder="设置登录密码">
|
||||
<label>联系电话
|
||||
<input type="text" name="phone" placeholder="座机或手机号">
|
||||
</label>
|
||||
<label>显示名称
|
||||
<input type="text" name="display_name" placeholder="可选">
|
||||
</label>
|
||||
<label>绑定服务部门
|
||||
<select name="service_department_id" id="userCreateDept">
|
||||
<option value="">暂不绑定</option>
|
||||
<label>上级部门
|
||||
<select name="parent_id" id="deptParentSelect">
|
||||
<option value="">设为顶级</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary" type="submit">创建用户</button>
|
||||
<label>备注
|
||||
<textarea name="description" rows="2" placeholder="可填写简要职责"></textarea>
|
||||
</label>
|
||||
<button class="primary" type="submit">新增服务部门</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>密码 / 部门调整</h3>
|
||||
<form id="userUpdateForm">
|
||||
<label>选择用户
|
||||
<select name="user_id" id="userUpdateSelect" required></select>
|
||||
</label>
|
||||
<label>新密码(留空表示不修改)
|
||||
<input type="password" name="password" placeholder="可选">
|
||||
</label>
|
||||
<label>新的服务部门
|
||||
<select name="service_department_id" id="userUpdateDept">
|
||||
<option value="">保持不变 / 清除绑定</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary" type="submit">保存调整</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>用户列表</h3>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>显示名</th>
|
||||
<th>部门</th>
|
||||
<th>角色</th>
|
||||
<th>创建时间</th>
|
||||
<th>名称</th>
|
||||
<th>上级</th>
|
||||
<th>电话</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody"></tbody>
|
||||
<tbody id="deptTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>服务部门管理</h2>
|
||||
<form id="deptCreateForm">
|
||||
<label>部门名称
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label>联系电话
|
||||
<input type="text" name="phone" placeholder="座机或手机号">
|
||||
</label>
|
||||
<label>上级部门
|
||||
<select name="parent_id" id="deptParentSelect">
|
||||
<option value="">设为顶级</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>备注
|
||||
<textarea name="description" rows="2" placeholder="可填写简要职责"></textarea>
|
||||
</label>
|
||||
<button class="primary" type="submit">新增服务部门</button>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>上级</th>
|
||||
<th>电话</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="deptTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<div class="tab-content" id="themes-tab">
|
||||
<section class="card">
|
||||
<h2>主题列表管理</h2>
|
||||
<form id="themeCreateForm">
|
||||
<label>主题名称
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<button class="primary" type="submit">添加主题</button>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>主题</th>
|
||||
<th>关联许可</th>
|
||||
<th>覆盖区划</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="themeTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>主题列表管理</h2>
|
||||
<form id="themeCreateForm">
|
||||
<label>主题名称
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<button class="primary" type="submit">添加主题</button>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>主题</th>
|
||||
<th>关联许可</th>
|
||||
<th>覆盖区划</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="themeTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<div class="tab-content" id="templates-tab">
|
||||
<section class="card">
|
||||
<h2>模板管理</h2>
|
||||
<div class="template-meta" id="templateMeta">
|
||||
模板未加载
|
||||
</div>
|
||||
<form id="templateForm">
|
||||
<label>上传新的 Excel 模板
|
||||
<input type="file" name="file" accept=".xlsx,.xls" required>
|
||||
</label>
|
||||
<button class="primary" type="submit">覆盖模板</button>
|
||||
</form>
|
||||
<button class="primary" type="button" id="downloadTemplateBtn">下载当前模板</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2>模板管理</h2>
|
||||
<div class="template-meta" id="templateMeta">
|
||||
模板未加载
|
||||
</div>
|
||||
<form id="templateForm">
|
||||
<label>上传新的 Excel 模板
|
||||
<input type="file" name="file" accept=".xlsx,.xls" required>
|
||||
</label>
|
||||
<button class="primary" type="submit">覆盖模板</button>
|
||||
</form>
|
||||
<button class="primary" type="button" id="downloadTemplateBtn">下载当前模板</button>
|
||||
</section>
|
||||
<div class="tab-content" id="org-chart-tab">
|
||||
<section class="card org-chart-card">
|
||||
<h2><span>🌳</span> 组织架构</h2>
|
||||
<div class="org-chart-controls">
|
||||
<div class="control-group">
|
||||
<input type="text" id="orgSearchInput" class="search-input" placeholder="搜索部门名称、编码或区域...">
|
||||
<button type="button" id="orgSearchBtn" class="search-btn">搜索</button>
|
||||
<button type="button" id="orgResetBtn" class="reset-btn">重置</button>
|
||||
</div>
|
||||
<div class="stats-bar">
|
||||
<span>📊 总部门: <strong id="totalDepts">0</strong></span>
|
||||
<span>🌿 层级: <strong id="totalLevels">0</strong></span>
|
||||
<span>🪴 根部门: <strong id="rootDepts">0</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="org-chart-container">
|
||||
<div class="org-tree" id="orgChartContainer">
|
||||
<div class="loading">正在加载组织架构...</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -363,6 +717,12 @@ const deptParentSelect = document.getElementById('deptParentSelect');
|
|||
const deptTableBody = document.getElementById('deptTableBody');
|
||||
const themeTableBody = document.getElementById('themeTableBody');
|
||||
const templateMetaBox = document.getElementById('templateMeta');
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
const orgChartContainer = document.getElementById('orgChartContainer');
|
||||
const totalDeptsEl = document.getElementById('totalDepts');
|
||||
const totalLevelsEl = document.getElementById('totalLevels');
|
||||
const rootDeptsEl = document.getElementById('rootDepts');
|
||||
let state = {
|
||||
users: [],
|
||||
departments: [],
|
||||
|
|
@ -370,6 +730,14 @@ let state = {
|
|||
templateMeta: {}
|
||||
};
|
||||
|
||||
let orgChartData = {
|
||||
tree: [],
|
||||
allNodes: [],
|
||||
parentMap: {},
|
||||
nodeMap: {}
|
||||
};
|
||||
let orgChartLoaded = false;
|
||||
|
||||
function showMessage(text, type = 'info') {
|
||||
if (!text) {
|
||||
messageBar.classList.remove('show');
|
||||
|
|
@ -667,6 +1035,353 @@ themeTableBody.addEventListener('click', async (evt) => {
|
|||
}
|
||||
});
|
||||
|
||||
function switchTab(tabId) {
|
||||
tabContents.forEach(content => {
|
||||
content.classList.toggle('active', content.id === tabId);
|
||||
});
|
||||
tabButtons.forEach(button => {
|
||||
button.classList.toggle('active', button.dataset.tab === tabId);
|
||||
});
|
||||
if (tabId === 'org-chart-tab' && !orgChartLoaded) {
|
||||
loadOrgChart();
|
||||
}
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const targetTab = button.dataset.tab;
|
||||
if (targetTab) {
|
||||
switchTab(targetTab);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadOrgChart() {
|
||||
if (!orgChartContainer) {
|
||||
return;
|
||||
}
|
||||
orgChartContainer.innerHTML = '<div class="loading">正在加载组织架构...</div>';
|
||||
try {
|
||||
const response = await fetchJSON(`${API_BASE}/admin/service-departments/tree`);
|
||||
const tree = (response.data && response.data.tree) || [];
|
||||
orgChartData.tree = tree;
|
||||
orgChartData.parentMap = {};
|
||||
orgChartData.nodeMap = {};
|
||||
orgChartData.allNodes = flattenTree(tree);
|
||||
orgChartLoaded = true;
|
||||
updateOrgStats(tree);
|
||||
if (!tree.length) {
|
||||
orgChartContainer.innerHTML = '<div class="loading">暂无组织架构数据</div>';
|
||||
return;
|
||||
}
|
||||
renderOrgTree(tree);
|
||||
} catch (err) {
|
||||
orgChartLoaded = false;
|
||||
orgChartContainer.innerHTML = `<div class="loading" style="color:#b91c1c;">加载组织架构失败:${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function flattenTree(nodes, level = 0, result = [], parentId = null) {
|
||||
nodes.forEach(node => {
|
||||
if (node.id === undefined || node.id === null) {
|
||||
return;
|
||||
}
|
||||
const nodeId = String(node.id);
|
||||
const flatNode = {
|
||||
id: nodeId,
|
||||
name: node.name || '未知部门',
|
||||
code: node.code || '',
|
||||
region_name: node.region_name || '',
|
||||
level,
|
||||
node
|
||||
};
|
||||
orgChartData.parentMap[nodeId] = parentId;
|
||||
orgChartData.nodeMap[nodeId] = flatNode;
|
||||
result.push(flatNode);
|
||||
if (node.children && node.children.length) {
|
||||
flattenTree(node.children, level + 1, result, nodeId);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function updateOrgStats(tree) {
|
||||
if (totalDeptsEl) {
|
||||
totalDeptsEl.textContent = orgChartData.allNodes.length;
|
||||
}
|
||||
if (totalLevelsEl) {
|
||||
totalLevelsEl.textContent = tree.length ? getMaxLevel(tree) + 1 : 0;
|
||||
}
|
||||
if (rootDeptsEl) {
|
||||
rootDeptsEl.textContent = tree.length;
|
||||
}
|
||||
}
|
||||
|
||||
function getMaxLevel(nodes, level = 0) {
|
||||
let maxLevel = level;
|
||||
nodes.forEach(node => {
|
||||
if (node.children && node.children.length) {
|
||||
const childMax = getMaxLevel(node.children, level + 1);
|
||||
maxLevel = Math.max(maxLevel, childMax);
|
||||
}
|
||||
});
|
||||
return maxLevel;
|
||||
}
|
||||
|
||||
function renderOrgTree(tree) {
|
||||
if (!orgChartContainer) return;
|
||||
orgChartContainer.innerHTML = '';
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'org-tree';
|
||||
orgChartContainer.appendChild(wrapper);
|
||||
|
||||
const rootLevel = document.createElement('div');
|
||||
rootLevel.className = 'tree-level root-level';
|
||||
wrapper.appendChild(rootLevel);
|
||||
|
||||
tree.forEach(node => {
|
||||
rootLevel.appendChild(renderTreeNode(node, true));
|
||||
});
|
||||
}
|
||||
|
||||
function renderTreeNode(node, isRoot = false) {
|
||||
const nodeDiv = document.createElement('div');
|
||||
nodeDiv.className = isRoot ? 'tree-node root-node' : 'tree-node';
|
||||
nodeDiv.setAttribute('data-node-id', node.id != null ? String(node.id) : '');
|
||||
|
||||
const nodeBox = document.createElement('div');
|
||||
nodeBox.className = 'tree-node-box';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'node-name';
|
||||
nameSpan.textContent = node.name || '未知部门';
|
||||
nodeBox.appendChild(nameSpan);
|
||||
|
||||
if (node.code) {
|
||||
const codeSpan = document.createElement('span');
|
||||
codeSpan.className = 'node-code';
|
||||
codeSpan.textContent = node.code;
|
||||
nodeBox.appendChild(codeSpan);
|
||||
}
|
||||
|
||||
if (node.region_name) {
|
||||
const regionSpan = document.createElement('span');
|
||||
regionSpan.className = 'node-region';
|
||||
regionSpan.textContent = node.region_name;
|
||||
nodeBox.appendChild(regionSpan);
|
||||
}
|
||||
|
||||
addTooltip(nodeBox, node);
|
||||
nodeDiv.appendChild(nodeBox);
|
||||
|
||||
if (node.children && node.children.length) {
|
||||
nodeDiv.classList.add('has-children');
|
||||
const childrenWrapper = document.createElement('div');
|
||||
childrenWrapper.className = 'tree-children';
|
||||
const nextLevel = document.createElement('div');
|
||||
nextLevel.className = 'tree-level';
|
||||
if (node.children.length === 1) {
|
||||
nextLevel.classList.add('single-child');
|
||||
}
|
||||
|
||||
node.children.forEach(child => {
|
||||
const childHolder = document.createElement('div');
|
||||
childHolder.className = 'node-children';
|
||||
childHolder.appendChild(renderTreeNode(child, false));
|
||||
nextLevel.appendChild(childHolder);
|
||||
});
|
||||
|
||||
childrenWrapper.appendChild(nextLevel);
|
||||
nodeDiv.appendChild(childrenWrapper);
|
||||
}
|
||||
|
||||
return nodeDiv;
|
||||
}
|
||||
|
||||
function addTooltip(element, node) {
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'tooltip';
|
||||
tooltip.innerHTML = `
|
||||
<div style="font-weight:600;margin-bottom:4px;">${node.name || '未知部门'}</div>
|
||||
${node.code ? `<div>编码: ${node.code}</div>` : ''}
|
||||
${node.region_name ? `<div>区域: ${node.region_name}</div>` : ''}
|
||||
${node.description ? `<div style="margin-top:4px;color:#d1d5db;">${node.description}</div>` : ''}
|
||||
`;
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
element.addEventListener('mouseenter', () => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
tooltip.style.left = `${rect.left + rect.width / 2}px`;
|
||||
tooltip.style.top = `${rect.top - 8}px`;
|
||||
tooltip.style.transform = 'translate(-50%, -100%)';
|
||||
tooltip.classList.add('show');
|
||||
});
|
||||
|
||||
element.addEventListener('mouseleave', () => {
|
||||
tooltip.classList.remove('show');
|
||||
});
|
||||
}
|
||||
|
||||
function searchOrgChart(query) {
|
||||
if (!orgChartData.allNodes.length) {
|
||||
return;
|
||||
}
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) {
|
||||
resetOrgSearchView(false);
|
||||
return;
|
||||
}
|
||||
resetOrgSearchView(false);
|
||||
const searchTerm = trimmed.toLowerCase();
|
||||
const matchedNodes = new Set();
|
||||
|
||||
orgChartData.allNodes.forEach(node => {
|
||||
const nameMatch = node.name.toLowerCase().includes(searchTerm);
|
||||
const codeMatch = node.code.toLowerCase().includes(searchTerm);
|
||||
const regionMatch = node.region_name.toLowerCase().includes(searchTerm);
|
||||
if (nameMatch || codeMatch || regionMatch) {
|
||||
matchedNodes.add(node.id);
|
||||
includeAncestorNodes(node.id, matchedNodes);
|
||||
includeDescendantNodes(node.node, matchedNodes);
|
||||
if (nameMatch) {
|
||||
highlightText(trimmed, node.id, 'name');
|
||||
}
|
||||
if (codeMatch) {
|
||||
highlightText(trimmed, node.id, 'code');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.org-tree .tree-node').forEach(nodeDiv => {
|
||||
const nodeId = nodeDiv.getAttribute('data-node-id');
|
||||
if (nodeId && matchedNodes.has(nodeId)) {
|
||||
nodeDiv.classList.remove('hidden');
|
||||
} else {
|
||||
nodeDiv.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
toggleNoResultsOverlay(matchedNodes.size === 0);
|
||||
}
|
||||
|
||||
function highlightText(searchTerm, nodeId, field) {
|
||||
const nodeDiv = document.querySelector(`.tree-node[data-node-id="${nodeId}"]`);
|
||||
if (!nodeDiv) return;
|
||||
const element = nodeDiv.querySelector(field === 'name' ? '.node-name' : '.node-code');
|
||||
if (!element) return;
|
||||
const nodeData = orgChartData.nodeMap[nodeId];
|
||||
if (!nodeData) return;
|
||||
const originalText = field === 'name' ? nodeData.name : nodeData.code;
|
||||
if (!originalText) return;
|
||||
const regex = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');
|
||||
element.innerHTML = originalText.replace(regex, '<span class="highlight">$1</span>');
|
||||
}
|
||||
|
||||
function includeAncestorNodes(nodeId, matchedNodes) {
|
||||
let parentId = orgChartData.parentMap[nodeId];
|
||||
while (parentId) {
|
||||
matchedNodes.add(parentId);
|
||||
parentId = orgChartData.parentMap[parentId];
|
||||
}
|
||||
}
|
||||
|
||||
function includeDescendantNodes(nodeData, matchedNodes) {
|
||||
if (!nodeData || !nodeData.children) return;
|
||||
nodeData.children.forEach(child => {
|
||||
if (child.id === undefined || child.id === null) return;
|
||||
const childId = String(child.id);
|
||||
matchedNodes.add(childId);
|
||||
includeDescendantNodes(child, matchedNodes);
|
||||
});
|
||||
}
|
||||
|
||||
function resetOrgSearchView(clearInput = false) {
|
||||
document.querySelectorAll('.org-tree .tree-node').forEach(nodeDiv => {
|
||||
nodeDiv.classList.remove('hidden');
|
||||
const nodeId = nodeDiv.getAttribute('data-node-id');
|
||||
const nodeData = orgChartData.nodeMap[nodeId];
|
||||
if (!nodeData) return;
|
||||
const nameEl = nodeDiv.querySelector('.node-name');
|
||||
const codeEl = nodeDiv.querySelector('.node-code');
|
||||
if (nameEl) nameEl.textContent = nodeData.name;
|
||||
if (codeEl) codeEl.textContent = nodeData.code;
|
||||
});
|
||||
if (clearInput) {
|
||||
const input = document.getElementById('orgSearchInput');
|
||||
if (input) input.value = '';
|
||||
}
|
||||
toggleNoResultsOverlay(false);
|
||||
}
|
||||
|
||||
function toggleNoResultsOverlay(show) {
|
||||
if (!orgChartContainer) return;
|
||||
const treeRoot = orgChartContainer.querySelector('.org-tree');
|
||||
if (!treeRoot) return;
|
||||
let overlay = treeRoot.querySelector('.no-results-overlay');
|
||||
if (show) {
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'no-results-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div style="font-size:40px;margin-bottom:8px;">🔍</div>
|
||||
<p style="margin:0 0 6px;">未找到匹配的组织部门</p>
|
||||
<p style="margin:0;color:#6b7280;font-size:13px;">尝试其他关键字或点击下方按钮</p>
|
||||
`;
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'overlay-close-btn';
|
||||
closeBtn.textContent = '返回完整组织架构';
|
||||
closeBtn.addEventListener('click', () => {
|
||||
resetOrgSearchView(true);
|
||||
});
|
||||
overlay.appendChild(closeBtn);
|
||||
treeRoot.appendChild(overlay);
|
||||
}
|
||||
} else if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function initOrgSearch() {
|
||||
const searchInput = document.getElementById('orgSearchInput');
|
||||
const searchBtn = document.getElementById('orgSearchBtn');
|
||||
const resetBtn = document.getElementById('orgResetBtn');
|
||||
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', () => {
|
||||
searchOrgChart(searchInput ? searchInput.value : '');
|
||||
});
|
||||
}
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
resetOrgSearchView(true);
|
||||
});
|
||||
}
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keypress', (evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
searchOrgChart(searchInput.value);
|
||||
}
|
||||
});
|
||||
let timer;
|
||||
searchInput.addEventListener('input', (evt) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
searchOrgChart(evt.target.value);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initTabs();
|
||||
initOrgSearch();
|
||||
|
||||
document.getElementById('templateForm').addEventListener('submit', async (evt) => {
|
||||
evt.preventDefault();
|
||||
const form = evt.target;
|
||||
|
|
|
|||
Loading…
Reference in New Issue