|
@@ -0,0 +1,242 @@
|
|
|
+(() => {
|
|
|
+ // 注入样式
|
|
|
+ const style = document.createElement('style');
|
|
|
+ style.textContent = `
|
|
|
+.tree-select-menu {
|
|
|
+ max-height: 500px;
|
|
|
+ overflow-y: auto;
|
|
|
+ min-width: 250px;
|
|
|
+ font-family: Arial, sans-serif;
|
|
|
+ font-size: 12px;
|
|
|
+ user-select: none;
|
|
|
+ border: 1px solid #dee2e6;
|
|
|
+ padding-left: 4px;
|
|
|
+}
|
|
|
+.tree-node {
|
|
|
+ cursor: default;
|
|
|
+ padding: 4px 8px;
|
|
|
+ display: block;
|
|
|
+ white-space: nowrap;
|
|
|
+ position: relative;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+.tree-node::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0px;
|
|
|
+ bottom: 0;
|
|
|
+ width: 1px;
|
|
|
+ border-left: 1px dashed #ccc;
|
|
|
+}
|
|
|
+.tree-node:not(.leaf) [data-level="0"] >.tree-label {
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+.tree-node[data-level="0"]::before,
|
|
|
+.tree-node[data-level="0"]::after {
|
|
|
+ display: none !important;
|
|
|
+}
|
|
|
+.tree-node::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 14px;
|
|
|
+ left: 0px;
|
|
|
+ width: 10px;
|
|
|
+ height: 1px;
|
|
|
+ background-color: #ccc;
|
|
|
+}
|
|
|
+.tree-node:last-child::before {
|
|
|
+ height: 14px;
|
|
|
+}
|
|
|
+.tree-node.leaf {
|
|
|
+ cursor: pointer;
|
|
|
+ color: #007bff;
|
|
|
+ background-color: #fff;
|
|
|
+}
|
|
|
+.tree-node.leaf:hover {
|
|
|
+ background-color: #e9ecef;
|
|
|
+}
|
|
|
+.tree-label {
|
|
|
+ display: inline-block;
|
|
|
+ vertical-align: middle;
|
|
|
+}
|
|
|
+.tree-toggle {
|
|
|
+ display: inline-block;
|
|
|
+ width: 14px;
|
|
|
+ text-align: center;
|
|
|
+ margin-right: 6px;
|
|
|
+ user-select: none;
|
|
|
+ font-size: 12px;
|
|
|
+ vertical-align: middle;
|
|
|
+}
|
|
|
+.tree-children {
|
|
|
+ display: none;
|
|
|
+ margin-left: 18px;
|
|
|
+ padding-left: 8px;
|
|
|
+}
|
|
|
+.tree-expanded > .tree-children {
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.tree-select button {
|
|
|
+ color: #495057;
|
|
|
+ border: 1px solid #ced4da;
|
|
|
+ padding: .25rem .5rem;
|
|
|
+}
|
|
|
+.tree-select button:active {
|
|
|
+ color: #6c757d !important;
|
|
|
+ border: 1px solid #ced4da;
|
|
|
+ background-color: #fff !important;
|
|
|
+}
|
|
|
+#tree-container .show>.btn-outline-secondary.dropdown-toggle {
|
|
|
+ color: #6c757d !important;
|
|
|
+ border: 1px solid #80bdff;
|
|
|
+ background-color: #fff !important;
|
|
|
+ box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
|
|
|
+}
|
|
|
+#tree-container .show>.btn-outline-secondary.dropdown-toggle:focus {
|
|
|
+ border-color: #80bdff;
|
|
|
+ box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
|
|
|
+}
|
|
|
+.tree-select .btn-outline-secondary:hover {
|
|
|
+ color: #6c757d;
|
|
|
+ border: 1px solid #ced4da;
|
|
|
+ background-color: #fff;
|
|
|
+}
|
|
|
+.tree-select .btn-outline-secondary:focus {
|
|
|
+ border-color: #80bdff;
|
|
|
+ box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
|
|
|
+}
|
|
|
+`;
|
|
|
+ document.head.appendChild(style);
|
|
|
+
|
|
|
+ class TreeSelect {
|
|
|
+ constructor({ element, data, selected, onSelect }) {
|
|
|
+ this.container = typeof element === 'string' ? document.querySelector(element) : element;
|
|
|
+ this.onSelect = onSelect;
|
|
|
+ this.selectedId = selected;
|
|
|
+ this.data = this.buildTree(data);
|
|
|
+ this.selectedNode = this.findNodeById(this.data, this.selectedId);
|
|
|
+ this.render();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ✅ flat → tree 转换函数
|
|
|
+ buildTree(data, parentId = '-1') {
|
|
|
+ // 找出所有属于 parentId 的节点
|
|
|
+ const children = data
|
|
|
+ .filter(item => item.tree_pid === parentId)
|
|
|
+ .sort((a, b) => a.tree_order - b.tree_order); // ✅ 按 tree_order 升序排序
|
|
|
+
|
|
|
+ return children.map(item => {
|
|
|
+ const nodeChildren = this.buildTree(data, item.id);
|
|
|
+ return {
|
|
|
+ id: item.id,
|
|
|
+ label: item.name,
|
|
|
+ selected: item.id === this.selectedId,
|
|
|
+ children: nodeChildren.length ? nodeChildren : undefined
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ findNodeById(nodes, id) {
|
|
|
+ for (const node of nodes) {
|
|
|
+ if (node.id === id) return node;
|
|
|
+ if (node.children) {
|
|
|
+ const found = this.findNodeById(node.children, id);
|
|
|
+ if (found) return found;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ render() {
|
|
|
+ this.container.innerHTML = `
|
|
|
+ <div class="dropdown tree-select">
|
|
|
+ <button class="btn btn-sm btn-outline-secondary w-100 dropdown-toggle text-left" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
|
+ ${this.selectedNode ? this.selectedNode.label : '请选择分类'}
|
|
|
+ </button>
|
|
|
+ <div class="dropdown-menu tree-select-menu"></div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ this.button = this.container.querySelector('button');
|
|
|
+ this.menu = this.container.querySelector('.dropdown-menu');
|
|
|
+ this.button.dataset.selectedId = this.selectedNode?.id || '';
|
|
|
+ this.renderTree(this.data, this.menu, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ renderTree(nodes, parentEl, level = 0) {
|
|
|
+ nodes.forEach(node => {
|
|
|
+ const wrapper = document.createElement('div');
|
|
|
+ wrapper.className = 'tree-node tree-expanded'; // 默认展开
|
|
|
+ wrapper.dataset.level = level;
|
|
|
+
|
|
|
+ const isLeaf = !node.children || node.children.length === 0;
|
|
|
+ if (isLeaf) wrapper.classList.add('leaf');
|
|
|
+ const toggle = document.createElement('span');
|
|
|
+ toggle.className = 'tree-toggle';
|
|
|
+ toggle.innerHTML = isLeaf ? '' : '<i class="fa fa-minus-square-o"></i>';
|
|
|
+
|
|
|
+ if (!isLeaf) {
|
|
|
+ wrapper.appendChild(toggle);
|
|
|
+ const toggle2 = document.createElement('span');
|
|
|
+ toggle2.className = 'tree-folder';
|
|
|
+ toggle2.innerHTML = '<i class="fa fa-folder-o"></i> ';
|
|
|
+ wrapper.appendChild(toggle2);
|
|
|
+ }
|
|
|
+
|
|
|
+ const label = document.createElement('span');
|
|
|
+ label.className = 'tree-label';
|
|
|
+ label.innerHTML = isLeaf ? ' ' + node.label : node.label;
|
|
|
+ wrapper.appendChild(label);
|
|
|
+
|
|
|
+ if (!isLeaf) {
|
|
|
+ const childrenContainer = document.createElement('div');
|
|
|
+ childrenContainer.className = 'tree-children';
|
|
|
+ wrapper.appendChild(childrenContainer);
|
|
|
+ this.renderTree(node.children, childrenContainer, level + 1);
|
|
|
+
|
|
|
+ if (this.isNodeOrChildSelected(node)) {
|
|
|
+ wrapper.classList.add('tree-expanded');
|
|
|
+ toggle.innerHTML = '<i class="fa fa-minus-square-o"></i>';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 👇 点击整行展开/收起
|
|
|
+ wrapper.addEventListener('click', e => {
|
|
|
+ e.stopPropagation();
|
|
|
+ const expanded = wrapper.classList.toggle('tree-expanded');
|
|
|
+ toggle.innerHTML = expanded ? '<i class="fa fa-minus-square-o"></i>' : '<i class="fa fa-plus-square-o"></i>';
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 👇 叶子节点可选中
|
|
|
+ wrapper.addEventListener('click', e => {
|
|
|
+ e.stopPropagation();
|
|
|
+ this.selectNode(node);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ parentEl.appendChild(wrapper);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ isNodeOrChildSelected(node) {
|
|
|
+ if (node.selected) return true;
|
|
|
+ if (!node.children) return false;
|
|
|
+ return node.children.some(child => this.isNodeOrChildSelected(child));
|
|
|
+ }
|
|
|
+
|
|
|
+ selectNode(node) {
|
|
|
+ this.selectedNode = node;
|
|
|
+ this.button.textContent = node.label;
|
|
|
+ this.button.dataset.selectedId = node.id;
|
|
|
+ $('.tree-select button').dropdown('toggle');
|
|
|
+ if (typeof this.onSelect === 'function') {
|
|
|
+ this.onSelect(node.id, node.label);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getValue() {
|
|
|
+ return this.button.dataset.selectedId || null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ window.TreeSelect = TreeSelect;
|
|
|
+})();
|