kartik Tree Manager 是作为一个模块提供的,所以,观察其源代码,有模块入口文件 Module.php,widget组件包括了TreeView和TreeViewInput,前者用来展示和管理树形节点,后者是可以树形方式选择节点作为输入。
TreeView这个widget,不仅在左边显示树形节点,而且在右边以表单形式显示当前选中节点的详情,表单视图用分部视图_form提供,并且它是可扩展的(更多的表单输入和更多的详情展示)。
Tree Manager 提供了完整的数据迁移(创建树形结构的数据库表),支持嵌套集合方式管理树形节点的模型类,管理节点用到的控制器。
示例如下(以部门为例,字段只增加部门名称和创建/修改时间戳):
创建数据迁移,添加 safeUp如下
public function safeUp()
{
$this->createTable('department', [
'id' => $this->primaryKey(),
'name' => $this->string(64)->notNull()->comment('部门名称'), // 除了 name,其余字段都是 kartik要求的
'root' => $this->integer(), // 不要为 root 等后面的字段生成验证规则,因为都是内部字段
'lft' => $this->integer()->notNull(),
'rgt' => $this->integer()->notNull(),
'lvl' => $this->smallInteger(5)->notNull(),
'icon' => $this->string(255),
'icon_type' => $this->smallInteger(1)->notNull()->defaultValue(1),
'active' => $this->boolean()->notNull()->defaultValue(true),
'selected' => $this->boolean()->notNull()->defaultValue(false),
'disabled' => $this->boolean()->notNull()->defaultValue(false),
'readonly' => $this->boolean()->notNull()->defaultValue(false),
'visible' => $this->boolean()->notNull()->defaultValue(true),
'collapsed' => $this->boolean()->notNull()->defaultValue(false),
'movable_u' => $this->boolean()->notNull()->defaultValue(true),
'movable_d' => $this->boolean()->notNull()->defaultValue(true),
'movable_l' => $this->boolean()->notNull()->defaultValue(true),
'movable_r' => $this->boolean()->notNull()->defaultValue(true),
'removable' => $this->boolean()->notNull()->defaultValue(true),
'removable_all' => $this->boolean()->notNull()->defaultValue(false),
'child_allowed' => $this->boolean()->notNull()->defaultValue(true),
'created_at' => $this->integer()->comment('创建时间'),
'updated_at' => $this->integer()->comment('修改时间'),
]);
$this->addCommentOnTable('department', '部门');
$this->createIndex('fk_department_NK1', 'department', 'root');
$this->createIndex('fk_department_NK2', 'department', 'lft');
$this->createIndex('fk_department_NK3', 'department', 'rgt');
$this->createIndex('fk_department_NK4', 'department', 'lvl');
$this->createIndex('fk_department_NK5', 'department', 'active');
}
创建模型,并且让它从 Tree Manager 提供的模型类 Tree 继承(Tree 本身是从 ActiveRecord继承的,所以,可以直接套用gii生成的代码,但是有几个注意点:不要绝大部分字段的验证规则,因为它们都是内部工作的,另外,如果使用新的behavior,必须合并父类Tree的behavior!否则嵌套集合的操作无法使用,因为NestedSetsBehavior是作为TreeTrait嵌入到Tree类的)
public function behaviors()
{
return array_merge(parent::behaviors(), [TimestampBehavior::class]); // 必须合并父类的行为
}
public function rules()
{
return [
[['name'], 'required'],
[['created_at', 'updated_at'], 'integer'],
[['name'], 'string', 'max' => 64],
];
}
为了能用控制台操作节点,我们在控制台console应用中新建一个模型Node,它从Department派生,并且为其添加嵌套集合管理的行为(注意:creocoder中用来表示深度的字段和kartik的不同,必须明确重新定义)。这么做的原因是,默认的Tree依赖Tree Manager模块,它是web方式的模块,控制台不能工作。
<?php
namespace console\models;
use common\models\Department;
use creocoder\nestedsets\NestedSetsBehavior;
class Node extends Department
{
public function behaviors()
{
return [
'tree' => [
'class' => NestedSetsBehavior::class,
// 'treeAttribute' => 'tree',
// 'leftAttribute' => 'lft',
// 'rightAttribute' => 'rgt',
'depthAttribute' => 'lvl', // kartik 修改了 creocoder 此字段名
],
];
}
}
创建新的数据迁移,用Node模型类来添加一些节点
public function safeUp()
{
// 添加 节点数据
$root = new Node(['name' => '全国NK']); // Node继承自 Department, 必须先创建 Department模型 才能进行此迁移
$root->makeRoot();
$guangxi = new Node(['name' => '广西NK']);
$guangxi->appendTo($root);
$yongxing = new Node(['name' => 'YX畜牧']);
$yongxing->appendTo($guangxi);
$admins = new Node(['name' => 'admins']);
$admins->appendTo($yongxing);
$deptNames = ['领导班子成员', '生产技术部', '兽医技术中心', '育种部', '产业化', '猪场'];
foreach ($deptNames as $name) {
$dept = new Node(['name' => $name]);
$dept->appendTo($yongxing);
}
$models = Node::find()->orderBy('id')->all();
echo "\nAll nodes as follows:\n";
echo "ID\tName\tRoot\tLeft\tRight\tLevel\n";
foreach ($models as $model) {
echo $model->id."\t".$model->name."\t".$model->root."\t".$model->lft."\t".$model->rgt."\t".$model->lvl."\n";
}
// 获得 指定名字 的节点 对应id
...........
// 建立用户到部门的外键联系
$this->update('user', ['department_id' => $deptNameId['admins']]); //此前添加的用户必然是管理员
$this->addForeignKey(
'fk-user-department_id',
'user',
'department_id',
'department',
'id',
'CASCADE'
);
// 导入用户对应部门的数据
...............
}
在应用中为Tree Manager配置模块,从而该模块可以帮我们管理树形节点(相当于树形节点的操作可以被NodeController的有关动作处理)。在common/config/main.php添加下面代码的treemanager部分
'modules' => [
'gridview' => [
'class' => '\kartik\grid\Module',
],
'treemanager' => [
'class' => '\kartik\tree\Module',
// other module settings, refer detailed documentation
]
],
接下来就可以设置控制器并在视图中用TreeView来显示树形节点了(记住 class DepartmentController extends NodeController // 从 NodeController继承)
public function actionIndex()
{
$rootDept = Department::findOne(['lvl' => 0]); // 0 = 全国NK
$yxDept = Department::findOne(['lvl' => 2]); // YX畜牧
$seeDept = $yxDept;
$currentDept = Yii::$app->session->get('current_dept', $seeDept);
// add children and itself
$query = $seeDept ? $seeDept->children()->orWhere(['id' => $seeDept->id])
->andFilterCompare('name', 'admins', '<>')
->addOrderBy('root, lft') :
Department::find();
return $this->render('index', [
'query' => $query,
'currentDept' => $currentDept,
'rootDept' => $rootDept
]);
}
<div class="department-index">
<?= TreeView::widget([
'query' => $query,
'headingOptions' => ['label' => '部门'],
'breadcrumbs' => [
'depth' => '2',
],
'allowNewRoots' => $rootDept == null,
'nodeView' => '@kvtree/views/_form', // 此为默认 form
'showIDAttribute' => false, // 不显示 ID
'iconEditSettings' => ['show' => 'none'], // 取消 图标 编辑
'showFormButtons' => true,
'nodeAddlViews' => [
\kartik\tree\Module::VIEW_PART_5 => '@backend/views/department/_grid',
],
'nodeActions' => [
\kartik\tree\Module::NODE_REMOVE => \yii\helpers\Url::to(['remove']),
],
'fontAwesome' => true, // 图标选择
'isAdmin' => false, // optional (toggle to enable admin mode)
'rootOptions' => ['label' => '您可以查阅的范围'],
'displayValue' => $currentDept->id, // initial display value
'toolbar' => [
// TreeView::BTN_REFRESH => true,
// TreeView::BTN_CREATE => true,
// TreeView::BTN_CREATE_ROOT => true,
// TreeView::BTN_REMOVE => true,
// TreeView::BTN_MOVE_UP => false,
// TreeView::BTN_MOVE_DOWN => false,
TreeView::BTN_MOVE_LEFT => false,
TreeView::BTN_MOVE_RIGHT => false,
TreeView::BTN_SEPARATOR => false,
],
'treeOptions' => ['style' => 'height: 500px'],
'detailOptions' => ['style' => 'height: 590px'],
'softDelete' => true, // defaults to true
'cacheSettings' => [
'enableCache' => true // defaults to true
]
])
?>
</div>
我们使用了默认的详情表单,并为详情表单添加了额外的分部视图_grid(当点击节点时,总是会post一堆数据,我们可以利用post的数据,在详情表单显示所需的数据)
$posted = Yii::$app->request->post();
//var_dump($posted);
$users = null;
if (array_key_exists('id', $posted)) {
$currentDeptId = $posted['id'];
$users = User::find()->where(['department_id' => $currentDeptId])->all();
}
?>
<div>部门人员:</div>
<?php if (empty($users)) : ?>
<div class="text-danger">(无)</div>
<?php else: ?>
<div class="card">
<div class="card-body">
<?php foreach ($users as $user) : ?>
<span class="badge badge-info"><?= $user->name . '(' . $user->username . ')' ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
对于移除节点,我们在DepartmentController中添加了对应的action,它是从NodeController的对应代码抄过来略加改写的(即实现override),防止部门下有用户时删除部门
public function actionRemove() // 从 kartik 源码改写
{
static::checkValidRequest();
$data = static::getPostData();
$nodeTitles = TreeSecurity::getNodeTitles($data);
$callback = function () use ($data) {
$id = ArrayHelper::getValue($data, 'id', null);
$parsedData = TreeSecurity::parseRemoveData($data);
$out = $parsedData['out'];
$oldHash = $parsedData['oldHash'];
$newHash = $parsedData['newHash'];
/**
* @var Tree $treeClass
* @var Tree $node
*/
$treeClass = $out['treeClass'];
TreeSecurity::checkSignature('remove', $oldHash, $newHash);
/**
* @var Tree $node
*/
$node = $treeClass::findOne($id);
return $node->canDelete() && $node->removeNode($out['softDelete']);
};
return self::process($callback, '移除节点时发生错误(可能节点对应记录非空)。请稍后再试。', '节点删除成功');
}
如果详情表单中用GridView来显示数据,要注意,GridView的分页组件,默认的基础目录并非我们的控制器,而是TreeView默认的那些action,所以用GridView显示数据,需要自己“管理”节点(自己写actionManage)。可以自己参考这个帖子 https://www.yiichina.com/tutorial/653
这里说的坑,就是
'breadcrumbs' => [
'depth' => '2',
],
这里的'2',如果设置为2,就会遇到问题。因为TreeSecurity组件初始用TreeView设置的值来计算hash值(我们设置为2,就是整形),后期用post传递的值计算hash值,post时,2总是字符串类型,两种类型在json并hash后,得到的是不同结果,就会遭遇签名错误(给作者github提了个issue,不知道作者会修改代码还是修改使用文档说明)。