فهرست منبع

切换项目树结构控件编写和调整

ellisran 5 روز پیش
والد
کامیت
93aaf554e6

+ 2 - 1
app/controller/sub_proj_setting_controller.js

@@ -560,7 +560,8 @@ module.exports = app => {
                 }).filter(x => { return x.groupList.length > 0; });
 
                 // 获取项目列表
-                const subProjects = await ctx.service.subProject.getAllDataByCondition({ where: { project_id: projectId, is_folder: 0, is_delete: 0 } });
+                // const subProjects = await ctx.service.subProject.getAllDataByCondition({ where: { project_id: projectId, is_folder: 0, is_delete: 0 } });
+                const subProjects = await ctx.service.subProject.getSubProject(projectId, ctx.session.sessionUser.accountId, ctx.session.sessionUser.is_admin);
                 const renderData = {
                     projectData,
                     tenderList,

+ 242 - 0
app/public/js/bootstrap/tree-select.js

@@ -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 ? '&nbsp; ' + 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;
+})();

+ 39 - 14
app/public/js/setting_manage.js

@@ -744,20 +744,20 @@ $(document).ready(() => {
         $('#no-project-tenders input').prop('checked', checked);
     });
 
-    $('#change-sp').change(function () {
-        const spid = $(this).val();
-        console.log(spid);
-        postData(`/sp/${spid}/setting/manage/tender/save`, { type: 'get-category', spid }, function (result) {
-            $('#cate-list', '#edit-bd').html(getCategoryHtml(result));
-            const tender = _.find(tenders, { id: cur_tenderid });
-            if (tender && tender.spid === spid) {
-                for (const c of tender.category) {
-                    // $('input[value=' + c.value + ']', '#edit-bd').prop('checked', 'checked');
-                    $('option[value=' + c.value + ']', '#edit-bd').prop('selected', true);
-                }
-            }
-        });
-    })
+    // $('#change-sp').change(function () {
+    //     const spid = $(this).val();
+    //     console.log(spid);
+    //     postData(`/sp/${spid}/setting/manage/tender/save`, { type: 'get-category', spid }, function (result) {
+    //         $('#cate-list', '#edit-bd').html(getCategoryHtml(result));
+    //         const tender = _.find(tenders, { id: cur_tenderid });
+    //         if (tender && tender.spid === spid) {
+    //             for (const c of tender.category) {
+    //                 // $('input[value=' + c.value + ']', '#edit-bd').prop('checked', 'checked');
+    //                 $('option[value=' + c.value + ']', '#edit-bd').prop('selected', true);
+    //             }
+    //         }
+    //     });
+    // })
 
     $('#filter-tender-ok').click(function() {
         const updateData = [];
@@ -774,6 +774,31 @@ $(document).ready(() => {
             window.location.reload();
         });
     });
+
+    $('#edit-bd').on('show.bs.modal', function () {
+
+        const tree = new TreeSelect({
+            element: '#tree-container',
+            data: subProjects,
+            selected: spid,
+            onSelect: (id, label) => {
+                const newSpid = id;
+                console.log(newSpid);
+                $('#change-sp').val(newSpid);
+                postData(`/sp/${newSpid}/setting/manage/tender/save`, { type: 'get-category', spid: newSpid }, function (result) {
+                    $('#cate-list', '#edit-bd').html(getCategoryHtml(result));
+                    const tender = _.find(tenders, { id: cur_tenderid });
+                    if (tender && tender.spid === newSpid) {
+                        for (const c of tender.category) {
+                            // $('input[value=' + c.value + ']', '#edit-bd').prop('checked', 'checked');
+                            $('option[value=' + c.value + ']', '#edit-bd').prop('selected', true);
+                        }
+                    }
+                });
+                // console.log('选中了:', id, label);
+            }
+        });
+    });
 });
 
 const tenderListSpec = (function(){

+ 3 - 0
app/view/sp_setting/manage.ejs

@@ -338,6 +338,7 @@
         </div>
     </div>
 </div>
+<!--<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>-->
 <script>
     const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
     const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
@@ -349,6 +350,8 @@
     const measureType = JSON.parse('<%- JSON.stringify(measureType) %>');
     const auditType = JSON.parse(unescape('<%- escape(JSON.stringify(auditType)) %>'));
     const scPermission = JSON.parse(unescape('<%- escape(JSON.stringify(scPermission)) %>'));
+    const subProjects = JSON.parse(unescape('<%- escape(JSON.stringify(subProjects)) %>'));
+    console.log(subProjects);
     const uid = '<%- uid %>';
     const pid = '<%- pid %>';
     const uphlname = 'user_' + uid + '_pro_' + pid + '_tender_manage_list';

+ 2 - 5
app/view/sp_setting/manage_modal.ejs

@@ -226,11 +226,8 @@
             <div class="modal-body">
                 <div class="form-group">
                     <label>所属项目<b class="text-danger">*</b></label>
-                    <select class="form-control form-control-sm" name="spid" id="change-sp">
-                        <% for (const sp of subProjects) { %>
-                        <option value="<%- sp.id %>"><%- sp.name %></option>
-                        <% } %>
-                    </select>
+                    <div id="tree-container"></div>
+                    <input type="hidden" name="spid" id="change-sp">
                 </div>
                 <div class="form-group">
                     <label>标段名称<b class="text-danger">*</b></label>

+ 2 - 0
config/web.js

@@ -1610,6 +1610,8 @@ const JsFiles = {
                     '/public/js/shares/show_level.js',
                     '/public/js/tender_showhide.js',
                     '/public/js/shenpi.js',
+                    '/public/js/popper/popper.min.js',
+                    '/public/js/bootstrap/tree-select.js',
                     '/public/js/setting_manage.js',
                     '/public/js/tender_list_base.js',
                 ],