树型菜单全功能代码

    <style>
      :root {
        --sidebar-width: 420px;
        --primary: #1a6b52;
        --primary-light: #e8f5e9;
        --primary-dark: #0d4a37;
        --accent: #2196f3;
        --accent-light: #e3f2fd;
        --danger: #e53935;
        --danger-light: #ffebee;
        --warning: #f57c00;
        --warning-light: #fff3e0;
        --bg: #eef2f5;
        --surface: #fff;
        --text: #1e293b;
        --text-secondary: #475569;
        --text-light: #94a3b8;
        --border: #e2e8f0;
        --border-light: #f1f5f9;
        --radius-sm: 6px;
        --radius: 10px;
        --radius-lg: 16px;
        --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
        --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
        --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.07);
        --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1);
        --shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.13);
      }
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        font-family:
          -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
          'Microsoft YaHei', 'Helvetica Neue', sans-serif;
        background: var(--bg);
        color: var(--text);
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 12px;
        -webkit-font-smoothing: antialiased;
        user-select: none;
        -webkit-user-select: none;
      }
      .app-container {
        display: flex;
        width: 100%;
        max-width: 1360px;
        height: 94vh;
        max-height: 920px;
        background: var(--surface);
        border-radius: var(--radius-lg);
        box-shadow: var(--shadow-xl);
        overflow: hidden;
      }
      /* ========== 左侧树面板 ========== */
      .tree-panel {
        width: var(--sidebar-width);
        min-width: var(--sidebar-width);
        display: flex;
        flex-direction: column;
        background: #fafbfd;
        border-right: 1px solid var(--border);
        position: relative;
        z-index: 2;
        height: 100%;
      }
      .panel-header {
        padding: 12px 16px 10px 16px;
        background: #fff;
        border-bottom: 1px solid var(--border-light);
        flex-shrink: 0;
      }
      .panel-header .brand {
        display: flex;
        align-items: center;
        gap: 8px;
        font-weight: 700;
        font-size: 0.95rem;
        color: var(--primary-dark);
        letter-spacing: 0.02em;
      }
      .panel-header .brand .icon {
        width: 30px;
        height: 30px;
        border-radius: 7px;
        background: linear-gradient(135deg, #1a6b52, #2e9d76);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 0.9rem;
        color: #fff;
        flex-shrink: 0;
      }
      .panel-header .subtitle {
        font-size: 0.66rem;
        color: var(--text-light);
        margin-top: 2px;
        padding-left: 38px;
        letter-spacing: 0.04em;
      }
      /* 树容器 */
      .tree-scroll {
        flex: 1;
        overflow-y: auto;
        overflow-x: hidden;
        padding: 8px 4px 12px 8px;
        scroll-behavior: smooth;
        -webkit-overflow-scrolling: touch;
        background: #fafbfd;
      }
      .tree-scroll::-webkit-scrollbar {
        width: 4px;
      }
      .tree-scroll::-webkit-scrollbar-track {
        background: transparent;
      }
      .tree-scroll::-webkit-scrollbar-thumb {
        background: #d0d7de;
        border-radius: 8px;
      }
      .tree-scroll::-webkit-scrollbar-thumb:hover {
        background: #b0b9c2;
      }
      /* 状态栏 */
      .panel-footer {
        padding: 7px 14px;
        border-top: 1px solid var(--border-light);
        background: #fff;
        flex-shrink: 0;
        display: flex;
        align-items: center;
        justify-content: space-between;
        font-size: 0.68rem;
        color: var(--text-light);
        letter-spacing: 0.03em;
        gap: 6px;
        flex-wrap: wrap;
      }
      .panel-footer .count-badge {
        font-weight: 600;
        color: var(--primary-dark);
        background: var(--primary-light);
        padding: 2px 10px;
        border-radius: 10px;
        font-size: 0.7rem;
        transition: all var(--transition);
      }
      .panel-footer .count-badge.zero {
        background: #f1f5f9;
        color: #94a3b8;
      }
      .panel-footer .footer-link {
        cursor: pointer;
        color: var(--accent);
        font-weight: 500;
        transition: color var(--transition);
        text-decoration: none;
        font-size: 0.68rem;
      }
      .panel-footer .footer-link:hover {
        color: #0d47a1;
        text-decoration: underline;
      }

      /* ========== 总节点样式 ========== */
      .root-node.summary-container {
        padding: 6px 8px;
        background: #f8fafd;
        border-radius: var(--radius-sm);
        margin-bottom: 6px;
        border: 1px solid #e9eef3;
        transition: all var(--transition);
      }
      .root-node.summary-container:hover {
        border-color: #cbd5e1;
      }
      .root-node > summary {
        display: flex;
        align-items: center;
        list-style: none;
        cursor: pointer;
        outline: none;
        gap: 4px;
        min-height: 42px;
        padding: 2px 4px;
      }
      .root-node > summary::-webkit-details-marker,
      .root-node > summary::marker {
        display: none;
      }
      .root-node .tree-arrow {
        width: 18px;
        height: 18px;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: transform var(--transition);
        color: #9aa8b4;
        font-size: 0.55rem;
        flex-shrink: 0;
      }
      .root-node[open] > summary .tree-arrow {
        transform: rotate(90deg);
        color: #556575;
      }
      .root-node .tree-checkbox {
        width: 16px;
        height: 16px;
        flex-shrink: 0;
        cursor: pointer;
        accent-color: var(--accent);
        margin: 0 3px;
        transition: transform var(--transition);
      }
      .root-node .tree-checkbox:active {
        transform: scale(0.85);
      }
      .root-node .node-dot.root-dot {
        width: 14px;
        height: 14px;
        border-radius: 50%;
        background: #1a6b52;
        box-shadow: 0 0 0 4px rgba(26, 107, 82, 0.15);
        flex-shrink: 0;
      }
      .root-node .node-name {
        font-size: 0.95rem;
        font-weight: 700;
        color: #0d4a37;
        letter-spacing: 0.03em;
        white-space: nowrap;
        margin-right: 8px;
        flex-shrink: 0;
      }
      .root-node .root-badge {
        font-size: 0.62rem;
        padding: 2px 8px;
        border-radius: 10px;
        font-weight: 600;
        background: #e8f5e9;
        color: #1a6b52;
        letter-spacing: 0.04em;
        flex-shrink: 0;
      }
      /* 总节点右侧操作区 */
      .root-actions {
        display: flex;
        align-items: center;
        gap: 5px;
        margin-left: auto;
        flex-shrink: 0;
        flex-wrap: wrap;
        justify-content: flex-end;
      }
      .root-actions .search-box {
        position: relative;
        display: flex;
        align-items: center;
        width: 120px;
      }
      .root-actions .search-box .search-icon {
        position: absolute;
        left: 7px;
        top: 50%;
        transform: translateY(-50%);
        color: #b0b8c1;
        font-size: 0.7rem;
        pointer-events: none;
        z-index: 1;
      }
      .root-actions .search-input {
        width: 100%;
        height: 30px;
        padding: 0 28px 0 26px;
        border: 2px solid #e2e8f0;
        border-radius: 15px;
        font-size: 0.72rem;
        color: var(--text);
        background: #fff;
        outline: none;
        transition: all var(--transition);
        letter-spacing: 0.02em;
      }
      .root-actions .search-input::placeholder {
        color: #bcc4cc;
      }
      .root-actions .search-input:focus {
        border-color: var(--accent);
        box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.06);
      }
      .root-actions .search-clear {
        position: absolute;
        right: 5px;
        top: 50%;
        transform: translateY(-50%);
        width: 18px;
        height: 18px;
        border-radius: 50%;
        border: none;
        background: #d5dbe3;
        color: #fff;
        cursor: pointer;
        font-size: 0.55rem;
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0;
        pointer-events: none;
        transition: all var(--transition);
        z-index: 1;
      }
      .root-actions .search-clear.visible {
        opacity: 1;
        pointer-events: auto;
      }
      .root-actions .search-clear:hover {
        background: #e57373;
        transform: translateY(-50%) scale(1.1);
      }
      .root-actions .sort-select {
        height: 30px;
        padding: 0 6px;
        border: 1.5px solid #e2e8f0;
        border-radius: 6px;
        font-size: 0.68rem;
        color: var(--text-secondary);
        background: #fff;
        cursor: pointer;
        outline: none;
        letter-spacing: 0.03em;
        transition: all var(--transition);
        min-width: 75px;
      }
      .root-actions .sort-select:hover {
        border-color: #c0c8d2;
      }
      .root-actions .sort-select:focus {
        border-color: var(--accent);
      }
      .root-actions .action-btn {
        height: 28px;
        width: 28px;
        border: 1.5px solid #e2e8f0;
        border-radius: 6px;
        background: #fff;
        cursor: pointer;
        font-size: 0.75rem;
        display: flex;
        align-items: center;
        justify-content: center;
        color: #5a6c7d;
        transition: all var(--transition);
        flex-shrink: 0;
      }
      .root-actions .action-btn:hover {
        background: #f0f4f8;
        border-color: #c0c8d2;
        color: #1e293b;
      }
      .root-actions .action-btn.danger {
        color: #c62828;
        border-color: #f5c6cb;
      }
      .root-actions .action-btn.danger:hover {
        background: #ffebee;
      }
      .root-actions .action-btn:active {
        transform: scale(0.9);
      }

      /* 普通树节点 (保持不变) */
      .tree-details {
        display: block;
        margin: 0;
        padding: 0;
      }
      .tree-details > summary {
        display: flex;
        align-items: center;
        cursor: pointer;
        list-style: none;
        padding: 5px 7px 5px 3px;
        margin: 1px 0;
        border-radius: var(--radius-sm);
        transition: all var(--transition);
        position: relative;
        outline: none;
        gap: 3px;
        min-height: 32px;
        letter-spacing: 0.02em;
      }
      .tree-details > summary::-webkit-details-marker,
      .tree-details > summary::marker {
        display: none;
      }
      .tree-details > summary:hover {
        background: #f0f4f8;
      }
      .tree-details[open] > summary {
        background: #f5f8fb;
        font-weight: 600;
      }
      .tree-details .tree-arrow {
        width: 16px;
        height: 16px;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: transform var(--transition);
        color: #9aa8b4;
        font-size: 0.5rem;
        border-radius: 3px;
        margin-right: 1px;
      }
      .tree-details[open] > summary .tree-arrow {
        transform: rotate(90deg);
        color: #556575;
      }
      .tree-checkbox {
        width: 15px;
        height: 15px;
        flex-shrink: 0;
        cursor: pointer;
        accent-color: var(--accent);
        margin: 0 2px;
      }
      .tree-checkbox:active {
        transform: scale(0.82);
      }
      .node-dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        flex-shrink: 0;
        transition: all var(--transition);
      }
      .node-dot.level-1 {
        width: 11px;
        height: 11px;
        background: #1a6b52;
        box-shadow: 0 0 0 3px rgba(26, 107, 82, 0.1);
      }
      .node-dot.level-2 {
        width: 8px;
        height: 8px;
        background: #2e9d76;
        box-shadow: 0 0 0 2px rgba(46, 157, 118, 0.09);
      }
      .node-dot.level-3 {
        width: 6px;
        height: 6px;
        background: #64b5a1;
        box-shadow: 0 0 0 2px rgba(100, 181, 161, 0.07);
      }
      .node-name {
        flex: 1;
        font-size: 0.82rem;
        color: var(--text);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        transition: color var(--transition);
        letter-spacing: 0.02em;
        cursor: pointer;
        padding: 1px 3px;
        border-radius: 3px;
      }
      .node-name:hover {
        color: var(--accent);
      }
      .level-1-name {
        font-size: 0.87rem;
        font-weight: 600;
        color: #1a3c34;
      }
      .level-2-name {
        font-size: 0.8rem;
        font-weight: 500;
        color: #2c4a3e;
      }
      .level-3-name {
        font-size: 0.76rem;
        font-weight: 400;
        color: #4a6358;
      }
      .node-badge {
        font-size: 0.58rem;
        padding: 1px 6px;
        border-radius: 8px;
        font-weight: 500;
        letter-spacing: 0.04em;
        flex-shrink: 0;
        opacity: 0.65;
        transition: opacity var(--transition);
        pointer-events: none;
      }
      .tree-details > summary:hover .node-badge,
      .tree-leaf:hover .node-badge {
        opacity: 1;
      }
      .badge-area {
        background: #e8f5e9;
        color: #1a6b52;
      }
      .badge-main {
        background: #e3f2fd;
        color: #1565c0;
      }
      .badge-branch {
        background: #fff3e0;
        color: #e65100;
      }
      .sub-tree {
        padding-left: 18px;
        border-left: 1.5px dashed #e0e6ec;
        margin-left: 9px;
        transition: border-color var(--transition);
      }
      .tree-details[open] > .sub-tree {
        border-left-color: #c8d6e0;
      }
      .tree-leaf {
        display: flex;
        align-items: center;
        cursor: pointer;
        padding: 5px 7px 5px 3px;
        margin: 1px 0;
        border-radius: var(--radius-sm);
        transition: all var(--transition);
        gap: 3px;
        min-height: 30px;
      }
      .tree-leaf:hover {
        background: #f0f4f8;
      }
      .tree-details > summary.row-selected,
      .tree-leaf.row-selected {
        background: var(--accent-light) !important;
        box-shadow: inset 3px 0 0 var(--accent);
      }
      .tree-details > summary.row-selected .node-name,
      .tree-leaf.row-selected .node-name {
        color: #0d47a1;
        font-weight: 600;
      }
      .tree-details > summary.search-highlight,
      .tree-leaf.search-highlight {
        background: #fffde7 !important;
        animation: highlightPulse 0.5s ease-out;
      }
      @keyframes highlightPulse {
        0% {
          background: #fff9c4 !important;
          transform: scale(1.02);
        }
        100% {
          background: #fffde7 !important;
          transform: scale(1);
        }
      }
      .tree-details.search-hidden,
      .tree-leaf.search-hidden {
        display: none !important;
      }

      /* 右键菜单 */
      .context-menu {
        position: fixed;
        z-index: 9999;
        background: #fff;
        border-radius: var(--radius);
        box-shadow: var(--shadow-xl);
        border: 1px solid var(--border);
        min-width: 175px;
        padding: 5px 0;
        display: none;
        animation: contextIn 0.15s ease-out;
      }
      @keyframes contextIn {
        from {
          opacity: 0;
          transform: scale(0.92);
        }
        to {
          opacity: 1;
          transform: scale(1);
        }
      }
      .context-menu .ctx-item {
        padding: 8px 15px;
        cursor: pointer;
        font-size: 0.76rem;
        color: var(--text);
        display: flex;
        align-items: center;
        gap: 8px;
        transition: all var(--transition);
        letter-spacing: 0.03em;
        white-space: nowrap;
      }
      .context-menu .ctx-item:hover {
        background: #f5f7f9;
        color: var(--primary-dark);
      }
      .context-menu .ctx-item.danger {
        color: #c62828;
      }
      .context-menu .ctx-item.danger:hover {
        background: var(--danger-light);
      }
      .context-menu .ctx-divider {
        height: 1px;
        background: var(--border-light);
        margin: 4px 0;
      }

      /* 右侧详情 */
      .detail-panel {
        flex: 1;
        display: flex;
        flex-direction: column;
        background: #fff;
        overflow-y: auto;
      }
      .detail-panel::-webkit-scrollbar {
        width: 5px;
      }
      .detail-panel::-webkit-scrollbar-track {
        background: transparent;
      }
      .detail-panel::-webkit-scrollbar-thumb {
        background: #d0d7de;
        border-radius: 10px;
      }
      .detail-header {
        padding: 18px 22px 12px;
        border-bottom: 1px solid var(--border-light);
        background: #fafcfd;
      }
      .detail-header .breadcrumb {
        font-size: 0.7rem;
        color: var(--text-light);
        margin-bottom: 5px;
        display: flex;
        flex-wrap: wrap;
        gap: 3px;
      }
      .breadcrumb span.sep {
        color: #b0b8c1;
        margin: 0 2px;
      }
      .breadcrumb .crumb-item.current {
        color: var(--primary-dark);
        font-weight: 600;
      }
      .detail-header .detail-title {
        font-size: 1.35rem;
        font-weight: 700;
        color: var(--primary-dark);
        display: flex;
        align-items: center;
        gap: 10px;
      }
      .detail-title .type-indicator {
        font-size: 0.66rem;
        padding: 3px 10px;
        border-radius: 12px;
        font-weight: 500;
        letter-spacing: 0.05em;
      }
      .type-indicator.area {
        background: #e8f5e9;
        color: #1a6b52;
      }
      .type-indicator.main-canal {
        background: #e3f2fd;
        color: #1565c0;
      }
      .type-indicator.branch-canal {
        background: #fff3e0;
        color: #e65100;
      }
      .detail-body {
        padding: 16px 22px;
      }
      .detail-card {
        background: #f9fafb;
        border-radius: var(--radius);
        padding: 14px 16px;
        margin-bottom: 10px;
        border: 1px solid var(--border-light);
        transition: all var(--transition);
      }
      .detail-card:hover {
        box-shadow: var(--shadow-sm);
        border-color: #d8dee4;
      }
      .detail-card .card-label {
        font-size: 0.65rem;
        text-transform: uppercase;
        letter-spacing: 0.06em;
        color: var(--text-light);
        margin-bottom: 3px;
        font-weight: 600;
      }
      .detail-card .card-value {
        font-size: 0.87rem;
        color: var(--text);
        font-weight: 500;
        line-height: 1.6;
      }
      .detail-info-grid {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 8px;
      }
      .empty-state {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 100%;
        color: #c0c8d0;
        text-align: center;
        padding: 40px;
      }
      .empty-state .empty-icon {
        font-size: 3.5rem;
        margin-bottom: 10px;
        opacity: 0.5;
        animation: float 3s infinite;
      }
      @keyframes float {
        0%,
        100% {
          transform: translateY(0);
        }
        50% {
          transform: translateY(-8px);
        }
      }
      .toast {
        position: fixed;
        bottom: 30px;
        left: 50%;
        transform: translateX(-50%);
        background: #1e293b;
        color: #fff;
        padding: 9px 20px;
        border-radius: 20px;
        font-size: 0.78rem;
        z-index: 10000;
        pointer-events: none;
        opacity: 0;
        transition: all 0.35s;
        box-shadow: var(--shadow-md);
        white-space: nowrap;
      }
      .toast.show {
        opacity: 1;
        transform: translateX(-50%) translateY(-10px);
      }
      .toast.success {
        background: #2e7d32;
      }
      .toast.error {
        background: #c62828;
      }
      @media (max-width: 860px) {
        .app-container {
          flex-direction: column;
          height: 98vh;
        }
        .tree-panel {
          width: 100%;
          min-width: 100%;
          max-height: 50%;
          border-right: none;
          border-bottom: 1px solid var(--border);
        }
        .detail-panel {
          flex: 1;
          min-height: 180px;
        }
        .root-actions {
          flex-wrap: wrap;
          gap: 3px;
        }
        .root-actions .search-box {
          width: 90px;
        }
        .detail-info-grid {
          grid-template-columns: 1fr;
        }
      }
    </style>

    <div class="app-container">
      <!-- ========== 左侧一体化树面板 ========== -->
      <aside class="tree-panel">
        <div class="panel-header">
          <div class="brand"><span class="icon">💧</span>灌区管理系统</div>
          <div class="subtitle">灌溉区 · 主渠道 · 支渠</div>
        </div>

        <!-- 树形菜单区域(包含总节点) -->
        <div class="tree-scroll" id="treeScroll">
          <nav id="treeContainer">
            <!-- ★ 总节点:灌区 ★ -->
            <details
              class="tree-details root-node"
              data-type="root"
              data-name="灌区"
              data-id="root"
              open
            >
              <summary data-type="root" data-name="灌区" data-id="root">
                <span class="tree-arrow"></span>
                <input
                  type="checkbox"
                  class="tree-checkbox root-checkbox"
                  data-id="root"
                  title="全选/取消全部"
                />
                <span class="node-dot root-dot"></span>
                <span class="node-name">灌区</span>
                <span class="root-badge">总灌区</span>
                <!-- 右侧集成操作区 -->
                <div class="root-actions" onclick="event.stopPropagation()">
                  <div class="search-box">
                    <span class="search-icon">🔍</span>
                    <input
                      type="text"
                      class="search-input"
                      id="searchInput"
                      placeholder="搜索..."
                      autocomplete="off"
                    />
                    <button class="search-clear" id="searchClear"></button>
                  </div>
                  <select class="sort-select" id="sortSelect">
                    <option value="default">📋 默认</option>
                    <option value="name-asc">🔤 A→Z</option>
                    <option value="name-desc">🔤 Z→A</option>
                    <option value="type-group">📂 类型</option>
                  </select>
                  <button
                    class="action-btn"
                    id="btnExpandAll"
                    title="展开全部节点"
                  >
                    📂
                  </button>
                  <button
                    class="action-btn"
                    id="btnCollapseAll"
                    title="折叠全部节点"
                  >
                    📁
                  </button>
                  <button
                    class="action-btn"
                    id="btnSelectAll"
                    title="全选所有节点"
                  ></button>
                  <button
                    class="action-btn"
                    id="btnDeselectAll"
                    title="取消全选"
                  ></button>
                  <button
                    class="action-btn danger"
                    id="btnDeleteSelected"
                    title="删除选中的节点"
                  >
                    🗑
                  </button>
                </div>
              </summary>
              <!-- 子节点:三个灌区保持不变 -->
              <div class="sub-tree">
                <details
                  class="tree-details"
                  data-type="area"
                  data-name="东风灌区"
                  data-id="area-1"
                  data-sort-key="东风灌区"
                >
                  <summary
                    data-type="area"
                    data-name="东风灌区"
                    data-id="area-1"
                    data-sort-key="东风灌区"
                  >
                    <span class="tree-arrow"></span>
                    <input
                      type="checkbox"
                      class="tree-checkbox"
                      data-id="area-1"
                    />
                    <span class="node-dot level-1"></span>
                    <span class="node-name level-1-name">东风灌区</span>
                    <span class="node-badge badge-area">灌区</span>
                  </summary>
                  <div class="sub-tree">
                    <details
                      class="tree-details"
                      data-type="main-canal"
                      data-name="东风主渠"
                      data-id="main-1-1"
                      data-sort-key="东风主渠"
                    >
                      <summary
                        data-type="main-canal"
                        data-name="东风主渠"
                        data-id="main-1-1"
                        data-sort-key="东风主渠"
                      >
                        <span class="tree-arrow"></span>
                        <input
                          type="checkbox"
                          class="tree-checkbox"
                          data-id="main-1-1"
                        />
                        <span class="node-dot level-2"></span>
                        <span class="node-name level-2-name">东风主渠</span>
                        <span class="node-badge badge-main">主渠道</span>
                      </summary>
                      <div class="sub-tree">
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="东一支渠"
                          data-id="branch-1-1-1"
                          data-sort-key="东一支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-1-1-1"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">东一支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="东二支渠"
                          data-id="branch-1-1-2"
                          data-sort-key="东二支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-1-1-2"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">东二支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="东三支渠"
                          data-id="branch-1-1-3"
                          data-sort-key="东三支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-1-1-3"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">东三支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                      </div>
                    </details>
                    <details
                      class="tree-details"
                      data-type="main-canal"
                      data-name="红旗主渠"
                      data-id="main-1-2"
                      data-sort-key="红旗主渠"
                    >
                      <summary
                        data-type="main-canal"
                        data-name="红旗主渠"
                        data-id="main-1-2"
                        data-sort-key="红旗主渠"
                      >
                        <span class="tree-arrow"></span
                        ><input
                          type="checkbox"
                          class="tree-checkbox"
                          data-id="main-1-2"
                        /><span class="node-dot level-2"></span
                        ><span class="node-name level-2-name">红旗主渠</span
                        ><span class="node-badge badge-main">主渠道</span>
                      </summary>
                      <div class="sub-tree">
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="红一支渠"
                          data-id="branch-1-2-1"
                          data-sort-key="红一支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-1-2-1"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">红一支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="红二支渠"
                          data-id="branch-1-2-2"
                          data-sort-key="红二支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-1-2-2"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">红二支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                      </div>
                    </details>
                  </div>
                </details>
                <details
                  class="tree-details"
                  data-type="area"
                  data-name="丰收灌区"
                  data-id="area-2"
                  data-sort-key="丰收灌区"
                >
                  <summary
                    data-type="area"
                    data-name="丰收灌区"
                    data-id="area-2"
                    data-sort-key="丰收灌区"
                  >
                    <span class="tree-arrow"></span
                    ><input
                      type="checkbox"
                      class="tree-checkbox"
                      data-id="area-2"
                    /><span class="node-dot level-1"></span
                    ><span class="node-name level-1-name">丰收灌区</span
                    ><span class="node-badge badge-area">灌区</span>
                  </summary>
                  <div class="sub-tree">
                    <details
                      class="tree-details"
                      data-type="main-canal"
                      data-name="丰收主渠"
                      data-id="main-2-1"
                      data-sort-key="丰收主渠"
                    >
                      <summary
                        data-type="main-canal"
                        data-name="丰收主渠"
                        data-id="main-2-1"
                        data-sort-key="丰收主渠"
                      >
                        <span class="tree-arrow"></span
                        ><input
                          type="checkbox"
                          class="tree-checkbox"
                          data-id="main-2-1"
                        /><span class="node-dot level-2"></span
                        ><span class="node-name level-2-name">丰收主渠</span
                        ><span class="node-badge badge-main">主渠道</span>
                      </summary>
                      <div class="sub-tree">
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="丰一支渠"
                          data-id="branch-2-1-1"
                          data-sort-key="丰一支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-2-1-1"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">丰一支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="丰二支渠"
                          data-id="branch-2-1-2"
                          data-sort-key="丰二支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-2-1-2"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">丰二支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                      </div>
                    </details>
                    <details
                      class="tree-details"
                      data-type="main-canal"
                      data-name="胜利主渠"
                      data-id="main-2-2"
                      data-sort-key="胜利主渠"
                    >
                      <summary
                        data-type="main-canal"
                        data-name="胜利主渠"
                        data-id="main-2-2"
                        data-sort-key="胜利主渠"
                      >
                        <span class="tree-arrow"></span
                        ><input
                          type="checkbox"
                          class="tree-checkbox"
                          data-id="main-2-2"
                        /><span class="node-dot level-2"></span
                        ><span class="node-name level-2-name">胜利主渠</span
                        ><span class="node-badge badge-main">主渠道</span>
                      </summary>
                      <div class="sub-tree">
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="胜一支渠"
                          data-id="branch-2-2-1"
                          data-sort-key="胜一支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-2-2-1"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">胜一支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="胜二支渠"
                          data-id="branch-2-2-2"
                          data-sort-key="胜二支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-2-2-2"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">胜二支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="胜三支渠"
                          data-id="branch-2-2-3"
                          data-sort-key="胜三支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-2-2-3"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">胜三支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                      </div>
                    </details>
                  </div>
                </details>
                <details
                  class="tree-details"
                  data-type="area"
                  data-name="清泉灌区"
                  data-id="area-3"
                  data-sort-key="清泉灌区"
                >
                  <summary
                    data-type="area"
                    data-name="清泉灌区"
                    data-id="area-3"
                    data-sort-key="清泉灌区"
                  >
                    <span class="tree-arrow"></span
                    ><input
                      type="checkbox"
                      class="tree-checkbox"
                      data-id="area-3"
                    /><span class="node-dot level-1"></span
                    ><span class="node-name level-1-name">清泉灌区</span
                    ><span class="node-badge badge-area">灌区</span>
                  </summary>
                  <div class="sub-tree">
                    <details
                      class="tree-details"
                      data-type="main-canal"
                      data-name="清泉主渠"
                      data-id="main-3-1"
                      data-sort-key="清泉主渠"
                    >
                      <summary
                        data-type="main-canal"
                        data-name="清泉主渠"
                        data-id="main-3-1"
                        data-sort-key="清泉主渠"
                      >
                        <span class="tree-arrow"></span
                        ><input
                          type="checkbox"
                          class="tree-checkbox"
                          data-id="main-3-1"
                        /><span class="node-dot level-2"></span
                        ><span class="node-name level-2-name">清泉主渠</span
                        ><span class="node-badge badge-main">主渠道</span>
                      </summary>
                      <div class="sub-tree">
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="清一支渠"
                          data-id="branch-3-1-1"
                          data-sort-key="清一支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-3-1-1"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">清一支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="清二支渠"
                          data-id="branch-3-1-2"
                          data-sort-key="清二支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-3-1-2"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">清二支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                      </div>
                    </details>
                    <details
                      class="tree-details"
                      data-type="main-canal"
                      data-name="碧波主渠"
                      data-id="main-3-2"
                      data-sort-key="碧波主渠"
                    >
                      <summary
                        data-type="main-canal"
                        data-name="碧波主渠"
                        data-id="main-3-2"
                        data-sort-key="碧波主渠"
                      >
                        <span class="tree-arrow"></span
                        ><input
                          type="checkbox"
                          class="tree-checkbox"
                          data-id="main-3-2"
                        /><span class="node-dot level-2"></span
                        ><span class="node-name level-2-name">碧波主渠</span
                        ><span class="node-badge badge-main">主渠道</span>
                      </summary>
                      <div class="sub-tree">
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="碧一支渠"
                          data-id="branch-3-2-1"
                          data-sort-key="碧一支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-3-2-1"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">碧一支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                        <div
                          class="tree-leaf"
                          data-type="branch-canal"
                          data-name="碧二支渠"
                          data-id="branch-3-2-2"
                          data-sort-key="碧二支渠"
                        >
                          <input
                            type="checkbox"
                            class="tree-checkbox"
                            data-id="branch-3-2-2"
                          /><span class="node-dot level-3"></span
                          ><span class="node-name level-3-name">碧二支渠</span
                          ><span class="node-badge badge-branch">支渠</span>
                        </div>
                      </div>
                    </details>
                  </div>
                </details>
              </div>
            </details>
          </nav>
        </div>

        <!-- 底部状态栏 -->
        <div class="panel-footer">
          <span
            >已选:
            <span class="count-badge zero" id="selectedCount">0</span></span
          >
          <span
            class="footer-link"
            id="clearSelectionLink"
            style="display: none"
            >清除选择</span
          >
        </div>
      </aside>

      <!-- ========== 右侧详情面板 ========== -->
      <main class="detail-panel" id="detailPanel">
        <div class="empty-state" id="emptyState">
          <div class="empty-icon">📋</div>
          <div class="empty-text">点击任意节点查看详情</div>
          <div class="empty-hint">右键节点可进行更多操作 · 双击重命名</div>
        </div>
        <div id="detailContent" style="display: none">
          <div class="detail-header">
            <div class="breadcrumb" id="breadcrumb"></div>
            <div class="detail-title" id="detailTitle"></div>
          </div>
          <div class="detail-body" id="detailBody"></div>
        </div>
      </main>
    </div>

    <!-- 右键菜单 -->
    <div class="context-menu" id="contextMenu">
      <div class="ctx-item" data-action="expand">📂 展开</div>
      <div class="ctx-item" data-action="collapse">📁 折叠</div>
      <div class="ctx-divider"></div>
      <div class="ctx-item" data-action="rename">✏️ 重命名</div>
      <div class="ctx-item" data-action="addChild">➕ 添加子节点</div>
      <div class="ctx-divider"></div>
      <div class="ctx-item" data-action="selectChildren">☑ 全选子节点</div>
      <div class="ctx-item" data-action="deselectChildren">
        ☐ 取消全选子节点
      </div>
      <div class="ctx-item" data-action="sortChildrenAsc">
        🔤 子节点排序 A→Z
      </div>
      <div class="ctx-item" data-action="sortChildrenDesc">
        🔤 子节点排序 Z→A
      </div>
      <div class="ctx-divider"></div>
      <div class="ctx-item danger" data-action="delete">🗑 删除节点</div>
    </div>

    <div class="toast" id="toast"></div>

    <script>
      ;(function () {
        // DOM 元素
        const treeContainer = document.getElementById('treeContainer')
        const searchInput = document.getElementById('searchInput')
        const searchClear = document.getElementById('searchClear')
        const detailPanel = document.getElementById('detailPanel')
        const emptyState = document.getElementById('emptyState')
        const detailContent = document.getElementById('detailContent')
        const breadcrumbEl = document.getElementById('breadcrumb')
        const detailTitleEl = document.getElementById('detailTitle')
        const detailBodyEl = document.getElementById('detailBody')
        const selectedCountEl = document.getElementById('selectedCount')
        const clearSelectionLink = document.getElementById('clearSelectionLink')
        const contextMenu = document.getElementById('contextMenu')
        const toast = document.getElementById('toast')
        const sortSelect = document.getElementById('sortSelect')
        const rootNode = document.querySelector('.root-node')
        const rootCheckbox = rootNode.querySelector('.root-checkbox')

        let currentRowSelected = null
        let contextTarget = null
        let searchTimeout = null
        let idCounter = 300
        let toastTimer = null

        // 数据映射 (同前)
        const nodeDataMap = {
          'area-1': {
            name: '东风灌区',
            type: 'area',
            typeLabel: '灌区',
            location: '河南省郑州市',
            area: '3,200 公顷',
            established: '1985年',
            waterSource: '黄河水系',
            annualSupply: '1.8亿立方米',
            description: '东风灌区是河南省重要的农业灌溉区,覆盖12个乡镇。',
          },
          'area-2': {
            name: '丰收灌区',
            type: 'area',
            typeLabel: '灌区',
            location: '山东省济南市',
            area: '4,500 公顷',
            established: '1992年',
            waterSource: '黄河水系',
            annualSupply: '2.3亿立方米',
            description:
              '丰收灌区位于山东省中部,是黄河流域重要的粮食生产基地。',
          },
          'area-3': {
            name: '清泉灌区',
            type: 'area',
            typeLabel: '灌区',
            location: '河北省石家庄市',
            area: '2,800 公顷',
            established: '1988年',
            waterSource: '地下水+地表水',
            annualSupply: '1.5亿立方米',
            description: '清泉灌区是河北省现代化灌区示范点。',
          },
          'main-1-1': {
            name: '东风主渠',
            type: 'main-canal',
            typeLabel: '主渠道',
            length: '25 公里',
            capacity: '15 立方米/秒',
            coverage: '8个行政村',
            material: '混凝土衬砌',
            completed: '1986年',
            description: '东风主渠是东风灌区的骨干输水渠道。',
          },
          'main-1-2': {
            name: '红旗主渠',
            type: 'main-canal',
            typeLabel: '主渠道',
            length: '18 公里',
            capacity: '12 立方米/秒',
            coverage: '6个行政村',
            material: '混凝土衬砌',
            completed: '1987年',
            description: '红旗主渠连接东风灌区东部区域。',
          },
          'main-2-1': {
            name: '丰收主渠',
            type: 'main-canal',
            typeLabel: '主渠道',
            length: '30 公里',
            capacity: '18 立方米/秒',
            coverage: '10个行政村',
            material: '钢筋混凝土',
            completed: '1993年',
            description: '丰收主渠是丰收灌区最长的主渠道。',
          },
          'main-2-2': {
            name: '胜利主渠',
            type: 'main-canal',
            typeLabel: '主渠道',
            length: '22 公里',
            capacity: '14 立方米/秒',
            coverage: '7个行政村',
            material: '混凝土衬砌',
            completed: '1994年',
            description: '胜利主渠为丰收灌区西线骨干渠道。',
          },
          'main-3-1': {
            name: '清泉主渠',
            type: 'main-canal',
            typeLabel: '主渠道',
            length: '20 公里',
            capacity: '13 立方米/秒',
            coverage: '5个行政村',
            material: '钢筋混凝土',
            completed: '1989年',
            description: '清泉主渠配备智能监控系统。',
          },
          'main-3-2': {
            name: '碧波主渠',
            type: 'main-canal',
            typeLabel: '主渠道',
            length: '15 公里',
            capacity: '10 立方米/秒',
            coverage: '4个行政村',
            material: '混凝土衬砌',
            completed: '1990年',
            description: '碧波主渠以生态护坡为特色。',
          },
          'branch-1-1-1': {
            name: '东一支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '5 公里',
            capacity: '3 立方米/秒',
            coverage: '3个自然村',
            material: 'U型槽衬砌',
            completed: '1987年',
            description: '东一支渠主要服务于东风灌区东北部。',
          },
          'branch-1-1-2': {
            name: '东二支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '3.5 公里',
            capacity: '2.5 立方米/秒',
            coverage: '2个自然村',
            material: 'U型槽衬砌',
            completed: '1988年',
            description: '东二支渠覆盖东风灌区东南部。',
          },
          'branch-1-1-3': {
            name: '东三支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '4.2 公里',
            capacity: '2.8 立方米/秒',
            coverage: '3个自然村',
            material: '现浇混凝土',
            completed: '1989年',
            description: '东三支渠是东风主渠最南端的支渠。',
          },
          'branch-1-2-1': {
            name: '红一支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '6 公里',
            capacity: '3.5 立方米/秒',
            coverage: '4个自然村',
            material: 'U型槽衬砌',
            completed: '1988年',
            description: '红一支渠是红旗主渠最大的支渠。',
          },
          'branch-1-2-2': {
            name: '红二支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '2.8 公里',
            capacity: '2 立方米/秒',
            coverage: '2个自然村',
            material: '现浇混凝土',
            completed: '1989年',
            description: '红二支渠以果树灌溉为主。',
          },
          'branch-2-1-1': {
            name: '丰一支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '7 公里',
            capacity: '4 立方米/秒',
            coverage: '5个自然村',
            material: 'U型槽衬砌',
            completed: '1994年',
            description: '丰一支渠是丰收灌区最大的支渠。',
          },
          'branch-2-1-2': {
            name: '丰二支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '4.5 公里',
            capacity: '3 立方米/秒',
            coverage: '3个自然村',
            material: '现浇混凝土',
            completed: '1995年',
            description: '丰二支渠覆盖丰收灌区西南部。',
          },
          'branch-2-2-1': {
            name: '胜一支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '5.5 公里',
            capacity: '3.2 立方米/秒',
            coverage: '3个自然村',
            material: 'U型槽衬砌',
            completed: '1995年',
            description: '胜一支渠以高效节水灌溉为特色。',
          },
          'branch-2-2-2': {
            name: '胜二支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '3.2 公里',
            capacity: '2.5 立方米/秒',
            coverage: '2个自然村',
            material: '现浇混凝土',
            completed: '1996年',
            description: '胜二支渠服务于胜利主渠中段。',
          },
          'branch-2-2-3': {
            name: '胜三支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '2.1 公里',
            capacity: '1.8 立方米/秒',
            coverage: '1个自然村',
            material: 'U型槽衬砌',
            completed: '1997年',
            description: '胜三支渠是胜利主渠末端支渠。',
          },
          'branch-3-1-1': {
            name: '清一支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '4 公里',
            capacity: '2.8 立方米/秒',
            coverage: '2个自然村',
            material: 'U型槽衬砌',
            completed: '1990年',
            description: '清一支渠配备智能闸门控制系统。',
          },
          'branch-3-1-2': {
            name: '清二支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '5.5 公里',
            capacity: '3.3 立方米/秒',
            coverage: '3个自然村',
            material: '现浇混凝土',
            completed: '1991年',
            description: '清二支渠覆盖清泉灌区北部。',
          },
          'branch-3-2-1': {
            name: '碧一支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '3.8 公里',
            capacity: '2.5 立方米/秒',
            coverage: '2个自然村',
            material: '生态护坡衬砌',
            completed: '1991年',
            description: '碧一支渠采用生态护坡设计。',
          },
          'branch-3-2-2': {
            name: '碧二支渠',
            type: 'branch-canal',
            typeLabel: '支渠',
            length: '4.6 公里',
            capacity: '2.8 立方米/秒',
            coverage: '3个自然村',
            material: 'U型槽衬砌',
            completed: '1992年',
            description: '碧二支渠是碧波主渠最大的支渠。',
          },
        }

        function showToast(msg, type = '') {
          if (toastTimer) clearTimeout(toastTimer)
          toast.textContent = msg
          toast.className = 'toast ' + type + ' show'
          toastTimer = setTimeout(() => toast.classList.remove('show'), 1800)
        }

        function getRowEl(el) {
          if (!el || el === treeContainer) return null
          if (
            el.tagName === 'SUMMARY' &&
            el.parentElement?.classList.contains('tree-details')
          )
            return el
          if (el.classList.contains('tree-leaf')) return el
          let cur = el
          while (cur && cur !== treeContainer) {
            if (
              cur.tagName === 'SUMMARY' &&
              cur.parentElement?.classList.contains('tree-details')
            )
              return cur
            if (cur.classList.contains('tree-leaf')) return cur
            cur = cur.parentElement
          }
          return null
        }
        function getRowContainer(el) {
          const r = getRowEl(el)
          return r ? (r.tagName === 'SUMMARY' ? r.parentElement : r) : null
        }
        function getNodeId(el) {
          const r = getRowEl(el)
          return r ? r.getAttribute('data-id') : null
        }
        function getNodeType(el) {
          const r = getRowEl(el)
          return r ? r.getAttribute('data-type') : null
        }
        function getNodeName(el) {
          const r = getRowEl(el)
          return r ? r.getAttribute('data-name') : null
        }
        function getCheckbox(rowEl) {
          return rowEl.querySelector(':scope > .tree-checkbox, .tree-checkbox')
        }
        function getSubTree(detailsEl) {
          return detailsEl.querySelector(':scope > .sub-tree')
        }
        function getAllRows() {
          const rows = []
          treeContainer
            .querySelectorAll('.tree-details > summary')
            .forEach((s) => rows.push(s))
          treeContainer
            .querySelectorAll('.tree-leaf')
            .forEach((l) => rows.push(l))
          return rows
        }
        function getAllCheckboxes() {
          return treeContainer.querySelectorAll('.tree-checkbox')
        }
        function getAllDetails() {
          return treeContainer.querySelectorAll('.tree-details')
        }

        function getChildRows(parentEl) {
          const children = []
          if (parentEl.classList.contains('tree-details')) {
            const sub = getSubTree(parentEl)
            if (sub) {
              sub
                .querySelectorAll(':scope > .tree-details > summary')
                .forEach((s) => children.push(s))
              sub
                .querySelectorAll(':scope > .tree-leaf')
                .forEach((l) => children.push(l))
            }
          }
          return children
        }
        function getChildCheckboxes(parentEl) {
          const cbs = []
          if (parentEl.classList.contains('tree-details')) {
            const sub = getSubTree(parentEl)
            if (sub)
              sub
                .querySelectorAll('.tree-checkbox')
                .forEach((cb) => cbs.push(cb))
          }
          return cbs
        }
        function setCheckboxState(cb, checked) {
          cb.checked = checked
          cb.indeterminate = false
        }

        function updateChildrenCheckboxes(parentEl, checked) {
          getChildCheckboxes(parentEl).forEach((cb) => {
            setCheckboxState(cb, checked)
            const row = getRowEl(cb)
            if (row && row.tagName === 'SUMMARY')
              updateChildrenCheckboxes(row.parentElement, checked)
          })
        }

        function getAncestorDetails(el) {
          const anc = []
          let cur = el.parentElement
          while (cur && cur !== treeContainer) {
            if (
              cur.classList.contains('sub-tree') &&
              cur.parentElement?.classList.contains('tree-details')
            )
              anc.push(cur.parentElement)
            cur = cur.parentElement
          }
          return anc
        }

        function updateAncestorCheckboxes(el) {
          getAncestorDetails(el).forEach((details) => {
            const summary = details.querySelector(':scope > summary')
            const cb = summary ? getCheckbox(summary) : null
            if (!cb) return
            const childCbs = getChildCheckboxes(details)
            const total = childCbs.length
            const checked = childCbs.filter((c) => c.checked).length
            const indet = childCbs.filter((c) => c.indeterminate).length
            if (total === 0) setCheckboxState(cb, cb.checked)
            else if (checked === total && indet === 0)
              setCheckboxState(cb, true)
            else if (checked === 0 && indet === 0) setCheckboxState(cb, false)
            else {
              cb.checked = false
              cb.indeterminate = true
            }
          })
        }

        function handleCheckboxChange(cb) {
          const row = getRowEl(cb)
          if (!row) return
          const checked = cb.checked
          cb.indeterminate = false
          if (row.tagName === 'SUMMARY')
            updateChildrenCheckboxes(row.parentElement, checked)
          updateAncestorCheckboxes(row)
          updateSelectedCount()
        }

        function getCheckedCheckboxes() {
          return Array.from(getAllCheckboxes()).filter(
            (cb) => cb.checked && !cb.indeterminate,
          )
        }

        function updateSelectedCount() {
          const count = getCheckedCheckboxes().length
          selectedCountEl.textContent = count
          if (count === 0) {
            selectedCountEl.classList.add('zero')
            clearSelectionLink.style.display = 'none'
          } else {
            selectedCountEl.classList.remove('zero')
            clearSelectionLink.style.display = 'inline'
          }
        }

        function clearAllSelections() {
          getAllCheckboxes().forEach((cb) => {
            cb.checked = false
            cb.indeterminate = false
          })
          updateSelectedCount()
          showToast('已清除全部选择', 'success')
        }
        function selectAllNodes() {
          getAllCheckboxes().forEach((cb) => {
            cb.checked = true
            cb.indeterminate = false
          })
          updateSelectedCount()
          showToast('已全选所有节点', 'success')
        }

        function selectRow(rowEl) {
          if (currentRowSelected && currentRowSelected !== rowEl)
            currentRowSelected.classList.remove('row-selected')
          rowEl.classList.add('row-selected')
          currentRowSelected = rowEl
          showDetail(rowEl)
        }
        function clearRowSelection() {
          if (currentRowSelected) {
            currentRowSelected.classList.remove('row-selected')
            currentRowSelected = null
          }
          emptyState.style.display = 'flex'
          detailContent.style.display = 'none'
        }

        function getNodePath(rowEl) {
          const path = []
          const name = rowEl.getAttribute('data-name')
          if (name) path.unshift({ name, el: rowEl })
          let cur = rowEl.parentElement
          while (cur && cur !== treeContainer) {
            if (
              cur.classList.contains('sub-tree') &&
              cur.parentElement?.classList.contains('tree-details')
            ) {
              const s = cur.parentElement.querySelector(':scope > summary')
              if (s) {
                const n = s.getAttribute('data-name')
                if (n) path.unshift({ name: n, el: s })
              }
            }
            cur = cur.parentElement
          }
          return path
        }

        function showDetail(rowEl) {
          const nodeId = rowEl.getAttribute('data-id')
          const nodeName = rowEl.getAttribute('data-name')
          const nodeType = rowEl.getAttribute('data-type')
          if (nodeId === 'root') {
            breadcrumbEl.innerHTML =
              '<span class="crumb-item current">灌区</span>'
            detailTitleEl.innerHTML =
              '灌区 <span class="type-indicator area">总灌区</span>'
            detailBodyEl.innerHTML =
              '<div class="detail-card"><div class="card-label">📝 描述</div><div class="card-value">灌区是全部灌溉区域的总管理机构,下辖东风、丰收、清泉三大灌区,负责统筹水资源调配。</div></div>'
            emptyState.style.display = 'none'
            detailContent.style.display = 'block'
            return
          }
          const data = nodeDataMap[nodeId]
          const path = getNodePath(rowEl)
          breadcrumbEl.innerHTML = path
            .map(
              (p, i) =>
                `<span class="crumb-item${i === path.length - 1 ? ' current' : ''}">${p.name}</span>${i < path.length - 1 ? '<span class="sep">›</span>' : ''}`,
            )
            .join('')
          const tc = {
            area: 'area',
            'main-canal': 'main-canal',
            'branch-canal': 'branch-canal',
          }
          const tl = {
            area: '灌区',
            'main-canal': '主渠道',
            'branch-canal': '支渠',
          }
          detailTitleEl.innerHTML = `${nodeName} <span class="type-indicator ${tc[nodeType] || 'area'}">${data?.typeLabel || tl[nodeType] || '节点'}</span>`
          let cards = ''
          if (data) {
            if (data.type === 'area')
              cards = `<div class="detail-info-grid"><div class="detail-card"><div class="card-label">📍 所在地</div><div class="card-value">${data.location}</div></div><div class="detail-card"><div class="card-label">📐 灌溉面积</div><div class="card-value">${data.area}</div></div><div class="detail-card"><div class="card-label">📅 建成年份</div><div class="card-value">${data.established}</div></div><div class="detail-card"><div class="card-label">💧 水源类型</div><div class="card-value">${data.waterSource}</div></div><div class="detail-card"><div class="card-label">📊 年供水量</div><div class="card-value">${data.annualSupply}</div></div></div><div class="detail-card"><div class="card-label">📝 灌区简介</div><div class="card-value" style="font-weight:400;line-height:1.7;">${data.description}</div></div>`
            else if (data.type === 'main-canal')
              cards = `<div class="detail-info-grid"><div class="detail-card"><div class="card-label">📏 渠道长度</div><div class="card-value">${data.length}</div></div><div class="detail-card"><div class="card-label">🌊 输水能力</div><div class="card-value">${data.capacity}</div></div><div class="detail-card"><div class="card-label">🏘️ 覆盖范围</div><div class="card-value">${data.coverage}</div></div><div class="detail-card"><div class="card-label">🧱 渠道材质</div><div class="card-value">${data.material}</div></div><div class="detail-card"><div class="card-label">📅 完工年份</div><div class="card-value">${data.completed}</div></div></div><div class="detail-card"><div class="card-label">📝 渠道简介</div><div class="card-value" style="font-weight:400;line-height:1.7;">${data.description}</div></div>`
            else
              cards = `<div class="detail-info-grid"><div class="detail-card"><div class="card-label">📏 支渠长度</div><div class="card-value">${data.length}</div></div><div class="detail-card"><div class="card-label">🌊 过水能力</div><div class="card-value">${data.capacity}</div></div><div class="detail-card"><div class="card-label">🏘️ 覆盖范围</div><div class="card-value">${data.coverage}</div></div><div class="detail-card"><div class="card-label">🧱 衬砌类型</div><div class="card-value">${data.material}</div></div><div class="detail-card"><div class="card-label">📅 完工年份</div><div class="card-value">${data.completed}</div></div></div><div class="detail-card"><div class="card-label">📝 支渠简介</div><div class="card-value" style="font-weight:400;line-height:1.7;">${data.description}</div></div>`
          } else
            cards =
              '<div class="detail-card"><div class="card-label">📝 信息</div><div class="card-value">暂无详细数据</div></div>'
          detailBodyEl.innerHTML = cards
          emptyState.style.display = 'none'
          detailContent.style.display = 'block'
          detailPanel.scrollTop = 0
        }

        // 排序
        function sortSubTree(sub, mode) {
          if (!sub) return
          const items = Array.from(sub.children)
          items.sort((a, b) => {
            const ka =
              a.getAttribute('data-sort-key') ||
              a.getAttribute('data-name') ||
              ''
            const kb =
              b.getAttribute('data-sort-key') ||
              b.getAttribute('data-name') ||
              ''
            if (mode === 'name-asc') return ka.localeCompare(kb, 'zh')
            if (mode === 'name-desc') return kb.localeCompare(ka, 'zh')
            if (mode === 'type-group') {
              const ta = a.getAttribute('data-type')
              const tb = b.getAttribute('data-type')
              const order = { area: 1, 'main-canal': 2, 'branch-canal': 3 }
              if ((order[ta] || 9) !== (order[tb] || 9))
                return (order[ta] || 9) - (order[tb] || 9)
              return ka.localeCompare(kb, 'zh')
            }
            return 0
          })
          items.forEach((item) => sub.appendChild(item))
        }
        function sortAllSubTrees(mode) {
          treeContainer
            .querySelectorAll('.sub-tree')
            .forEach((sub) => sortSubTree(sub, mode))
        }
        sortSelect.addEventListener('change', function () {
          const mode = this.value
          if (mode === 'default') showToast('排序已重置为默认', 'success')
          else {
            sortAllSubTrees(mode)
            showToast('节点已排序', 'success')
          }
        })

        // 右键菜单
        function hideContextMenu() {
          contextMenu.style.display = 'none'
          contextTarget = null
        }
        function showContextMenu(x, y, targetRow) {
          contextTarget = targetRow
          const items = contextMenu.querySelectorAll('.ctx-item')
          const nodeType = getNodeType(targetRow)
          items.forEach((item) => {
            const action = item.getAttribute('data-action')
            item.style.display = 'flex'
            if (action === 'addChild' && nodeType === 'branch-canal')
              item.style.display = 'none'
            if (
              (action === 'expand' ||
                action === 'collapse' ||
                action === 'sortChildrenAsc' ||
                action === 'sortChildrenDesc') &&
              nodeType === 'branch-canal'
            )
              item.style.display = 'none'
            if (
              (action === 'selectChildren' || action === 'deselectChildren') &&
              nodeType === 'branch-canal'
            )
              item.style.display = 'none'
          })
          contextMenu.style.display = 'block'
          const mr = contextMenu.getBoundingClientRect()
          let l = x,
            t = y
          if (l + mr.width > window.innerWidth - 10)
            l = window.innerWidth - mr.width - 10
          if (t + mr.height > window.innerHeight - 10)
            t = window.innerHeight - mr.height - 10
          contextMenu.style.left = Math.max(5, l) + 'px'
          contextMenu.style.top = Math.max(5, t) + 'px'
        }
        document.addEventListener('click', (e) => {
          if (!contextMenu.contains(e.target)) hideContextMenu()
        })
        document.addEventListener('keydown', (e) => {
          if (e.key === 'Escape') hideContextMenu()
        })
        contextMenu.addEventListener('click', function (e) {
          const act = e.target.closest('.ctx-item')
          if (!act || !contextTarget) return
          const action = act.getAttribute('data-action')
          const target = contextTarget
          hideContextMenu()
          switch (action) {
            case 'expand':
              if (target.tagName === 'SUMMARY') target.parentElement.open = true
              break
            case 'collapse':
              if (target.tagName === 'SUMMARY')
                target.parentElement.open = false
              break
            case 'rename':
              renameNode(target)
              break
            case 'addChild':
              addChildNode(target)
              break
            case 'selectChildren':
              if (target.tagName === 'SUMMARY') {
                getChildCheckboxes(target.parentElement).forEach((cb) => {
                  cb.checked = true
                  cb.indeterminate = false
                })
                updateAncestorCheckboxes(target)
                updateSelectedCount()
                showToast('已全选子节点', 'success')
              }
              break
            case 'deselectChildren':
              if (target.tagName === 'SUMMARY') {
                getChildCheckboxes(target.parentElement).forEach((cb) => {
                  cb.checked = false
                  cb.indeterminate = false
                })
                updateAncestorCheckboxes(target)
                updateSelectedCount()
                showToast('已取消全选子节点', 'success')
              }
              break
            case 'sortChildrenAsc':
              sortChildrenOfNode(target, 'name-asc')
              break
            case 'sortChildrenDesc':
              sortChildrenOfNode(target, 'name-desc')
              break
            case 'delete':
              deleteNode(target)
              break
          }
        })
        function sortChildrenOfNode(targetRow, mode) {
          if (targetRow.tagName !== 'SUMMARY') return
          const sub = getSubTree(targetRow.parentElement)
          if (sub) sortSubTree(sub, mode)
          showToast('子节点已排序', 'success')
        }

        function renameNode(targetRow) {
          const old = getNodeName(targetRow)
          const nw = prompt('请输入新名称:', old)
          if (nw && nw.trim() && nw.trim() !== old) {
            const t = nw.trim()
            targetRow.setAttribute('data-name', t)
            targetRow.setAttribute('data-sort-key', t)
            const ns = targetRow.querySelector('.node-name')
            if (ns) ns.textContent = t
            const nid = getNodeId(targetRow)
            if (nodeDataMap[nid]) nodeDataMap[nid].name = t
            if (currentRowSelected === targetRow) showDetail(targetRow)
            showToast('重命名成功:' + t, 'success')
          }
        }
        function addChildNode(targetRow) {
          const nt = getNodeType(targetRow)
          if (nt === 'branch-canal') {
            showToast('支渠不能添加子节点', 'warning')
            return
          }
          const nm = prompt('请输入新节点名称:', '新建节点')
          if (!nm || !nm.trim()) return
          const tn = nm.trim()
          let ct, cl, bc, dc, nc
          if (nt === 'area' || nt === 'root') {
            ct = 'main-canal'
            cl = '主渠道'
            bc = 'badge-main'
            dc = 'level-2'
            nc = 'level-2-name'
          } else {
            ct = 'branch-canal'
            cl = '支渠'
            bc = 'badge-branch'
            dc = 'level-3'
            nc = 'level-3-name'
          }
          const nid = ct + '-' + idCounter++
          nodeDataMap[nid] = {
            name: tn,
            type: ct,
            typeLabel: cl,
            location: '待填写',
            area: '待填写',
            established: '待填写',
            waterSource: '待填写',
            annualSupply: '待填写',
            length: '待填写',
            capacity: '待填写',
            coverage: '待填写',
            material: '待填写',
            completed: '待填写',
            description: '新增节点,信息待完善。',
          }
          let container
          if (targetRow.tagName === 'SUMMARY') {
            const det = targetRow.parentElement
            let sub = getSubTree(det)
            if (!sub) {
              sub = document.createElement('div')
              sub.className = 'sub-tree'
              det.appendChild(sub)
            }
            container = sub
          } else container = targetRow.parentElement
          if (ct === 'main-canal') {
            const nd = document.createElement('details')
            nd.className = 'tree-details'
            nd.setAttribute('data-type', ct)
            nd.setAttribute('data-name', tn)
            nd.setAttribute('data-id', nid)
            nd.setAttribute('data-sort-key', tn)
            nd.innerHTML = `<summary data-type="${ct}" data-name="${tn}" data-id="${nid}" data-sort-key="${tn}"><span class="tree-arrow"></span><input type="checkbox" class="tree-checkbox" data-id="${nid}"><span class="node-dot ${dc}"></span><span class="node-name ${nc}">${tn}</span><span class="node-badge ${bc}">${cl}</span></summary><div class="sub-tree"></div>`
            container.appendChild(nd)
            nd.open = true
          } else {
            const nl = document.createElement('div')
            nl.className = 'tree-leaf'
            nl.setAttribute('data-type', ct)
            nl.setAttribute('data-name', tn)
            nl.setAttribute('data-id', nid)
            nl.setAttribute('data-sort-key', tn)
            nl.innerHTML = `<input type="checkbox" class="tree-checkbox" data-id="${nid}"><span class="node-dot ${dc}"></span><span class="node-name ${nc}">${tn}</span><span class="node-badge ${bc}">${cl}</span>`
            container.appendChild(nl)
          }
          if (targetRow.tagName === 'SUMMARY')
            targetRow.parentElement.open = true
          updateSelectedCount()
          showToast('已添加:' + tn, 'success')
        }
        function deleteNode(targetRow) {
          if (targetRow.getAttribute('data-id') === 'root') {
            showToast('不能删除总节点', 'error')
            return
          }
          const nm = getNodeName(targetRow)
          const nt = getNodeType(targetRow)
          if (!confirm(`确定删除「${nm}」吗?`)) return
          const cont = getRowContainer(targetRow)
          if (cont) cont.remove()
          if (currentRowSelected === targetRow) clearRowSelection()
          updateAncestorAfterDelete()
          updateSelectedCount()
          showToast('已删除:' + nm, 'error')
        }
        function updateAncestorAfterDelete() {
          getAllDetails().forEach((details) => {
            const summary = details.querySelector(':scope > summary')
            const cb = summary ? getCheckbox(summary) : null
            if (!cb) return
            const childCbs = getChildCheckboxes(details)
            const total = childCbs.length
            const checked = childCbs.filter((c) => c.checked).length
            const indet = childCbs.filter((c) => c.indeterminate).length
            if (total === 0) setCheckboxState(cb, cb.checked)
            else if (checked === total && indet === 0)
              setCheckboxState(cb, true)
            else if (checked === 0 && indet === 0) setCheckboxState(cb, false)
            else {
              cb.checked = false
              cb.indeterminate = true
            }
          })
        }
        function deleteSelectedNodes() {
          const cbs = getCheckedCheckboxes()
          if (cbs.length === 0) {
            showToast('没有选中的节点', 'warning')
            return
          }
          if (!confirm(`确定删除 ${cbs.length} 个选中节点吗?`)) return
          const rowsToDel = new Set()
          cbs.forEach((cb) => {
            const r = getRowEl(cb)
            if (r && r.getAttribute('data-id') !== 'root') rowsToDel.add(r)
          })
          const topDel = new Set()
          rowsToDel.forEach((row) => {
            let isChild = false
            let cur = row.parentElement
            while (cur && cur !== treeContainer) {
              if (
                cur.classList.contains('sub-tree') &&
                cur.parentElement?.classList.contains('tree-details')
              ) {
                const ps = cur.parentElement.querySelector(':scope>summary')
                if (ps && rowsToDel.has(ps)) {
                  isChild = true
                  break
                }
              }
              cur = cur.parentElement
            }
            if (!isChild) topDel.add(row)
          })
          let cnt = 0
          topDel.forEach((row) => {
            const c = getRowContainer(row)
            if (c) {
              c.remove()
              cnt++
            }
          })
          if (currentRowSelected && !document.contains(currentRowSelected))
            clearRowSelection()
          updateAncestorAfterDelete()
          updateSelectedCount()
          showToast(`已删除 ${cnt} 个节点`, 'error')
        }

        // 搜索
        function performSearch(query) {
          const allRows = getAllRows()
          const q = query.trim().toLowerCase()
          allRows.forEach((r) =>
            r.classList.remove('search-highlight', 'search-hidden'),
          )
          if (!q) {
            searchClear.classList.remove('visible')
            return
          }
          searchClear.classList.add('visible')
          const matched = [],
            mSet = new Set()
          allRows.forEach((r) => {
            if ((r.getAttribute('data-name') || '').toLowerCase().includes(q)) {
              matched.push(r)
              mSet.add(r)
            }
          })
          const toShow = new Set(mSet)
          matched.forEach((r) => {
            let cur = r.parentElement
            while (cur && cur !== treeContainer) {
              if (
                cur.classList.contains('sub-tree') &&
                cur.parentElement?.classList.contains('tree-details')
              ) {
                const s = cur.parentElement.querySelector(':scope>summary')
                if (s) toShow.add(s)
                cur.parentElement.open = true
              }
              cur = cur.parentElement
            }
          })
          allRows.forEach((r) => {
            if (toShow.has(r)) {
              r.classList.remove('search-hidden')
              if (mSet.has(r)) r.classList.add('search-highlight')
            } else r.classList.add('search-hidden')
          })
          if (matched.length === 1) {
            selectRow(matched[0])
            matched[0].scrollIntoView({ behavior: 'smooth', block: 'center' })
          }
        }
        searchInput.addEventListener('input', function () {
          if (searchTimeout) clearTimeout(searchTimeout)
          searchTimeout = setTimeout(() => performSearch(this.value), 200)
        })
        searchClear.addEventListener('click', function () {
          searchInput.value = ''
          performSearch('')
          searchInput.focus()
        })
        searchInput.addEventListener('keydown', function (e) {
          if (e.key === 'Escape') {
            searchInput.value = ''
            performSearch('')
            searchInput.blur()
          }
        })

        // 总节点复选框联动
        rootCheckbox.addEventListener('change', function (e) {
          e.stopPropagation()
          const checked = this.checked
          this.indeterminate = false
          const allCbs = getAllCheckboxes()
          allCbs.forEach((cb) => {
            if (cb !== rootCheckbox) {
              cb.checked = checked
              cb.indeterminate = false
            }
          })
          updateSelectedCount()
        })

        // 事件委托
        treeContainer.addEventListener('click', function (e) {
          const cb = e.target.closest('.tree-checkbox')
          if (cb && cb !== rootCheckbox) {
            e.stopPropagation()
            handleCheckboxChange(cb)
            return
          }
          const row = getRowEl(e.target)
          if (row && !e.target.closest('.tree-checkbox')) selectRow(row)
        })
        treeContainer.addEventListener('contextmenu', function (e) {
          const row = getRowEl(e.target)
          if (row && row.getAttribute('data-id') !== 'root') {
            e.preventDefault()
            showContextMenu(e.clientX, e.clientY, row)
          }
        })
        treeContainer.addEventListener('dblclick', function (e) {
          const row = getRowEl(e.target)
          if (
            row &&
            row.getAttribute('data-id') !== 'root' &&
            !e.target.closest('.tree-checkbox')
          )
            renameNode(row)
        })

        // 总节点工具按钮
        document
          .getElementById('btnExpandAll')
          .addEventListener('click', () => {
            getAllDetails().forEach((d) => (d.open = true))
            showToast('已展开全部', 'success')
          })
        document
          .getElementById('btnCollapseAll')
          .addEventListener('click', () => {
            getAllDetails().forEach((d) => (d.open = false))
            showToast('已折叠全部', 'success')
          })
        document
          .getElementById('btnSelectAll')
          .addEventListener('click', selectAllNodes)
        document
          .getElementById('btnDeselectAll')
          .addEventListener('click', clearAllSelections)
        document
          .getElementById('btnDeleteSelected')
          .addEventListener('click', deleteSelectedNodes)
        clearSelectionLink.addEventListener('click', clearAllSelections)

        // 键盘快捷键
        document.addEventListener('keydown', function (e) {
          if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
            e.preventDefault()
            searchInput.focus()
            searchInput.select()
          }
          if (
            (e.ctrlKey || e.metaKey) &&
            e.key === 'a' &&
            document.activeElement !== searchInput
          ) {
            e.preventDefault()
            selectAllNodes()
          }
          if (
            e.key === 'Delete' &&
            document.activeElement !== searchInput &&
            !e.target.closest('input')
          ) {
            const cbs = getCheckedCheckboxes()
            if (cbs.length > 0) {
              e.preventDefault()
              deleteSelectedNodes()
            }
          }
        })

        // 初始
        updateSelectedCount()
        console.log('✅ 灌区总节点已就绪,所有核心功能集成于总节点一行。')
      })()
    </script>

 

posted @ 2026-07-15 16:43  华腾智算  阅读(1)  评论(0)    收藏  举报
https://damo.alibaba.com/ https://tianchi.aliyun.com/course?spm=5176.21206777.J_3941670930.5.87dc17c9BZNvLL