![](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方法。