diff --git a/COLOR_SCHEME_UPDATE.md b/COLOR_SCHEME_UPDATE.md new file mode 100644 index 0000000..4e92a55 --- /dev/null +++ b/COLOR_SCHEME_UPDATE.md @@ -0,0 +1,79 @@ +# 组织架构配色方案更新 - 柔和版 + +## 反馈与调整 + +根据您的反馈"颜色太刺眼了,背景颜色很淡,这个就没必要搞这么深色了",我们对组织架构的颜色方案进行了全面调整,采用更柔和、更淡雅的配色。 + +## 新配色方案 + +### 层级 0 - 根节点 +- **背景**: 淡蓝紫渐变 `#e0e7ff → #f0f4ff` +- **文字**: 深蓝紫色 `#312e81` (不刺眼) +- **边框**: 柔和蓝色 `#818cf8` +- **标签背景**: `#e0e7ff` + +### 层级 1 - 二级节点 +- **背景**: 淡紫渐变 `#ede9fe → #f3e8ff` +- **文字**: 深紫色 `#5b21b6` (温和) +- **边框**: 柔和紫色 `#a78bfa` +- **标签背景**: `#ede9fe` + +### 层级 2+ - 三级节点 +- **背景**: 很浅淡紫 `#f5f3ff → #faf5ff` +- **文字**: 深紫色 `#4c1d95` (非常柔和) +- **边框**: 很浅紫色 `#c4b5fd` +- **标签背景**: `#f5f3ff` + +## 主要改进 + +### ✅ 视觉舒适度提升 +- **之前**: 深色背景 + 白色文字,色彩饱和度高,刺眼 +- **现在**: 浅色背景 + 深色文字,色彩柔和,不刺眼 + +### ✅ 保持层级区分 +- 通过背景色的深浅变化区分层级 +- 保持缩进距离不变 (0px → 20px → 40px) +- 左侧边框颜色渐变,层次清晰 + +### ✅ 对比度优化 +- 深色文字配浅色背景,符合 WCAG 可读性标准 +- 避免长时间使用的视觉疲劳 +- 在浅色背景下文字清晰易读 + +### ✅ 阴影效果柔和 +- 降低阴影不透明度至 0.25 +- 使用对应主题色作为阴影色 +- Hover 效果明显但不突兀 + +## 文件修改 + +**static/super_admin.html** (行 308-562) +- 调整所有层级的背景渐变色 +- 修改文字颜色为深色系 +- 降低边框和阴影强度 +- 优化标签和按钮颜色 + +## 预览效果 + +您可以通过以下方式查看新的柔和配色: +1. 直接访问 `static/super_admin.html` (已更新) +2. 查看独立预览页:`test_org_chart_soft.html` + +## 测试验证 + +已通过自动化测试验证: +- ✅ 根节点配色柔和正确 +- ✅ 5个二级节点配色统一 +- ✅ 6个三级节点配色淡雅 +- ✅ Toggle 按钮交互正常 +- ✅ Hover 效果柔和舒适 + +## 总结 + +新的配色方案在保持清晰层级关系的同时,大幅提升了视觉舒适度: +- **柔和**: 色彩饱和度降低,不刺眼 +- **淡雅**: 整体感觉清新、现代 +- **专业**: 适合政府部门的正式场景 +- **舒适**: 长时间使用不疲劳 + +这个方案更适合日常办公使用,既保持了专业感,又提供了良好的用户体验。 diff --git a/ORG_CHART_VISUAL_IMPROVEMENTS.md b/ORG_CHART_VISUAL_IMPROVEMENTS.md new file mode 100644 index 0000000..a5309e0 --- /dev/null +++ b/ORG_CHART_VISUAL_IMPROVEMENTS.md @@ -0,0 +1,228 @@ +# 组织架构视觉改进方案 + +## 改进概述 + +根据您的反馈,我们对组织架构界面进行了全面的视觉优化,使其在视觉上更好地区分上下级关系。新的设计使用颜色渐变、缩进和视觉层次来清晰地展示组织结构的层级关系。 + +## 主要改进点 + +### 1. 视觉层级分明 - 颜色分级系统 + +**根节点 (Level 0)** +- **背景**: 深蓝紫色渐变 `linear-gradient(135deg, #4338ca 0%, #6d28d9 100%)` +- **文字**: 白色,大字体(18px),粗体 +- **内边距**: 20px (顶部/底部), 24px (左右) +- **边框**: 2px solid #4338ca + 左侧4px强调条 +- **用途**: 代表顶级组织,如"佛山市市场监管局服务部门" + +**二级节点 (Level 1)** +- **背景**: 中等紫色渐变 `linear-gradient(135deg, #7c3aed 0%, #a855f7 100%)` +- **文字**: 白色,较大字体(17px),中等粗细 +- **内边距**: 18px (顶部/底部), 22px (左右) +- **边框**: 2px solid #7c3aed + 左侧4px强调条 +- **左边距**: 20px (明显缩进) +- **用途**: 代表区级部门,如"禅城区服务部门"、"南海区服务部门" + +**三级及以下节点 (Level 2+)** +- **背景**: 浅紫色渐变 `linear-gradient(135deg, #a78bfa 0%, #c4b5fd 100%)` +- **文字**: 深色(#1e1b4b),标准字体(16px),中等粗细 +- **内边距**: 16px (顶部/底部), 20px (左右) +- **边框**: 2px solid #a78bfa + 左侧4px强调条 +- **左边距**: 40px (更明显缩进) +- **用途**: 代表具体执行部门,如各区市场监督管理局 + +### 2. 增强缩进指示 + +| 层级 | 缩进距离 | 视觉效果 | +|------|----------|----------| +| Level 0 (根) | 0px | 紧贴左侧,视觉焦点 | +| Level 1 (二级) | 20px | 清晰缩进,隶属关系 | +| Level 2+ (三级) | 40px | 明显缩进,次级关系 | + +### 3. 左侧边框指示系统 + +每个节点左侧都有一个4px宽的彩色竖条,与节点边框颜色一致: +- 根节点: #4338ca (深蓝紫) +- 二级节点: #7c3aed (中紫) +- 三级节点: #a78bfa (浅紫) + +这形成了垂直的视觉引导线,强化了层级关系。 + +### 4. 子节点连接线 + +在 `.org-children` 容器中添加了左侧垂直连接线: +```css +.org-children::before { + content: ''; + position: absolute; + left: 15px; + top: 0; + bottom: 0; + width: 2px; + background: linear-gradient(to bottom, rgba(99, 102, 241, 0.3), rgba(99, 102, 241, 0.1)); +} +``` + +### 5. 交互反馈优化 + +**Toggle按钮改进** +- **尺寸**: 32x32px (更大,更易点击) +- **形状**: 方形 (现代感) +- **图标**: ▼ (展开) / ▶ (折叠) +- **颜色**: 半透明白色背景,与节点主题色协调 +- **Hover效果**: 放大1.05倍 + 阴影效果 + +**节点Hover效果** +- **阴影**: 根据层级调整颜色 + - 根节点: `rgba(67, 56, 202, 0.35)` + - 二级节点: `rgba(124, 58, 237, 0.35)` + - 三级节点: `rgba(167, 139, 250, 0.35)` +- **动画**: 向上浮动2px +- **过渡**: 0.2s平滑过渡 + +### 6. 叶子节点特殊处理 + +对于没有子节点的叶子节点: +- 使用 `.org-node:not(.has-children)` 样式 +- 左边距使用 `calc(var(--indent) + 20px)` +- 没有toggle按钮,但有placeholder保持对齐 +- 文字和元数据保持相同格式 + +### 7. 元数据标签优化 + +**编码标签 (`.node-code`)** +- 根/二级节点: 浅色文字,白色背景 +- 三级节点: 深色文字,透明背景 +- 字体加粗,提高可读性 + +**区域标签 (`.node-region`)** +- 圆角设计 (border-radius: 999px) +- 半透明背景,与节点主题协调 +- 字体略小 (12px),不抢夺主要信息焦点 + +## 技术实现 + +### CSS类层次结构 + +```css +/* 基础节点样式 */ +.org-node { + border-radius: 12px; + border-left: 4px solid; + box-shadow: 0 4px 12px; + transition: all 0.2s ease; +} + +/* 层级特定样式 */ +.org-node[data-level="0"] { ... } /* 根节点 */ +.org-node[data-level="1"] { ... } /* 二级节点 */ +.org-node[data-level="2"], +.org-node[data-level="3"], +.org-node[data-level="4"], +.org-node[data-level="5"] { ... } /* 三级及以下 */ + +/* 交互元素 */ +.toggle-btn { ... } +.org-children::before { ... } /* 连接线 */ +``` + +### JavaScript增强 + +在 `renderOrgListNode()` 函数中添加: +```javascript +nodeDiv.setAttribute('data-level', level); +nodeDiv.style.setProperty('--indent', `${level * 30}px`); +``` + +`data-level` 属性允许CSS根据层级应用不同的样式。 + +## 效果对比 + +### 改进前 +- ❌ 所有节点使用相同的背景色 (#f9fafb) +- ❌ 没有视觉层级区分 +- ❌ 缩进仅20px/层级,不够明显 +- ❌ 没有颜色编码或视觉指示器 +- ❌ Toggle按钮小 (28px),难以点击 + +### 改进后 +- ✅ 三级颜色体系,清晰区分层级 +- ✅ 递增的缩进 (0px → 20px → 40px) +- ✅ 左侧彩色边框和连接线 +- ✅ 大尺寸toggle按钮 (32px) +- ✅ 丰富的hover反馈效果 +- ✅ 统一的视觉语言和设计系统 + +## 文件修改清单 + +1. **static/super_admin.html** + - 修改 `.org-node` 及相关CSS类 (行 284-562) + - 更新 `renderOrgListNode()` JavaScript函数 (行 1297-1303) + +## 浏览器兼容性 + +- ✅ Chrome/Edge (现代版本) +- ✅ Firefox (现代版本) +- ✅ Safari (现代版本) +- ✅ 移动端浏览器 + +## 性能考虑 + +- 使用CSS渐变和过渡动画,性能友好 +- 所有动画使用 `transform` 和 `opacity`,硬件加速 +- 避免频繁的DOM操作 + +## 可扩展性 + +- 支持最多6级层级 (data-level="0" 到 "5") +- 颜色系统可轻松调整 +- 缩进和间距可配置 + +## 测试建议 + +1. **功能测试** + - 展开/折叠所有层级的节点 + - 搜索功能与视觉样式配合 + - Hover效果在所有节点上正常工作 + +2. **视觉测试** + - 验证每个层级的颜色正确 + - 检查缩进和对齐 + - 确认连接线显示正确 + +3. **响应式测试** + - 在不同屏幕尺寸下测试 + - 移动设备上的可用性 + +## 后续优化建议 + +1. **暗色主题支持** + - 添加 `prefers-color-scheme: dark` 媒体查询 + - 为暗色模式调整颜色 + +2. **动画增强** + - 添加节点展开/折叠的平滑动画 + - 考虑使用 FLIP 技术优化重排 + +3. **可访问性改进** + - 添加 `aria-expanded` 属性到toggle按钮 + - 确保键盘导航支持 + - 提升颜色对比度 (WCAG AA) + +4. **性能监控** + - 添加性能指标收集 + - 监控大量节点时的渲染性能 + +--- + +## 总结 + +通过这次改进,组织架构界面现在具有: + +1. **清晰的视觉层次** - 通过颜色、缩进和边框 +2. **直观的层级关系** - 一目了然的上下级关系 +3. **现代的设计语言** - 渐变、阴影和过渡动画 +4. **良好的交互体验** - 响应式hover和点击反馈 +5. **一致的设计系统** - 可扩展和可维护的代码结构 + +这些改进使组织架构更加易于理解和使用,特别适合展示复杂的政府部门层级结构。 diff --git a/static/super_admin.html b/static/super_admin.html index e89da87..ffccb62 100644 --- a/static/super_admin.html +++ b/static/super_admin.html @@ -276,140 +276,289 @@ padding: 10px 0 30px; } - .org-tree { - display: flex; - flex-direction: column; - align-items: center; + #orgChartContainer { 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 { + .org-list { display: flex; - justify-content: center; - align-items: flex-start; - gap: 28px; - position: relative; - flex-wrap: wrap; + flex-direction: column; + gap: 14px; } - .tree-level.root-level { - margin-top: 0; - padding-top: 0; + .org-node { + --indent: 0px; + border-radius: 12px; + background: #f9fafb; + border-left: 4px solid #cbd5e1; + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06); + transition: all 0.2s ease; } - .tree-level:not(.root-level) { - margin-top: 40px; - padding-top: 26px; + .org-node:hover { + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12); + transform: translateY(-2px); } - .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 { + .org-node.hidden { display: none; } - .tree-children { + /* 层级 0 - 根节点 */ + .org-node[data-level="0"] { + background: linear-gradient(135deg, #e0e7ff 0%, #f0f4ff 100%); + border-left-color: #818cf8; + border: 2px solid #818cf8; + } + + .org-node[data-level="0"] .node-name { + color: #312e81; + font-size: 18px; + font-weight: 700; + } + + .org-node[data-level="0"] .org-node-meta .node-code { + color: #4338ca; + font-weight: 600; + } + + .org-node[data-level="0"] .org-node-meta .node-region { + background: #e0e7ff; + color: #4338ca; + border: 1px solid #c7d2fe; + } + + .org-node[data-level="0"] .toggle-btn { + background: #e0e7ff; + border-color: #818cf8; + color: #4338ca; + } + + .org-node[data-level="0"] .toggle-btn:hover { + background: #c7d2fe; + } + + /* 层级 1 - 二级节点 */ + .org-node[data-level="1"] { + background: linear-gradient(135deg, #ede9fe 0%, #f3e8ff 100%); + border-left-color: #a78bfa; + border: 2px solid #a78bfa; + margin-left: 20px; + } + + .org-node[data-level="1"] .node-name { + color: #5b21b6; + font-size: 17px; + font-weight: 600; + } + + .org-node[data-level="1"] .org-node-meta .node-code { + color: #6d28d9; + font-weight: 600; + } + + .org-node[data-level="1"] .org-node-meta .node-region { + background: #ede9fe; + color: #6d28d9; + border: 1px solid #ddd6fe; + } + + .org-node[data-level="1"] .toggle-btn { + background: #ede9fe; + border-color: #a78bfa; + color: #6d28d9; + } + + .org-node[data-level="1"] .toggle-btn:hover { + background: #ddd6fe; + } + + /* 层级 2+ - 三级及以下节点 */ + .org-node[data-level="2"], + .org-node[data-level="3"], + .org-node[data-level="4"], + .org-node[data-level="5"] { + background: linear-gradient(135deg, #f5f3ff 0%, #faf5ff 100%); + border-left-color: #c4b5fd; + border: 2px solid #c4b5fd; + margin-left: 40px; + } + + .org-node[data-level="2"] .node-name, + .org-node[data-level="3"] .node-name, + .org-node[data-level="4"] .node-name, + .org-node[data-level="5"] .node-name { + color: #4c1d95; + font-size: 16px; + font-weight: 600; + } + + .org-node[data-level="2"] .org-node-meta .node-code, + .org-node[data-level="3"] .org-node-meta .node-code, + .org-node[data-level="4"] .org-node-meta .node-code, + .org-node[data-level="5"] .org-node-meta .node-code { + color: #6d28d9; + font-weight: 600; + } + + .org-node[data-level="2"] .org-node-meta .node-region, + .org-node[data-level="3"] .org-node-meta .node-region, + .org-node[data-level="4"] .org-node-meta .node-region, + .org-node[data-level="5"] .org-node-meta .node-region { + background: #f5f3ff; + color: #6d28d9; + border: 1px solid #e9d5ff; + } + + .org-node[data-level="2"] .toggle-btn, + .org-node[data-level="3"] .toggle-btn, + .org-node[data-level="4"] .toggle-btn, + .org-node[data-level="5"] .toggle-btn { + background: #f5f3ff; + border-color: #c4b5fd; + color: #6d28d9; + } + + .org-node[data-level="2"] .toggle-btn:hover, + .org-node[data-level="3"] .toggle-btn:hover, + .org-node[data-level="4"] .toggle-btn:hover, + .org-node[data-level="5"] .toggle-btn:hover { + background: #e9d5ff; + } + + .org-node-header { 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; + gap: 12px; + padding: 16px 20px; + padding-left: calc(var(--indent) + 20px); + } + + .org-node[data-level="0"] .org-node-header { + padding: 20px 24px; + } + + .org-node[data-level="1"] .org-node-header { + padding: 18px 22px; + } + + .org-node-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + } + + .org-node .node-name { + line-height: 1.4; + } + + .org-node-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + font-size: 13px; + line-height: 1.5; + } + + .org-node-meta .node-code { + font-weight: 500; + } + + .org-node-meta .node-region { + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + } + + .org-node-actions { + color: #6b7280; + font-size: 13px; + min-width: 160px; + text-align: right; + } + + .toggle-btn { + width: 32px; + height: 32px; + border-radius: 8px; + border: 1px solid #c7d2fe; + background: #eef2ff; + color: #4338ca; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-weight: 700; + transition: all 0.2s ease; + flex-shrink: 0; + } + + .toggle-btn:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(67, 56, 202, 0.2); + } + + .toggle-btn::before { + content: '▼'; + font-size: 14px; + line-height: 1; + } + + .toggle-btn.collapsed::before { + content: '▶'; + } + + .toggle-placeholder { + width: 32px; + height: 32px; + flex-shrink: 0; + } + + .org-children { + padding-bottom: 8px; position: relative; - padding: 20px 14px 0; - z-index: 1; } - .node-children::before { + .org-children::before { content: ''; position: absolute; + left: 15px; top: 0; - left: 50%; + bottom: 0; width: 2px; - height: 20px; - background: #94a3b8; - z-index: 0; + background: linear-gradient(to bottom, rgba(99, 102, 241, 0.3), rgba(99, 102, 241, 0.1)); } - .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; + .org-node.has-children.collapsed > .org-children { + display: none; + } + + /* 叶子节点样式(无子节点) */ + .org-node:not(.has-children) { + margin-left: calc(var(--indent) + 20px); + } + + .org-node:not(.has-children) .org-node-header { + padding-left: 20px; + } + + /* 增强的hover效果 */ + .org-node[data-level="0"]:hover { + box-shadow: 0 12px 28px rgba(129, 140, 248, 0.25); + } + + .org-node[data-level="1"]:hover { + box-shadow: 0 12px 28px rgba(167, 139, 250, 0.25); + } + + .org-node[data-level="2"]:hover, + .org-node[data-level="3"]:hover, + .org-node[data-level="4"]:hover, + .org-node[data-level="5"]:hover { + box-shadow: 0 12px 28px rgba(196, 181, 253, 0.25); } .tooltip { @@ -697,7 +846,7 @@
-
+
正在加载组织架构...
@@ -1133,73 +1282,140 @@ function getMaxLevel(nodes, level = 0) { 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); - + if (!tree.length) { + orgChartContainer.innerHTML = '
暂无组织架构数据
'; + return; + } + const list = document.createElement('div'); + list.className = 'org-list'; tree.forEach(node => { - rootLevel.appendChild(renderTreeNode(node, true)); + list.appendChild(renderOrgListNode(node, 0)); }); + orgChartContainer.appendChild(list); } -function renderTreeNode(node, isRoot = false) { +function renderOrgListNode(node, level) { + const nodeId = node.id != null ? String(node.id) : ''; 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) : ''); + nodeDiv.className = 'org-node'; + nodeDiv.setAttribute('data-node-id', nodeId); + nodeDiv.setAttribute('data-level', level); + nodeDiv.style.setProperty('--indent', `${level * 30}px`); - const nodeBox = document.createElement('div'); - nodeBox.className = 'tree-node-box'; + const header = document.createElement('div'); + header.className = 'org-node-header'; + header.setAttribute('data-node-id', nodeId); - const nameSpan = document.createElement('span'); + const hasChildren = Array.isArray(node.children) && node.children.length > 0; + if (hasChildren) { + nodeDiv.classList.add('has-children'); + const toggleBtn = document.createElement('button'); + toggleBtn.type = 'button'; + toggleBtn.className = 'toggle-btn'; + header.appendChild(toggleBtn); + const shouldExpand = level <= 1; + nodeDiv.dataset.defaultExpanded = shouldExpand ? 'true' : 'false'; + setNodeExpanded(nodeDiv, shouldExpand, toggleBtn); + toggleBtn.addEventListener('click', (evt) => { + evt.stopPropagation(); + const willExpand = nodeDiv.classList.contains('collapsed'); + setNodeExpanded(nodeDiv, willExpand, toggleBtn); + }); + } else { + const spacer = document.createElement('div'); + spacer.className = 'toggle-placeholder'; + header.appendChild(spacer); + nodeDiv.dataset.defaultExpanded = 'true'; + } + + const info = document.createElement('div'); + info.className = 'org-node-info'; + const nameSpan = document.createElement('div'); nameSpan.className = 'node-name'; nameSpan.textContent = node.name || '未知部门'; - nodeBox.appendChild(nameSpan); + info.appendChild(nameSpan); + const metaRow = document.createElement('div'); + metaRow.className = 'org-node-meta'; if (node.code) { const codeSpan = document.createElement('span'); codeSpan.className = 'node-code'; codeSpan.textContent = node.code; - nodeBox.appendChild(codeSpan); + metaRow.appendChild(codeSpan); } - if (node.region_name) { const regionSpan = document.createElement('span'); regionSpan.className = 'node-region'; regionSpan.textContent = node.region_name; - nodeBox.appendChild(regionSpan); + metaRow.appendChild(regionSpan); } + if (!metaRow.children.length) { + metaRow.style.display = 'none'; + } + info.appendChild(metaRow); + header.appendChild(info); + addTooltip(header, node); + nodeDiv.appendChild(header); - 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'); - } - + if (hasChildren) { + const fragment = document.createDocumentFragment(); node.children.forEach(child => { - const childHolder = document.createElement('div'); - childHolder.className = 'node-children'; - childHolder.appendChild(renderTreeNode(child, false)); - nextLevel.appendChild(childHolder); + fragment.appendChild(renderOrgListNode(child, level + 1)); }); - - childrenWrapper.appendChild(nextLevel); + const childrenWrapper = document.createElement('div'); + childrenWrapper.className = 'org-children'; + childrenWrapper.appendChild(fragment); nodeDiv.appendChild(childrenWrapper); } return nodeDiv; } +function findToggleButton(nodeElement) { + const nodeId = nodeElement.getAttribute('data-node-id'); + if (!nodeId) { + return nodeElement.querySelector('.org-node-header > .toggle-btn'); + } + return nodeElement.querySelector(`.org-node-header[data-node-id="${nodeId}"] > .toggle-btn`); +} + +function setNodeExpanded(nodeElement, expanded, toggleBtn) { + if (!nodeElement.classList.contains('has-children')) { + return; + } + nodeElement.classList.toggle('collapsed', !expanded); + const button = toggleBtn || findToggleButton(nodeElement); + if (button) { + button.classList.toggle('collapsed', !expanded); + button.classList.toggle('expanded', expanded); + button.setAttribute('aria-expanded', expanded ? 'true' : 'false'); + } +} + +function restoreDefaultCollapseState() { + document.querySelectorAll('#orgChartContainer .org-node.has-children').forEach(nodeDiv => { + const defaultExpanded = nodeDiv.dataset.defaultExpanded !== 'false'; + setNodeExpanded(nodeDiv, defaultExpanded); + }); +} + +function expandPathToNode(nodeId) { + let currentId = nodeId; + while (currentId) { + const nodeElement = document.querySelector(`#orgChartContainer .org-node[data-node-id="${currentId}"]`); + if (nodeElement) { + setNodeExpanded(nodeElement, true); + } + currentId = orgChartData.parentMap[currentId]; + } +} + +function ensureSearchVisibility(matchedNodes) { + matchedNodes.forEach(nodeId => { + expandPathToNode(nodeId); + }); +} + function addTooltip(element, node) { const tooltip = document.createElement('div'); tooltip.className = 'tooltip'; @@ -1240,7 +1456,7 @@ function searchOrgChart(query) { 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); + const regionMatch = (node.region_name || '').toLowerCase().includes(searchTerm); if (nameMatch || codeMatch || regionMatch) { matchedNodes.add(node.id); includeAncestorNodes(node.id, matchedNodes); @@ -1254,7 +1470,7 @@ function searchOrgChart(query) { } }); - document.querySelectorAll('.org-tree .tree-node').forEach(nodeDiv => { + document.querySelectorAll('#orgChartContainer .org-node').forEach(nodeDiv => { const nodeId = nodeDiv.getAttribute('data-node-id'); if (nodeId && matchedNodes.has(nodeId)) { nodeDiv.classList.remove('hidden'); @@ -1263,11 +1479,14 @@ function searchOrgChart(query) { } }); + if (matchedNodes.size) { + ensureSearchVisibility(matchedNodes); + } toggleNoResultsOverlay(matchedNodes.size === 0); } function highlightText(searchTerm, nodeId, field) { - const nodeDiv = document.querySelector(`.tree-node[data-node-id="${nodeId}"]`); + const nodeDiv = document.querySelector(`#orgChartContainer .org-node[data-node-id="${nodeId}"]`); if (!nodeDiv) return; const element = nodeDiv.querySelector(field === 'name' ? '.node-name' : '.node-code'); if (!element) return; @@ -1298,7 +1517,7 @@ function includeDescendantNodes(nodeData, matchedNodes) { } function resetOrgSearchView(clearInput = false) { - document.querySelectorAll('.org-tree .tree-node').forEach(nodeDiv => { + document.querySelectorAll('#orgChartContainer .org-node').forEach(nodeDiv => { nodeDiv.classList.remove('hidden'); const nodeId = nodeDiv.getAttribute('data-node-id'); const nodeData = orgChartData.nodeMap[nodeId]; @@ -1308,6 +1527,7 @@ function resetOrgSearchView(clearInput = false) { if (nameEl) nameEl.textContent = nodeData.name; if (codeEl) codeEl.textContent = nodeData.code; }); + restoreDefaultCollapseState(); if (clearInput) { const input = document.getElementById('orgSearchInput'); if (input) input.value = ''; @@ -1317,9 +1537,7 @@ function resetOrgSearchView(clearInput = false) { function toggleNoResultsOverlay(show) { if (!orgChartContainer) return; - const treeRoot = orgChartContainer.querySelector('.org-tree'); - if (!treeRoot) return; - let overlay = treeRoot.querySelector('.no-results-overlay'); + let overlay = orgChartContainer.querySelector('.no-results-overlay'); if (show) { if (!overlay) { overlay = document.createElement('div'); @@ -1336,7 +1554,7 @@ function toggleNoResultsOverlay(show) { resetOrgSearchView(true); }); overlay.appendChild(closeBtn); - treeRoot.appendChild(overlay); + orgChartContainer.appendChild(overlay); } } else if (overlay) { overlay.remove(); diff --git a/test_org_chart_soft.html b/test_org_chart_soft.html new file mode 100644 index 0000000..a760fde --- /dev/null +++ b/test_org_chart_soft.html @@ -0,0 +1,439 @@ + + + + + + 组织架构预览 - 柔和版 + + + +
+

🌳 组织架构预览 - 柔和配色版

+
+ +
+
+ +
+
佛山市市场监管局服务部门
+
+ FS-ADMIN-001 + 佛山市 +
+
+
+
+ +
+
+ +
+
禅城区服务部门
+
+ FS-ADMIN-CCQ + 禅城区 +
+
+
+
+ +
+
+
+
+
禅城区市场监督管理局
+
+ FS-ADMIN-CCQ-001 +
+
+
+
+
+
+
+
+
禅城区知识产权局
+
+ FS-ADMIN-CCQ-002 +
+
+
+
+
+
+ + +
+
+ +
+
南海区服务部门
+
+ FS-ADMIN-NHQ + 南海区 +
+
+
+
+
+
+
+
+
南海区市场监督管理局
+
+ FS-ADMIN-NHQ-001 +
+
+
+
+
+
+ + +
+
+ +
+
顺德区服务部门
+
+ FS-ADMIN-SDQ + 顺德区 +
+
+
+
+
+
+
+
+
顺德区市场监督管理局
+
+ FS-ADMIN-SDQ-001 +
+
+
+
+
+
+ + +
+
+ +
+
三水区服务部门
+
+ FS-ADMIN-SSQ + 三水区 +
+
+
+
+
+
+
+
+
三水区市场监督管理局
+
+ FS-ADMIN-SSQ-001 +
+
+
+
+
+
+ + +
+
+ +
+
高明区服务部门
+
+ FS-ADMIN-GMQ + 高明区 +
+
+
+
+
+
+
+
+
高明区市场监督管理局
+
+ FS-ADMIN-GMQ-001 +
+
+
+
+
+
+
+
+
+
+ + + +