团队-思维图-全图-族谱图.原生html js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>组织结构图 - 连接线优化</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
color: #333;
padding: 20px;
line-height: 1.6;
min-height: 100vh;
}
.container {
max-width: 100%;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 25px;
background-color: white;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
}
h1 {
color: #2c3e50;
margin-bottom: 10px;
font-size: 2.2rem;
position: relative;
display: inline-block;
}
h1:after {
content: '';
position: absolute;
width: 60px;
height: 4px;
background: linear-gradient(90deg, #3498db, #2ecc71);
bottom: -10px;
left: 50%;
transform: translateX(-50%);
border-radius: 2px;
}
.description {
color: #7f8c8d;
max-width: 800px;
margin: 20px auto 0;
font-size: 1.05rem;
line-height: 1.7;
}
.controls {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 25px;
padding: 20px;
background-color: white;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.btn {
background: linear-gradient(to right, #3498db, #2980b9);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11);
}
.btn:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-expand-all {
background: linear-gradient(to right, #2ecc71, #27ae60);
}
.btn-collapse-all {
background: linear-gradient(to right, #e74c3c, #c0392b);
}
.btn-loading {
background: linear-gradient(to right, #95a5a6, #7f8c8d);
}
.btn-loading::after {
content: '';
width: 16px;
height: 16px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.org-chart-container {
position: relative;
padding: 30px 20px;
background-color: white;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
min-height: 600px;
overflow: hidden;
margin-bottom: 30px;
}
.org-chart-wrapper {
position: relative;
min-width: 100%;
min-height: 100%;
overflow-x: auto;
overflow-y: auto;
}
.org-chart {
display: flex;
flex-direction: column;
align-items: flex-start;
position: relative;
min-width: fit-content;
padding: 40px 20px;
}
.level {
display: flex;
justify-content: center;
width: 100%;
margin-bottom: 100px;
position: relative;
min-width: fit-content;
}
.level:last-child {
margin-bottom: 0;
}
.level-label {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
background-color: #f8f9fa;
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
color: #7f8c8d;
border: 1px solid #e1e4e8;
z-index: 1;
white-space: nowrap;
}
.node-container {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.node-wrapper {
margin: 0 20px;
position: relative;
}
.node {
background-color: white;
border-radius: 10px;
padding: 18px;
width: 280px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
cursor: pointer;
position: relative;
z-index: 2;
border: 1px solid #e1e4e8;
}
.node:hover {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.12);
transform: translateY(-5px);
border-color: #3498db;
}
.node-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
padding-bottom: 12px;
border-bottom: 2px solid #f1f5f9;
}
.node-title {
font-weight: 700;
font-size: 16px;
color: #2c3e50;
display: flex;
flex-direction: column;
gap: 4px;
}
.node-name {
font-size: 14px;
color: #7f8c8d;
font-weight: 500;
}
.toggle-btn {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #3498db, #2ecc71);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
color: white;
transition: all 0.2s ease;
box-shadow: 0 3px 6px rgba(52, 152, 219, 0.3);
flex-shrink: 0;
margin-top: 4px;
}
.toggle-btn:hover {
transform: scale(1.1);
}
.toggle-btn.loading {
background: #95a5a6;
}
.toggle-btn.loading::after {
content: '';
width: 12px;
height: 12px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
.toggle-btn.loading span {
display: none;
}
.node-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
font-size: 13px;
}
.detail-item {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
.detail-label {
color: #7f8c8d;
font-weight: 500;
}
.detail-value {
color: #2c3e50;
font-weight: 600;
}
.level-1 .node { border-left: 5px solid #e74c3c; }
.level-2 .node { border-left: 5px solid #e67e22; }
.level-3 .node { border-left: 5px solid #f39c12; }
.level-4 .node { border-left: 5px solid #3498db; }
.level-5 .node { border-left: 5px solid #2ecc71; }
.level-6 .node { border-left: 5px solid #1abc9c; }
.level-7 .node { border-left: 5px solid #9b59b6; }
.level-8 .node { border-left: 5px solid #34495e; }
.level-9 .node { border-left: 5px solid #e67e22; }
.level-10 .node { border-left: 5px solid #f39c12; }
.level-1 .detail-value { color: #e74c3c; }
.level-2 .detail-value { color: #e67e22; }
.level-3 .detail-value { color: #f39c12; }
.level-4 .detail-value { color: #3498db; }
.level-5 .detail-value { color: #2ecc71; }
.level-6 .detail-value { color: #1abc9c; }
.level-7 .detail-value { color: #9b59b6; }
.level-8 .detail-value { color: #34495e; }
.level-9 .detail-value { color: #e67e22; }
.level-10 .detail-value { color: #f39c12; }
.level-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
color: white;
margin-top: 4px;
align-self: flex-start;
}
.level-1 .level-badge { background-color: #e74c3c; }
.level-2 .level-badge { background-color: #e67e22; }
.level-3 .level-badge { background-color: #f39c12; }
.level-4 .level-badge { background-color: #3498db; }
.level-5 .level-badge { background-color: #2ecc71; }
.level-6 .level-badge { background-color: #1abc9c; }
.level-7 .level-badge { background-color: #9b59b6; }
.level-8 .level-badge { background-color: #34495e; }
.level-9 .level-badge { background-color: #e67e22; }
.level-10 .level-badge { background-color: #f39c12; }
.stats {
margin-top: 30px;
padding: 25px;
background-color: white;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
}
.stats h3 {
margin-bottom: 20px;
color: #2c3e50;
font-size: 1.5rem;
display: flex;
align-items: center;
gap: 10px;
}
.stats h3:before {
content: '';
width: 8px;
height: 25px;
background: linear-gradient(to bottom, #3498db, #2ecc71);
border-radius: 4px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 10px;
border-left: 5px solid #3498db;
transition: all 0.3s ease;
}
.stat-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.stat-label {
color: #7f8c8d;
font-weight: 600;
font-size: 15px;
}
.stat-value {
color: #2c3e50;
font-weight: 800;
font-size: 22px;
}
.empty-level {
font-style: italic;
color: #bdc3c7;
padding: 20px;
text-align: center;
background-color: #f8f9fa;
border-radius: 8px;
border: 2px dashed #e1e4e8;
min-width: 200px;
}
.performance-bar {
height: 6px;
background-color: #ecf0f1;
border-radius: 3px;
margin-top: 8px;
overflow: hidden;
}
.performance-fill {
height: 100%;
border-radius: 3px;
}
.level-1 .performance-fill { background-color: #e74c3c; }
.level-2 .performance-fill { background-color: #e67e22; }
.level-3 .performance-fill { background-color: #f39c12; }
.level-4 .performance-fill { background-color: #3498db; }
.level-5 .performance-fill { background-color: #2ecc71; }
.level-6 .performance-fill { background-color: #1abc9c; }
.level-7 .performance-fill { background-color: #9b59b6; }
.level-8 .performance-fill { background-color: #34495e; }
.level-9 .performance-fill { background-color: #e67e22; }
.level-10 .performance-fill { background-color: #f39c12; }
.svg-connectors {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.connector-line {
stroke-width: 2;
fill: none;
}
.node-content-wrapper {
position: relative;
z-index: 3;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
border-radius: 12px;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 8px;
background-color: #2ecc71;
color: white;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
transform: translateX(150%);
transition: transform 0.3s ease;
z-index: 10000;
}
.notification.show {
transform: translateX(0);
}
.notification.error {
background-color: #e74c3c;
}
.notification.warning {
background-color: #f39c12;
}
/* 水平滚动提示 */
.scroll-hint {
position: absolute;
right: 20px;
top: 20px;
display: flex;
align-items: center;
gap: 8px;
color: #7f8c8d;
font-size: 14px;
background: rgba(255, 255, 255, 0.9);
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #e1e4e8;
}
.scroll-hint-icon {
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateX(0);
}
40% {
transform: translateX(-5px);
}
60% {
transform: translateX(-3px);
}
}
@media (max-width: 1200px) {
.node {
width: 250px;
}
.level {
margin-bottom: 80px;
}
.node-wrapper {
margin: 0 15px;
}
}
@media (max-width: 992px) {
.node {
width: 220px;
padding: 15px;
}
.node-details {
grid-template-columns: 1fr;
gap: 6px;
}
.level {
margin-bottom: 60px;
}
}
@media (max-width: 768px) {
.level {
flex-direction: column;
align-items: center;
margin-bottom: 40px;
}
.node-wrapper {
margin: 10px 0;
}
.level-label {
position: static;
transform: none;
margin-bottom: 10px;
align-self: flex-start;
}
.org-chart-wrapper {
overflow-x: scroll;
overflow-y: visible;
}
.org-chart {
padding: 30px 15px;
}
}
@media (max-width: 576px) {
.container {
padding: 10px;
}
header {
padding: 20px 15px;
}
.controls {
padding: 15px;
}
.btn {
padding: 10px 20px;
font-size: 14px;
}
.node {
width: 200px;
padding: 12px;
}
.node-title {
font-size: 15px;
}
.node-name {
font-size: 13px;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>组织结构图 - 连接线优化</h1>
<p class="description">修复连接线显示问题,确保所有连接线完整显示,优化节点布局和滚动体验。</p>
</header>
<div class="controls">
<button class="btn btn-expand-all" id="expandAll">
<span>展开全部</span>
</button>
<button class="btn btn-collapse-all" id="collapseAll">
<span>折叠全部</span>
</button>
<button class="btn" id="refreshData">
<span>刷新数据</span>
</button>
</div>
<div class="org-chart-container">
<div id="loadingOverlay" class="loading-overlay" style="display: none;">
<div class="loading-spinner"></div>
</div>
<div class="scroll-hint" id="scrollHint" style="display: none;">
<span class="scroll-hint-icon">←→</span>
<span>可左右滚动查看更多节点</span>
</div>
<div class="org-chart-wrapper" id="orgChartWrapper">
<svg class="svg-connectors" id="svgConnectors"></svg>
<div class="org-chart" id="orgChart">
<!-- 组织结构图将在这里动态生成 -->
</div>
</div>
</div>
<div class="stats">
<h3>组织统计</h3>
<div class="stats-grid" id="statsGrid">
<!-- 统计信息将在这里动态生成 -->
</div>
</div>
</div>
<!-- 通知弹窗 -->
<div id="notification" class="notification"></div>
<script>
// 配置
const API_URL = 'http://abc'; // 接口地址
const TOKEN = '123'; // 认证token
const DEFAULT_PID = 0; // 根节点pid
// 存储组织结构数据
let orgData = {
id: null, // 虚拟根节点
pid: DEFAULT_PID,
nickname: "虚拟根节点",
level_name: "根级",
maxq: 0,
minq: 0,
team_perf: 0,
num: 0,
bind_contact: "",
children: [],
expanded: true,
loaded: false,
level: 0
};
// 节点ID映射,用于快速查找节点
const nodeMap = new Map();
// 记录每个节点的实际层级
const nodeLevels = new Map();
// 格式化数字,添加千位分隔符
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 显示通知
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type}`;
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// 显示/隐藏加载遮罩
function toggleLoading(show) {
const loadingOverlay = document.getElementById('loadingOverlay');
loadingOverlay.style.display = show ? 'flex' : 'none';
}
// 显示/隐藏滚动提示
function toggleScrollHint(show) {
const scrollHint = document.getElementById('scrollHint');
scrollHint.style.display = show ? 'flex' : 'none';
}
// 从接口获取数据
async function fetchOrgData(pid = DEFAULT_PID) {
try {
toggleLoading(true);
// 模拟API调用 - 实际使用时取消注释
/*
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `pid=${pid}&token=${TOKEN}`
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.code !== 1) {
throw new Error(data.msg || '获取数据失败');
}
return data.data || [];
*/
// 模拟数据 - 实际使用时删除
return simulateApiResponse(pid);
} catch (error) {
console.error('获取组织数据失败:', error);
showNotification(`获取数据失败: ${error.message}`, 'error');
return [];
} finally {
toggleLoading(false);
}
}
// 模拟API响应
function simulateApiResponse(pid) {
return new Promise(resolve => {
setTimeout(() => {
if (pid === 0) {
// 根节点下的第一级
resolve([
{
id: 1,
bind_contact: "13800138000",
level_name: "董事长",
maxq: 3850000,
minq: 1250000,
nickname: "董事长",
num: 5,
team_perf: 1250000
},
{
id: 2,
bind_contact: "13800138001",
level_name: "董事",
maxq: 2850000,
minq: 850000,
nickname: "董事A",
num: 3,
team_perf: 850000
},
{
id: 20,
bind_contact: "13800138020",
level_name: "董事",
maxq: 2200000,
minq: 750000,
nickname: "董事B",
num: 2,
team_perf: 750000
},
{
id: 21,
bind_contact: "13800138021",
level_name: "董事",
maxq: 1800000,
minq: 650000,
nickname: "董事C",
num: 1,
team_perf: 650000
},
{
id: 22,
bind_contact: "13800138022",
level_name: "董事",
maxq: 1500000,
minq: 550000,
nickname: "董事D",
num: 0,
team_perf: 550000
}
]);
} else if (pid === 1) {
// 董事长的下级
resolve([
{
id: 3,
bind_contact: "13800138002",
level_name: "北区总监",
maxq: 2100000,
minq: 850000,
nickname: "北区总监",
num: 3,
team_perf: 850000
},
{
id: 4,
bind_contact: "13800138003",
level_name: "南区总监",
maxq: 1750000,
minq: 920000,
nickname: "南区总监",
num: 2,
team_perf: 920000
},
{
id: 23,
bind_contact: "13800138023",
level_name: "东区总监",
maxq: 1900000,
minq: 780000,
nickname: "东区总监",
num: 2,
team_perf: 780000
},
{
id: 24,
bind_contact: "13800138024",
level_name: "西区总监",
maxq: 1600000,
minq: 680000,
nickname: "西区总监",
num: 2,
team_perf: 680000
},
{
id: 25,
bind_contact: "13800138025",
level_name: "中区总监",
maxq: 1400000,
minq: 580000,
nickname: "中区总监",
num: 1,
team_perf: 580000
}
]);
} else if (pid === 2) {
// 董事A的下级
resolve([
{
id: 5,
bind_contact: "13800138004",
level_name: "西区总监",
maxq: 1500000,
minq: 650000,
nickname: "西区总监",
num: 2,
team_perf: 650000
},
{
id: 26,
bind_contact: "13800138026",
level_name: "西北区总监",
maxq: 1200000,
minq: 520000,
nickname: "西北区总监",
num: 1,
team_perf: 520000
},
{
id: 27,
bind_contact: "13800138027",
level_name: "西南区总监",
maxq: 1100000,
minq: 480000,
nickname: "西南区总监",
num: 0,
team_perf: 480000
}
]);
} else if (pid === 3) {
// 北区总监的下级
resolve([
{
id: 6,
bind_contact: "13800138005",
level_name: "北区经理A",
maxq: 850000,
minq: 320000,
nickname: "北区经理A",
num: 1,
team_perf: 320000
},
{
id: 7,
bind_contact: "13800138006",
level_name: "北区经理B",
maxq: 1250000,
minq: 530000,
nickname: "北区经理B",
num: 1,
team_perf: 530000
},
{
id: 28,
bind_contact: "13800138028",
level_name: "北区经理C",
maxq: 950000,
minq: 420000,
nickname: "北区经理C",
num: 1,
team_perf: 420000
},
{
id: 29,
bind_contact: "13800138029",
level_name: "北区经理D",
maxq: 780000,
minq: 380000,
nickname: "北区经理D",
num: 0,
team_perf: 380000
}
]);
} else {
// 默认返回空数组
resolve([]);
}
}, 500); // 模拟网络延迟
});
}
// 将API数据转换为节点格式
function apiDataToNode(apiData, pid, level = 1) {
return {
id: apiData.id,
pid: pid,
bind_contact: apiData.bind_contact,
level_name: apiData.level_name,
maxq: apiData.maxq || 0,
minq: apiData.minq || 0,
nickname: apiData.nickname,
num: apiData.num || 0,
team_perf: apiData.team_perf || 0,
children: [],
expanded: false,
loaded: false,
hasChildren: apiData.num > 0,
level: level
};
}
// 获取或创建节点
async function getOrLoadNode(pid, forceRefresh = false) {
// 如果是根节点
if (pid === DEFAULT_PID) {
if (!orgData.loaded || forceRefresh) {
const data = await fetchOrgData(DEFAULT_PID);
orgData.children = data.map(item => apiDataToNode(item, DEFAULT_PID, 1));
orgData.loaded = true;
orgData.expanded = true;
}
return orgData;
}
// 查找节点
let targetNode = findNodeById(pid);
if (!targetNode) {
console.warn(`未找到ID为${pid}的节点`);
return null;
}
// 如果强制刷新或未加载过,则重新加载
if (forceRefresh || !targetNode.loaded) {
const data = await fetchOrgData(pid);
targetNode.children = data.map(item => apiDataToNode(item, pid, targetNode.level + 1));
targetNode.loaded = true;
}
return targetNode;
}
// 查找节点
function findNodeById(id) {
return nodeMap.get(id);
}
// 计算节点的实际层级
function calculateNodeLevels(node, level = 0) {
nodeLevels.set(node.id, level);
node.level = level;
if (node.children && node.expanded) {
node.children.forEach(child => {
calculateNodeLevels(child, level + 1);
});
}
}
// 获取树的最大深度
function getTreeDepth(node) {
calculateNodeLevels(node, 0);
let maxDepth = 0;
nodeLevels.forEach(level => {
if (level > maxDepth) {
maxDepth = level;
}
});
return maxDepth;
}
// 按层级组织节点
function organizeNodesByLevel() {
const levels = {};
// 从虚拟根节点的子节点开始
function addNodeToLevel(node) {
const level = nodeLevels.get(node.id);
if (level === undefined) return;
if (!levels[level]) {
levels[level] = [];
}
// 检查是否已经添加过这个节点
if (!levels[level].some(n => n.id === node.id)) {
levels[level].push(node);
}
// 如果节点展开且有子节点,添加子节点
if (node.expanded && node.children && node.children.length > 0) {
node.children.forEach(child => {
addNodeToLevel(child);
});
}
}
// 从根节点的子节点开始
orgData.children.forEach(child => {
addNodeToLevel(child);
});
return levels;
}
// 创建节点元素
function createNodeElement(node) {
const nodeElement = document.createElement('div');
nodeElement.className = 'node';
nodeElement.setAttribute('data-id', node.id);
nodeElement.setAttribute('data-level', node.level);
const hasChildren = node.hasChildren;
const levelClass = `level-${Math.min(node.level, 10)}`;
// 计算业绩百分比
const maxPerformance = Math.max(node.maxq, 1000000);
const realtimePercent = Math.min((node.team_perf / maxPerformance) * 100, 100);
nodeElement.innerHTML = `
<div class="node-content-wrapper">
<div class="node-header">
<div class="node-title">
<span>${node.nickname}</span>
<span class="node-name">${node.bind_contact || '无手机号'}</span>
<span class="level-badge">${node.level_name || '未知级别'}</span>
</div>
${hasChildren ?
`<div class="toggle-btn ${node.loading ? 'loading' : ''}">
<span>${node.expanded ? '▼' : '▶'}</span>
</div>` :
`<div class="toggle-btn" style="opacity: 0.5; cursor: default;">•</div>`
}
</div>
<div class="node-details">
<div class="detail-item">
<span class="detail-label">直推人数:</span>
<span class="detail-value">${node.num || 0}人</span>
</div>
<div class="detail-item">
<span class="detail-label">新增业绩:</span>
<span class="detail-value">¥${formatNumber(node.team_perf || 0)}</span>
</div>
<div class="performance-bar">
<div class="performance-fill ${levelClass}" style="width: ${realtimePercent}%"></div>
</div>
<div class="detail-item">
<span class="detail-label">大区业绩:</span>
<span class="detail-value">¥${formatNumber(node.maxq || 0)}</span>
</div>
<div class="detail-item">
<span class="detail-label">小区业绩:</span>
<span class="detail-value">¥${formatNumber(node.minq || 0)}</span>
</div>
</div>
</div>
`;
nodeElement.classList.add(levelClass);
return nodeElement;
}
// 优化连接线渲染
function renderConnectors() {
const svg = document.getElementById('svgConnectors');
svg.innerHTML = '';
// 获取容器和包装器的位置
const container = document.querySelector('.org-chart-container');
const wrapper = document.querySelector('.org-chart-wrapper');
if (!container || !wrapper) return;
const containerRect = container.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
// 计算滚动偏移
const scrollLeft = wrapper.scrollLeft;
const scrollTop = wrapper.scrollTop;
// 更新SVG的尺寸以匹配内容
const orgChart = document.getElementById('orgChart');
if (orgChart) {
svg.style.width = orgChart.scrollWidth + 'px';
svg.style.height = orgChart.scrollHeight + 'px';
}
// 收集所有需要绘制连接线的父节点
const parentNodes = [];
nodeMap.forEach((node, nodeId) => {
if (node.children && node.children.length > 0 && node.expanded) {
parentNodes.push(node);
}
});
// 为每个父节点绘制连接线
parentNodes.forEach(parentNode => {
const parentElement = document.querySelector(`.node[data-id="${parentNode.id}"]`);
if (!parentElement) return;
const parentRect = parentElement.getBoundingClientRect();
// 计算父节点的中心点(相对于SVG)
const parentX = parentRect.left - containerRect.left + scrollLeft + parentRect.width / 2;
const parentY = parentRect.top - containerRect.top + scrollTop + parentRect.height;
// 获取所有可见的子节点
const visibleChildren = parentNode.children.filter(child => {
const childElement = document.querySelector(`.node[data-id="${child.id}"]`);
return childElement && childElement.offsetParent !== null;
});
if (visibleChildren.length === 0) return;
// 为每个子节点绘制连接线
visibleChildren.forEach(child => {
const childElement = document.querySelector(`.node[data-id="${child.id}"]`);
if (!childElement) return;
const childRect = childElement.getBoundingClientRect();
// 计算子节点的中心点(相对于SVG)
const childX = childRect.left - containerRect.left + scrollLeft + childRect.width / 2;
const childY = childRect.top - containerRect.top + scrollTop;
// 计算中间点
const midY = parentY + (childY - parentY) / 2;
// 创建连接线路径
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
// 使用贝塞尔曲线创建更平滑的连接线
const d = `M ${parentX} ${parentY}
C ${parentX} ${midY}, ${childX} ${midY}, ${childX} ${childY}`;
path.setAttribute('d', d);
path.setAttribute('class', 'connector-line');
// 设置线条样式
const level = parentNode.level || 1;
const lineColor = getLevelColor(level);
path.setAttribute('stroke', lineColor);
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
svg.appendChild(path);
// 在连接线起点添加小圆点
const startDot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
startDot.setAttribute('cx', parentX);
startDot.setAttribute('cy', parentY);
startDot.setAttribute('r', '3');
startDot.setAttribute('fill', lineColor);
svg.appendChild(startDot);
// 在连接线终点添加小圆点
const endDot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
endDot.setAttribute('cx', childX);
endDot.setAttribute('cy', childY);
endDot.setAttribute('r', '3');
endDot.setAttribute('fill', lineColor);
svg.appendChild(endDot);
});
});
}
// 获取层级颜色
function getLevelColor(level) {
const colors = {
1: '#e74c3c',
2: '#e67e22',
3: '#f39c12',
4: '#3498db',
5: '#2ecc71',
6: '#1abc9c',
7: '#9b59b6',
8: '#34495e',
9: '#e67e22',
10: '#f39c12'
};
return colors[Math.min(level, 10)] || '#d1d8e0';
}
// 检查是否需要显示水平滚动提示
function checkScrollHint() {
const orgChart = document.getElementById('orgChart');
const orgChartWrapper = document.getElementById('orgChartWrapper');
if (orgChart && orgChartWrapper) {
const chartWidth = orgChart.scrollWidth;
const wrapperWidth = orgChartWrapper.clientWidth;
// 如果内容宽度大于容器宽度,显示滚动提示
toggleScrollHint(chartWidth > wrapperWidth + 50);
}
}
// 渲染组织图
async function renderOrgChart() {
const orgChart = document.getElementById('orgChart');
orgChart.innerHTML = '';
// 清除节点映射
nodeMap.clear();
nodeLevels.clear();
// 计算节点层级
calculateNodeLevels(orgData);
// 获取按层级组织的节点
const levels = organizeNodesByLevel();
const maxDepth = getTreeDepth(orgData);
// 为每个层级创建容器
for (let level = 1; level <= maxDepth; level++) {
const levelContainer = document.createElement('div');
levelContainer.className = `level level-${Math.min(level, 10)}`;
levelContainer.setAttribute('data-level', level);
levelContainer.id = `level-${level}`;
// 添加层级标签
const levelLabel = document.createElement('div');
levelLabel.className = 'level-label';
const levelNames = ["", "一级", "二级", "三级", "四级", "五级", "六级", "七级", "八级", "九级", "十级"];
levelLabel.textContent = levelNames[Math.min(level, 10)] || `层级 ${level}`;
levelContainer.appendChild(levelLabel);
// 获取该层级的所有节点
const levelNodes = levels[level] || [];
if (levelNodes.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.className = 'empty-level';
emptyMsg.textContent = '该层级暂无成员';
levelContainer.appendChild(emptyMsg);
} else {
// 创建节点容器
const nodesContainer = document.createElement('div');
nodesContainer.className = 'level-nodes';
nodesContainer.style.display = 'flex';
nodesContainer.style.justifyContent = 'flex-start';
nodesContainer.style.flexWrap = 'nowrap';
nodesContainer.style.minWidth = 'fit-content';
nodesContainer.id = `level-nodes-${level}`;
// 添加该层级的所有节点
levelNodes.forEach(node => {
// 更新节点映射
nodeMap.set(node.id, node);
const nodeWrapper = document.createElement('div');
nodeWrapper.className = 'node-wrapper';
nodeWrapper.id = `node-wrapper-${node.id}`;
const nodeContainer = document.createElement('div');
nodeContainer.className = 'node-container';
nodeContainer.id = `node-container-${node.id}`;
// 生成节点内容
const nodeElement = createNodeElement(node);
nodeContainer.appendChild(nodeElement);
nodeWrapper.appendChild(nodeContainer);
nodesContainer.appendChild(nodeWrapper);
});
levelContainer.appendChild(nodesContainer);
}
orgChart.appendChild(levelContainer);
}
// 等待DOM更新完成后渲染连接线
setTimeout(() => {
renderConnectors();
// 检查是否需要显示滚动提示
setTimeout(checkScrollHint, 100);
}, 100);
// 添加点击事件
attachNodeEvents();
}
// 为节点添加点击事件
function attachNodeEvents() {
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', async function(e) {
e.stopPropagation();
if (this.classList.contains('loading')) return;
const nodeElement = this.closest('.node');
const nodeId = parseInt(nodeElement.getAttribute('data-id'));
const node = nodeMap.get(nodeId);
if (node && node.hasChildren) {
await toggleNode(node, nodeElement, this);
}
});
});
// 节点点击事件
document.querySelectorAll('.node').forEach(node => {
node.addEventListener('click', async function(e) {
if (e.target.closest('.toggle-btn')) return;
const nodeId = parseInt(this.getAttribute('data-id'));
const node = nodeMap.get(nodeId);
if (node && node.hasChildren) {
const toggleBtn = this.querySelector('.toggle-btn');
if (toggleBtn && !toggleBtn.classList.contains('loading')) {
await toggleNode(node, this, toggleBtn);
}
}
});
});
}
// 切换节点展开/折叠
async function toggleNode(node, nodeElement, toggleBtn) {
// 如果节点未加载,先加载子节点
if (!node.loaded) {
toggleBtn.classList.add('loading');
await getOrLoadNode(node.id);
toggleBtn.classList.remove('loading');
}
// 切换展开状态
node.expanded = !node.expanded;
// 更新按钮图标
const toggleIcon = toggleBtn.querySelector('span');
if (toggleIcon) {
toggleIcon.textContent = node.expanded ? '▼' : '▶';
}
// 重新渲染组织图
await renderOrgChart();
// 显示通知
const action = node.expanded ? '展开' : '折叠';
showNotification(`${node.nickname} 已${action}`, 'success');
}
// 计算组织统计数据
function calculateStats() {
let totalMembers = 0;
let totalTeamPerf = 0;
let totalMaxq = 0;
let totalMinq = 0;
const levelCounts = {};
function traverse(node) {
if (node.id !== null) {
totalMembers++;
totalTeamPerf += node.team_perf || 0;
totalMaxq += node.maxq || 0;
totalMinq += node.minq || 0;
const levelName = node.level_name || '未知';
levelCounts[levelName] = (levelCounts[levelName] || 0) + 1;
}
if (node.children) {
node.children.forEach(child => {
traverse(child);
});
}
}
traverse(orgData);
return { totalMembers, totalTeamPerf, totalMaxq, totalMinq, levelCounts };
}
// 渲染统计数据
function renderStats() {
const stats = calculateStats();
const statsGrid = document.getElementById('statsGrid');
statsGrid.innerHTML = `
<div class="stat-item">
<span class="stat-label">总成员数</span>
<span class="stat-value">${stats.totalMembers}</span>
</div>
<div class="stat-item">
<span class="stat-label">总新增业绩</span>
<span class="stat-value">¥${formatNumber(stats.totalTeamPerf)}</span>
</div>
<div class="stat-item">
<span class="stat-label">总大区业绩</span>
<span class="stat-value">¥${formatNumber(stats.totalMaxq)}</span>
</div>
<div class="stat-item">
<span class="stat-label">总小区业绩</span>
<span class="stat-value">¥${formatNumber(stats.totalMinq)}</span>
</div>
`;
Object.entries(stats.levelCounts).forEach(([level, count]) => {
if (level && level !== '未知') {
const statItem = document.createElement('div');
statItem.className = 'stat-item';
statItem.innerHTML = `
<span class="stat-label">${level}人数</span>
<span class="stat-value">${count}</span>
`;
statsGrid.appendChild(statItem);
}
});
}
// 展开所有节点
async function expandAll() {
document.getElementById('expandAll').disabled = true;
async function expandNode(node) {
if (node.hasChildren && !node.loaded) {
await getOrLoadNode(node.id);
}
node.expanded = true;
if (node.children) {
for (let child of node.children) {
await expandNode(child);
}
}
}
for (let child of orgData.children) {
await expandNode(child);
}
await renderOrgChart();
showNotification('已展开所有节点', 'success');
document.getElementById('expandAll').disabled = false;
}
// 折叠所有节点
async function collapseAll() {
document.getElementById('collapseAll').disabled = true;
function collapseNode(node) {
node.expanded = false;
if (node.children) {
for (let child of node.children) {
collapseNode(child);
}
}
}
for (let child of orgData.children) {
collapseNode(child);
}
await renderOrgChart();
showNotification('已折叠所有节点', 'success');
document.getElementById('collapseAll').disabled = false;
}
// 刷新数据
async function refreshData() {
document.getElementById('refreshData').disabled = true;
await getOrLoadNode(DEFAULT_PID, true);
await renderOrgChart();
renderStats();
showNotification('数据已刷新', 'success');
document.getElementById('refreshData').disabled = false;
}
// 初始化
async function init() {
document.getElementById('expandAll').addEventListener('click', expandAll);
document.getElementById('collapseAll').addEventListener('click', collapseAll);
document.getElementById('refreshData').addEventListener('click', refreshData);
// 窗口大小变化时重新渲染连接线和检查滚动提示
window.addEventListener('resize', () => {
setTimeout(() => {
renderConnectors();
checkScrollHint();
}, 100);
});
// 监听滚动事件,重新渲染连接线
const wrapper = document.getElementById('orgChartWrapper');
if (wrapper) {
wrapper.addEventListener('scroll', () => {
setTimeout(renderConnectors, 50);
});
}
// 初始化加载数据
try {
await getOrLoadNode(DEFAULT_PID);
await renderOrgChart();
renderStats();
showNotification('组织结构图加载完成', 'success');
} catch (error) {
console.error('初始化失败:', error);
showNotification('初始化失败,请检查网络连接', 'error');
}
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>元宝ai实现的效果图

本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 阿牛哥
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果