🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
![](https://box.kancloud.cn/19f471fce14ecfe3338ea6dd6c2e3249_1687x743.png) 客户需要这种左侧树形分类,点击子节点后 显示对应分类下列表的表格 我看海豚自带jstree, 就研究了一下官网demo sitebrowser。 ## 树的问题 ### 树的展示 首先复制一份common下的builder table对应的layout,在js_list那块后面加上树的js ~~~ <script src="__LIBS__/jstree/jstree.min.js"></script> <script> $(document).ready(function(){ var post_url = '{:url("material/operation", [], false, false)}'; $('#jstree').jstree({ core : { 'data' : { url : post_url+'?operation=get_node&prop=1&cid={:input("cid", 0)}', data : function (node) { return { 'id' : node.id }; } }, check_callback : true, themes : { responsive : false }, }, force_text : true, plugins: ["contextmenu", "dnd"], contextmenu: { "items": { "新建菜单": { "label": "新增", "action": function(data) { var ref = $('#jstree').jstree(true), sel = ref.get_selected(); if(!sel.length) { return false; } sel = sel[0]; sel = ref.create_node(sel, { // type: "file", // icon: "glyphicon glyphicon-folder-open", }); if(sel) { ref.edit(sel); } } }, "编辑菜单": { "label": "编辑", "action": function(data) { console.log(data); var ref = $('#jstree').jstree(true), sel = ref.get_selected(); if(!sel.length) { return false; } sel = sel[0]; ref.edit(sel); } }, "删除菜单": { "label": "删除", "action": function(data) { console.log(data); var ref = $('#jstree').jstree(true), sel = ref.get_selected(); if(!sel.length) { return false; } ref.delete_node(sel); } }, }, select_mode:false } }) .on("loaded.jstree", function (event, data) { data.instance.open_node(-1); }) .on('click.jstree', function (e, data) { console.log(e); }) .on('changed.jstree', function (e, data) { console.log(e); console.log(data) if(data && data.selected && data.selected.length) { // 非右键时跳转 if(data.event.which !== 3){ location.href = dolphin.curr_url+'/cid/'+data.selected; } }else { location.href = dolphin.curr_url; } return ; }) .on('delete_node.jstree', function (e, data) { $.get(post_url+'?operation=delete_node', { 'id' : data.node.id }) .fail(function () { data.instance.refresh(); }); }) .on('create_node.jstree', function (e, data) { $.get(post_url+'?operation=create_node', { 'id' : data.node.parent, 'position' : data.position, 'text' : data.node.text }) .done(function (d) { data.instance.set_id(data.node, d.id); }) .fail(function () { data.instance.refresh(); }); }) .on('rename_node.jstree', function (e, data) { $.get(post_url+ '?operation=rename_node', { 'id' : data.node.id, 'text' : data.text }) .fail(function () { data.instance.refresh(); }); }) ; }); </script> ~~~ 这个其实海豚自带的util/Tree类的toLayer方法就能实现 但是遇到一个问题,如何加载树就全展开,试了好多参数都不行,干脆sql查询时加上 state:{opened:true} ### 点击分类后跳转 demo里有change 事件,想法是拿到节点id 自己拼url,然后跳转。 但是遇到问题是,点右键也触发change,看上面代码,排除which == 3 的情况。 ### 右键的定制 从网上找了个文章覆盖 开了右键插件后的 contextmenu 属性 ### 树的节点操作 先将官方的类修改了一通,改成tp5的db操作。不过没有实现剪切、粘贴 ~~~ <?php namespace util; use think\Db; /** * 树结构生成类 * @author CaiWeiMing <314013107@qq.com> */ class JsTree { protected static $instance; /** * 架构函数 * @param array $config */ public function __construct($config = []) { self::$config = array_merge(self::$config, $config); // trace(self::$config); } /** * 配置参数 * @var array */ protected static $config = [ 'table' => '', 'id' => 'id', // id名称 'pid' => 'pid', // pid名称 'left' => 'left', 'right' => 'right', 'level' => 'level', 'title' => 'title', // 标题名称 'child' => 'children', // 子元素键名 'position' => 'pos', 'html' => '┝ ', // 层级标记 'step' => 4, // 层级步进数量 ]; /** * 配置参数 * @param array $config * @return object */ public static function config($config = []) { if (!empty($config)) { $config = array_merge(self::$config, $config); } if (is_null(self::$instance)) { self::$instance = new static($config); } return self::$instance; } /** * 将数据集格式化成层次结构 * @param array/object $lists 要格式化的数据集,可以是数组,也可以是对象 * @param int $pid 父级id * @param int $max_level 最多返回多少层,0为不限制 * @param int $curr_level 当前层数 * @author 蔡伟明 <314013107@qq.com> * @return array */ public static function toLayer($lists = [], $pid = 0, $max_level = 0, $curr_level = 0) { $trees = []; $lists = array_values($lists); foreach ($lists as $key => $value) { if ($value[self::$config['pid']] == $pid) { if ($max_level > 0 && $curr_level == $max_level) { return $trees; } unset($lists[$key]); $child = self::toLayer($lists, $value[self::$config['id']], $max_level, $curr_level + 1); if (!empty($child)) { $value[self::$config['child']] = $child; } $trees[] = $value; } } return $trees; } public function get_node($id, $options = ['with_children'=>true]) { $node = db(self::$config['table'])->where(self::$config['id'], $id)->find(); if(!$node) { throw new \Exception('Node does not exist'); } if(isset($options['with_children'])) { $node['children'] = $this->get_children($id, isset($options['deep_children'])); } if(isset($options['with_path'])) { $node['path'] = $this->get_path($id); } return $node; } public function get_children($id, $recursive = false) { $config = self::$config; if($recursive) { $node = $this->get_node($id); return db(self::$config['table']) ->where($config['left'], 'gt', (int)$node[$config['left']]) ->where($config['right'], 'lt', (int)$node[$config['right']]) ->order($config['left']) ->select(); }else { return db($config['table']) ->where($config['pid'], (int)$id) ->order($config['position']) ->select(); } } public function get_path($id) { $node = $this->get_node($id); $config = self::$config; if($node) { return db($config['table']) ->where($config['left'], 'lt', (int)$node[$config['left']]) ->where($config['right'], 'gt', (int)$node[$confg['right']]) ->order($config['left']) ->select(); }else{ return false; } } public function mk($parent, $position = 0, $data = []) { $parent = (int)$parent; if($parent == 0) { throw new Exception('Parent is 0'); } $parent = $this->get_node($parent, ['with_children'=> true]); if(!$parent['children']) { $position = 0; } if($parent['children'] && $position >= count($parent['children'])) { $position = count($parent['children']); } $sql = []; $par = []; // PREPARE NEW PARENT // update positions of all next elements $config = self::$config; db($config['table'])->where($config['position'], 'gt', $position) ->setInc($config['position'], 1); // update left indexes $ref_lft = false; if(!$parent['children']) { $ref_lft = $parent[$config['right']]; }else if(!isset($parent['children'][$position])) { $ref_lft = $parent[$config['right']]; }else { $ref_lft = $parent['children'][(int)$position][$config['left']]; } db($config['table'])->where($config['left'], 'egt', (int)$ref_lft) ->setInc($config['left'], 2); // update right indexes $ref_rgt = false; if(!$parent['children']) { $ref_rgt = $parent[$config['right']]; }else if(!isset($parent['children'][$position])) { $ref_rgt = $parent[$config['right']]; }else { $ref_rgt = $parent['children'][(int)$position][$config['left']] + 1; } db($config['table'])->where($config['right'], 'egt', (int)$ref_rgt)->setInc($config['right'], 2); $tmp = [ $config['left'] => (int)$ref_lft, $config['right'] => (int)$ref_lft+1, $config['level'] => (int)$parent[$config['level']]+1, $config['pid'] => $parent[$config['id']], $config['position'] => $position, ]; $id = db($config['table'])->insertGetId($tmp); if($data && count($data)) { $node = $id; if(!$this->rn($node, $data)) { $this->rm($node); throw new \Exception('Could not rename after create'); } } return $node; } public function mv($id, $parent, $position = 0) { $id = (int)$id; $parent = (int)$parent; if($parent == 0 || $id == 0 || $id == 1) { throw new \Exception('Cannot move inside 0, or move root node'); } $config = self::$config; $parent = $this->get_node($parent, ['with_children'=> true, 'with_path' => true]); $id = $this->get_node($id, ['with_children'=> true, 'deep_children' => true, 'with_path' => true]); if(!$parent['children']) { $position = 0; } if($id[$config['pid']] == $parent[$config['id']] && $position > $id[$config['position']]) { $position ++; } if($parent['children'] && $position >= count($parent['children'])) { $position = count($parent['children']); } if($id[$config['left']] < $parent[$config['left']] && $id[$config['right']] > $parent[$config['right']]) { throw new \Exception('Could not move parent inside child'); } $tmp = []; $tmp[] = (int)$id[$config['id']]; if($id['children'] && is_array($id['children'])) { foreach($id['children'] as $c) { $tmp[] = (int)$c[$config['id']]; } } $width = (int)$id[$config['right']] - (int)$id[$config['left']] + 1; // PREPARE NEW PARENT // update positions of all next elements db($config['table']) ->where($config['id'], 'neq', (int)$id[$config['id']]) ->where($config['pid'], 'eq', (int)$parent[$config['id']]) ->where($config['position'], 'egt', $position) ->setInc($config['position'], 1); // update left indexes $ref_lft = false; if(!$parent['children']) { $ref_lft = $parent[$config['right']]; }else if(!isset($parent['children'][$position])) { $ref_lft = $parent[$config['right']]; }else { $ref_lft = $parent['children'][(int)$position][$config['left']]; } db($config['table']) ->where($config['left'], 'egt', (int)$ref_lft) ->where($config['id'], 'not in', $tmp) ->setInc($config['left'], $width); // update right indexes $ref_rgt = false; if(!$parent['children']) { $ref_rgt = $parent[$cofnig['right']]; }else if(!isset($parent['children'][$position])) { $ref_rgt = $parent[$cofnig['right']]; }else { $ref_rgt = $parent['children'][(int)$position][$config['left']] + 1; } db($config['table']) ->where($config['right'], 'egt', (int)$ref_rgt) ->where($config['id'], 'not in', $tmp) ->setInc($config['right'], $width); // MOVE THE ELEMENT AND CHILDREN // left, right and level $diff = $ref_lft - (int)$id[$config['left']]; if($diff > 0) { $diff = $diff - $width; } $ldiff = ((int)$parent[$config['level']] + 1) - (int)$id[$config['level']]; db($config['table']) ->where($config['id'], 'in'. $tmp) ->update([ $config['right'] => Db::raw("{$config['right']}+{$diff}"), $config['left'] => Db::raw("{$config['left']}+{$diff}"), $config['level'] => Db::raw("{$config['level']}+{$ldiff}") ]); // position and parent_id db($config['table']) ->where($config['id'], (int)$id[$config['id']]) ->update([ $config['position'] => $position, $config['pid'] => (int)$parent[$config['id']] ]); // CLEAN OLD PARENT // position of all next elements db($config['table']) ->where($config['pid'], (int)$id[$config['pid']]) ->where($config['position'], (int)$config['position']) ->setDec($config['position'], 1); // left indexes db($config['table']) ->where($config['left'], 'gt', (int)$id[$config['right']]) ->where($config['id'], 'not in', $tmp) ->setDec($config['left'], $width); // right indexes db($config['table']) ->where($config['right'], 'gt', (int)$id[$config['right']]) ->where($config['id'], 'not in', $tmp) ->setDec($config['right'], $width); return true; } public function rm($id) { $id = (int)$id; if(!$id || $id === 1) { throw new \Exception('Could not create inside roots'); } $config = self::$config; $data = $this->get_node($id, ['with_children' => true, 'deep_children' => true]); $lft = (int)$data[$config['left']]; $rgt = (int)$data[$config['right']]; $pid = (int)$data[$config['pid']]; $pos = (int)$data[$config['position']]; $dif = $rgt - $lft + 1; // deleting node and its children from structure db($config['table']) ->where($config['left'], 'egt', (int)$lft) ->where($config['right'], '<=', (int)$rgt) ->delete(); // shift left indexes of nodes right of the node db($config['table']) ->where($config['left'], 'gt', (int)$rgt) ->setDec($config['left'], (int)$dif); // shift right indexes of nodes right of the node and the node's parents db($config['table']) ->where($config['right'], 'gt', (int)$lft) ->setDec($config['right'], (int)$dif); // Update position of siblings below the deleted node db($config['table']) ->where($config['pid'], $pid) ->where($config['position'], 'gt', (int)$pos) ->setDec($config['position'], 1); return true; } public function rn($id, $data) { $config = self::$config; if(!$exist = db($config['table'])->find($id)) { throw new \Exception('Could not rename non-existing node'); } $data = array_merge($exist, $data); $ret = db($config['table'])->insertAll([$data], true); if(false === $ret){ throw new \Exception('Could not rename'); } return true; } } ~~~ 然后一个控制的方法实现operation ~~~ public function operation(){ $fs = new JsTree(['table' => 'document_category', 'title'=>'text']); if(isset($_GET['operation'])) { try { $rslt = null; switch($_GET['operation']) { case 'get_node': $node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0; if($node == 0){ $list = db('document_category')->field('*,title text')->where('prop', $_GET['prop'])->select(); if($list){ foreach ($list as &$li) { $li['state']['opened'] = true; $cid = input('cid', 0); if($cid && $cid == $li['id']){ $li['state']['selected'] = true; } } } $rslt = JsTree::config(['child'=>'children', 'title'=>'text'])->toLayer($list); }else{ $temp = $fs->get_children($node); $rslt = []; foreach($temp as $v) { $rslt[] = [ 'id' => $v['id'], 'text' => $v['text'], 'children' => ($v['right'] - $v['left'] > 1) ]; } } break; case 'create_node': $node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0; $temp = $fs->mk($node, isset($_GET['position']) ? (int)$_GET['position'] : 0, ['nm' => isset($_GET['text']) ? $_GET['text'] : 'New node']); $rslt = ['id' => $temp]; break; case 'rename_node': $node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0; $rslt = $fs->rn($node, ['title' => isset($_GET['text']) ? $_GET['text'] : 'Renamed node']); break; case 'delete_node': $node = isset($_GET['id']) && $_GET['id'] !== '#' ? (int)$_GET['id'] : 0; $rslt = $fs->rm($node); break; default: throw new \Exception('Unsupported operation: ' . $_GET['operation']); break; } header('Content-Type: application/json; charset=utf-8'); return json($rslt); }catch (\Exception $e) { header($_SERVER["SERVER_PROTOCOL"] . ' 500 Server Error'); header('Status: 500 Server Error'); return $e->getMessage().PHP_EOL.$e->getTraceAsString(); } } } ~~~ 对应表结构 ```[sql] CREATE TABLE `document_category` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `pid` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '上级菜单id', `title` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '菜单标题', `prop` int(1) UNSIGNED NULL DEFAULT 1 COMMENT '1-招标 2-投标', `icon` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '菜单图标', `online_hide` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '网站上线后是否隐藏', `create_time` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', `update_time` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', `status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态', `left` int(10) UNSIGNED NOT NULL DEFAULT 0, `right` int(10) UNSIGNED NOT NULL DEFAULT 0, `level` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '层级', `pos` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '位置 相当于顺序', PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM AUTO_INCREMENT = 18 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; ``` 前端部分:on相关事件,直接参照官方示列。一定要实现get_node方法。