Menu.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 海豚PHP框架 [ DolphinPHP ]
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2016~2019 广东卓锐软件有限公司 [ http://www.zrthink.com ]
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: http://dolphinphp.com
  8. // +----------------------------------------------------------------------
  9. namespace app\admin\controller;
  10. use app\common\builder\ZBuilder;
  11. use app\admin\model\Module as ModuleModel;
  12. use app\admin\model\Menu as MenuModel;
  13. use app\user\model\Role as RoleModel;
  14. use think\facade\Cache;
  15. /**
  16. * 节点管理
  17. * @package app\admin\controller
  18. */
  19. class Menu extends Admin
  20. {
  21. /**
  22. * 节点首页
  23. * @param string $group 分组
  24. * @author 蔡伟明 <314013107@qq.com>
  25. * @return mixed
  26. * @throws \Exception
  27. */
  28. public function index($group = 'admin')
  29. {
  30. // 保存模块排序
  31. if ($this->request->isPost()) {
  32. $modules = $this->request->post('sort/a');
  33. if ($modules) {
  34. $data = [];
  35. foreach ($modules as $key => $module) {
  36. $data[] = [
  37. 'id' => $module,
  38. 'sort' => $key + 1
  39. ];
  40. }
  41. $MenuModel = new MenuModel();
  42. if (false !== $MenuModel->saveAll($data)) {
  43. $this->success('保存成功');
  44. } else {
  45. $this->error('保存失败');
  46. }
  47. }
  48. }
  49. cookie('__forward__', $_SERVER['REQUEST_URI']);
  50. // 配置分组信息
  51. $list_group = MenuModel::getGroup();
  52. foreach ($list_group as $key => $value) {
  53. $tab_list[$key]['title'] = $value;
  54. $tab_list[$key]['url'] = url('index', ['group' => $key]);
  55. }
  56. // 模块排序
  57. if ($group == 'module-sort') {
  58. $map['status'] = 1;
  59. $map['pid'] = 0;
  60. $modules = MenuModel::where($map)->order('sort,id')->column('icon,title', 'id');
  61. $this->assign('modules', $modules);
  62. } else {
  63. // 获取节点数据
  64. $data_list = MenuModel::getMenusByGroup($group);
  65. $max_level = $this->request->get('max', 0);
  66. $this->assign('menus', $this->getNestMenu($data_list, $max_level));
  67. }
  68. $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]);
  69. $this->assign('page_title', '节点管理');
  70. return $this->fetch();
  71. }
  72. /**
  73. * 新增节点
  74. * @param string $module 所属模块
  75. * @param string $pid 所属节点id
  76. * @author 蔡伟明 <314013107@qq.com>
  77. * @return mixed
  78. * @throws \Exception
  79. */
  80. public function add($module = 'admin', $pid = '')
  81. {
  82. // 保存数据
  83. if ($this->request->isPost()) {
  84. $data = $this->request->post('', null, 'trim');
  85. // 验证
  86. $result = $this->validate($data, 'Menu');
  87. // 验证失败 输出错误信息
  88. if(true !== $result) $this->error($result);
  89. // 顶部节点url检查
  90. if ($data['pid'] == 0 && $data['url_value'] == '' && ($data['url_type'] == 'module_admin' || $data['url_type'] == 'module_home')) {
  91. $this->error('顶级节点的节点链接不能为空');
  92. }
  93. if ($menu = MenuModel::create($data)) {
  94. // 自动创建子节点
  95. if ($data['auto_create'] == 1 && !empty($data['child_node'])) {
  96. unset($data['icon']);
  97. unset($data['params']);
  98. $this->createChildNode($data, $menu['id']);
  99. }
  100. // 添加角色权限
  101. if (isset($data['role'])) {
  102. $this->setRoleMenu($menu['id'], $data['role']);
  103. }
  104. Cache::clear();
  105. // 记录行为
  106. $details = '所属模块('.$data['module'].'),所属节点ID('.$data['pid'].'),节点标题('.$data['title'].'),节点链接('.$data['url_value'].')';
  107. action_log('menu_add', 'admin_menu', $menu['id'], UID, $details);
  108. $this->success('新增成功', cookie('__forward__'));
  109. } else {
  110. $this->error('新增失败');
  111. }
  112. }
  113. // 使用ZBuilder快速创建表单
  114. return ZBuilder::make('form')
  115. ->setPageTitle('新增节点')
  116. ->addLinkage('module', '所属模块', '', ModuleModel::getModule(), $module, url('ajax/getModuleMenus'), 'pid')
  117. ->addFormItems([
  118. ['select', 'pid', '所属节点', '所属上级节点', MenuModel::getMenuTree(0, '', $module), $pid],
  119. ['text', 'title', '节点标题'],
  120. ['radio', 'url_type', '链接类型', '', ['module_admin' => '模块链接(后台)', 'module_home' => '模块链接(前台)', 'link' => '普通链接'], 'module_admin']
  121. ])
  122. ->addFormItem(
  123. 'text',
  124. 'url_value',
  125. '节点链接',
  126. "可留空,如果是模块链接,请填写<code>模块/控制器/操作</code>,如:<code>admin/menu/add</code>。如果是普通链接,则直接填写url地址,如:<code>http://www.dolphinphp.com</code>"
  127. )
  128. ->addText('params', '参数', '如:a=1&b=2')
  129. ->addSelect('role', '角色', '除超级管理员外,拥有该节点权限的角色', RoleModel::where('id', 'neq', 1)->column('id,name'), '', 'multiple')
  130. ->addRadio('auto_create', '自动添加子节点', '选择【是】则自动添加指定的子节点', ['否', '是'], 0)
  131. ->addCheckbox('child_node', '子节点', '仅上面选项为【是】时起作用', ['add' => '新增', 'edit' => '编辑', 'delete' => '删除', 'enable' => '启用', 'disable' => '禁用', 'quickedit' => '快速编辑'], 'add,edit,delete,enable,disable,quickedit')
  132. ->addRadio('url_target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self')
  133. ->addIcon('icon', '图标', '导航图标')
  134. ->addRadio('online_hide', '网站上线后隐藏', '关闭开发模式后,则隐藏该菜单节点', ['否', '是'], 0)
  135. ->addText('sort', '排序', '', 100)
  136. ->setTrigger('auto_create', '1', 'child_node', false)
  137. ->fetch();
  138. }
  139. /**
  140. * 编辑节点
  141. * @param int $id 节点ID
  142. * @author 蔡伟明 <314013107@qq.com>
  143. * @return mixed
  144. * @throws \Exception
  145. * @throws \think\db\exception\DataNotFoundException
  146. * @throws \think\db\exception\ModelNotFoundException
  147. * @throws \think\exception\DbException
  148. */
  149. public function edit($id = 0)
  150. {
  151. if ($id === 0) $this->error('缺少参数');
  152. // 保存数据
  153. if ($this->request->isPost()) {
  154. $data = $this->request->post('', null, 'trim');
  155. // 验证
  156. $result = $this->validate($data, 'Menu');
  157. // 验证失败 输出错误信息
  158. if(true !== $result) $this->error($result);
  159. // 顶部节点url检查
  160. if ($data['pid'] == 0 && $data['url_value'] == '' && ($data['url_type'] == 'module_admin' || $data['url_type'] == 'module_home')) {
  161. $this->error('顶级节点的节点链接不能为空');
  162. }
  163. // 设置角色权限
  164. $this->setRoleMenu($data['id'], isset($data['role']) ? $data['role'] : []);
  165. // 验证是否更改所属模块,如果是,则该节点的所有子孙节点的模块都要修改
  166. $map['id'] = $data['id'];
  167. $map['module'] = $data['module'];
  168. if (!MenuModel::where($map)->find()) {
  169. MenuModel::changeModule($data['id'], $data['module']);
  170. }
  171. if (MenuModel::update($data)) {
  172. Cache::clear();
  173. // 记录行为
  174. $details = '节点ID('.$id.')';
  175. action_log('menu_edit', 'admin_menu', $id, UID, $details);
  176. $this->success('编辑成功', cookie('__forward__'));
  177. } else {
  178. $this->error('编辑失败');
  179. }
  180. }
  181. // 获取数据
  182. $info = MenuModel::get($id);
  183. // 拥有该节点权限的角色
  184. $info['role'] = RoleModel::getRoleWithMenu($id);
  185. // 使用ZBuilder快速创建表单
  186. return ZBuilder::make('form')
  187. ->setPageTitle('编辑节点')
  188. ->addFormItem('hidden', 'id')
  189. ->addLinkage('module', '所属模块', '', ModuleModel::getModule(), '', url('ajax/getModuleMenus'), 'pid')
  190. ->addFormItem('select', 'pid', '所属节点', '所属上级节点', MenuModel::getMenuTree(0, '', $info['module']))
  191. ->addFormItem('text', 'title', '节点标题')
  192. ->addFormItem('radio', 'url_type', '链接类型', '', ['module_admin' => '模块链接(后台)', 'module_home' => '模块链接(前台)', 'link' => '普通链接'], 'module_admin')
  193. ->addFormItem(
  194. 'text',
  195. 'url_value',
  196. '节点链接',
  197. "可留空,如果是模块链接,请填写<code>模块/控制器/操作</code>,如:<code>admin/menu/add</code>。如果是普通链接,则直接填写url地址,如:<code>http://www.dolphinphp.com</code>"
  198. )
  199. ->addText('params', '参数', '如:a=1&b=2')
  200. ->addSelect('role', '角色', '除超级管理员外,拥有该节点权限的角色', RoleModel::where('id', 'neq', 1)->column('id,name'), '', 'multiple')
  201. ->addRadio('url_target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self')
  202. ->addIcon('icon', '图标', '导航图标')
  203. ->addRadio('online_hide', '网站上线后隐藏', '关闭开发模式后,则隐藏该菜单节点', ['否', '是'])
  204. ->addText('sort', '排序', '', 100)
  205. ->setFormData($info)
  206. ->fetch();
  207. }
  208. /**
  209. * 设置角色权限
  210. * @param string $role_id 角色id
  211. * @param array $roles 角色id
  212. * @author 蔡伟明 <314013107@qq.com>
  213. * @throws \Exception
  214. */
  215. private function setRoleMenu($role_id = '', $roles = [])
  216. {
  217. $RoleModel = new RoleModel();
  218. // 该节点的所有子节点,包括本身节点
  219. $menu_child = MenuModel::getChildsId($role_id);
  220. $menu_child[] = (int)$role_id;
  221. // 该节点的所有上下级节点
  222. $menu_all = MenuModel::getLinkIds($role_id);
  223. $menu_all = array_map('strval', $menu_all);
  224. if (!empty($roles)) {
  225. // 拥有该节点的所有角色id及节点权限
  226. $role_menu_auth = RoleModel::getRoleWithMenu($role_id, true);
  227. // 已有该节点权限的角色id
  228. $role_exists = array_keys($role_menu_auth);
  229. // 新节点权限的角色
  230. $role_new = $roles;
  231. // 原有权限角色差集
  232. $role_diff = array_diff($role_exists, $role_new);
  233. // 新权限角色差集
  234. $role_diff_new = array_diff($role_new, $role_exists);
  235. // 新节点角色权限
  236. $role_new_auth = RoleModel::getAuthWithRole($roles);
  237. // 删除原先角色的该节点权限
  238. if ($role_diff) {
  239. $role_del_auth = [];
  240. foreach ($role_diff as $role) {
  241. $auth = json_decode($role_menu_auth[$role], true);
  242. $auth_new = array_diff($auth, $menu_child);
  243. $role_del_auth[] = [
  244. 'id' => $role,
  245. 'menu_auth' => array_values($auth_new)
  246. ];
  247. }
  248. if ($role_del_auth) {
  249. $RoleModel->saveAll($role_del_auth);
  250. }
  251. }
  252. // 新增权限角色
  253. if ($role_diff_new) {
  254. $role_update_auth = [];
  255. foreach ($role_new_auth as $role => $auth) {
  256. $auth = json_decode($auth, true);
  257. if (in_array($role, $role_diff_new)) {
  258. $auth = array_unique(array_merge($auth, $menu_all));
  259. }
  260. $role_update_auth[] = [
  261. 'id' => $role,
  262. 'menu_auth' => array_values($auth)
  263. ];
  264. }
  265. if ($role_update_auth) {
  266. $RoleModel->saveAll($role_update_auth);
  267. }
  268. }
  269. } else {
  270. $role_menu_auth = RoleModel::getRoleWithMenu($role_id, true);
  271. $role_del_auth = [];
  272. foreach ($role_menu_auth as $role => $auth) {
  273. $auth = json_decode($auth, true);
  274. $auth_new = array_diff($auth, $menu_child);
  275. $role_del_auth[] = [
  276. 'id' => $role,
  277. 'menu_auth' => array_values($auth_new)
  278. ];
  279. }
  280. if ($role_del_auth) {
  281. $RoleModel->saveAll($role_del_auth);
  282. }
  283. }
  284. }
  285. /**
  286. * 删除节点
  287. * @param array $record 行为日志内容
  288. * @author 蔡伟明 <314013107@qq.com>
  289. * @throws \think\db\exception\DataNotFoundException
  290. * @throws \think\db\exception\ModelNotFoundException
  291. * @throws \think\exception\DbException
  292. */
  293. public function delete($record = [])
  294. {
  295. $id = $this->request->param('id');
  296. $menu = MenuModel::where('id', $id)->find();
  297. if ($menu['system_menu'] == '1') $this->error('系统节点,禁止删除');
  298. // 获取该节点的所有后辈节点id
  299. $menu_childs = MenuModel::getChildsId($id);
  300. // 要删除的所有节点id
  301. $all_ids = array_merge([(int)$id], $menu_childs);
  302. // 删除节点
  303. if (MenuModel::destroy($all_ids)) {
  304. Cache::clear();
  305. // 记录行为
  306. $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')';
  307. action_log('menu_delete', 'admin_menu', $id, UID, $details);
  308. $this->success('删除成功');
  309. } else {
  310. $this->error('删除失败');
  311. }
  312. }
  313. /**
  314. * 保存节点排序
  315. * @author 蔡伟明 <314013107@qq.com>
  316. */
  317. public function save()
  318. {
  319. if ($this->request->isPost()) {
  320. $data = $this->request->post();
  321. if (!empty($data)) {
  322. $menus = $this->parseMenu($data['menus']);
  323. foreach ($menus as $menu) {
  324. if ($menu['pid'] == 0) {
  325. continue;
  326. }
  327. MenuModel::update($menu);
  328. }
  329. Cache::clear();
  330. $this->success('保存成功');
  331. } else {
  332. $this->error('没有需要保存的节点');
  333. }
  334. }
  335. $this->error('非法请求');
  336. }
  337. /**
  338. * 添加子节点
  339. * @param array $data 节点数据
  340. * @param string $pid 父节点id
  341. * @author 蔡伟明 <314013107@qq.com>
  342. */
  343. private function createChildNode($data = [], $pid = '')
  344. {
  345. $url_value = substr($data['url_value'], 0, strrpos($data['url_value'], '/')).'/';
  346. $child_node = [];
  347. $data['pid'] = $pid;
  348. foreach ($data['child_node'] as $item) {
  349. switch ($item) {
  350. case 'add':
  351. $data['title'] = '新增';
  352. break;
  353. case 'edit':
  354. $data['title'] = '编辑';
  355. break;
  356. case 'delete':
  357. $data['title'] = '删除';
  358. break;
  359. case 'enable':
  360. $data['title'] = '启用';
  361. break;
  362. case 'disable':
  363. $data['title'] = '禁用';
  364. break;
  365. case 'quickedit':
  366. $data['title'] = '快速编辑';
  367. break;
  368. }
  369. $data['url_value'] = $url_value.$item;
  370. $data['create_time'] = $this->request->time();
  371. $data['update_time'] = $this->request->time();
  372. $child_node[] = $data;
  373. }
  374. if ($child_node) {
  375. $MenuModel = new MenuModel();
  376. $MenuModel->insertAll($child_node);
  377. }
  378. }
  379. /**
  380. * 递归解析节点
  381. * @param array $menus 节点数据
  382. * @param int $pid 上级节点id
  383. * @author 蔡伟明 <314013107@qq.com>
  384. * @return array 解析成可以写入数据库的格式
  385. */
  386. private function parseMenu($menus = [], $pid = 0)
  387. {
  388. $sort = 1;
  389. $result = [];
  390. foreach ($menus as $menu) {
  391. $result[] = [
  392. 'id' => (int)$menu['id'],
  393. 'pid' => (int)$pid,
  394. 'sort' => $sort,
  395. ];
  396. if (isset($menu['children'])) {
  397. $result = array_merge($result, $this->parseMenu($menu['children'], $menu['id']));
  398. }
  399. $sort ++;
  400. }
  401. return $result;
  402. }
  403. /**
  404. * 获取嵌套式节点
  405. * @param array $lists 原始节点数组
  406. * @param int $pid 父级id
  407. * @param int $max_level 最多返回多少层,0为不限制
  408. * @param int $curr_level 当前层数
  409. * @author 蔡伟明 <314013107@qq.com>
  410. * @return string
  411. */
  412. private function getNestMenu($lists = [], $max_level = 0, $pid = 0, $curr_level = 1)
  413. {
  414. $result = '';
  415. foreach ($lists as $key => $value) {
  416. if ($value['pid'] == $pid) {
  417. $disable = $value['status'] == 0 ? 'dd-disable' : '';
  418. // 组合节点
  419. $result .= '<li class="dd-item dd3-item '.$disable.'" data-id="'.$value['id'].'">';
  420. $result .= '<div class="dd-handle dd3-handle">拖拽</div><div class="dd3-content"><i class="'.$value['icon'].'"></i> '.$value['title'];
  421. if ($value['url_value'] != '') {
  422. $result .= '<span class="link"><i class="fa fa-link"></i> '.$value['url_value'].'</span>';
  423. }
  424. $result .= '<div class="action">';
  425. $result .= '<a href="'.url('add', ['module' => $value['module'], 'pid' => $value['id']]).'" data-toggle="tooltip" data-original-title="新增子节点"><i class="list-icon fa fa-plus fa-fw"></i></a><a href="'.url('edit', ['id' => $value['id']]).'" data-toggle="tooltip" data-original-title="编辑"><i class="list-icon fa fa-pencil fa-fw"></i></a>';
  426. if ($value['status'] == 0) {
  427. // 启用
  428. $result .= '<a href="javascript:void(0);" data-ids="'.$value['id'].'" class="enable" data-toggle="tooltip" data-original-title="启用"><i class="list-icon fa fa-check-circle-o fa-fw"></i></a>';
  429. } else {
  430. // 禁用
  431. $result .= '<a href="javascript:void(0);" data-ids="'.$value['id'].'" class="disable" data-toggle="tooltip" data-original-title="禁用"><i class="list-icon fa fa-ban fa-fw"></i></a>';
  432. }
  433. $result .= '<a href="'.url('delete', ['id' => $value['id'], 'table' => 'admin_menu']).'" data-toggle="tooltip" data-original-title="删除" class="ajax-get confirm"><i class="list-icon fa fa-times fa-fw"></i></a></div>';
  434. $result .= '</div>';
  435. if ($max_level == 0 || $curr_level != $max_level) {
  436. unset($lists[$key]);
  437. // 下级节点
  438. $children = $this->getNestMenu($lists, $max_level, $value['id'], $curr_level + 1);
  439. if ($children != '') {
  440. $result .= '<ol class="dd-list">'.$children.'</ol>';
  441. }
  442. }
  443. $result .= '</li>';
  444. }
  445. }
  446. return $result;
  447. }
  448. /**
  449. * 启用节点
  450. * @param array $record 行为日志
  451. * @author 蔡伟明 <314013107@qq.com>
  452. * @throws \think\db\exception\DataNotFoundException
  453. * @throws \think\db\exception\ModelNotFoundException
  454. * @throws \think\exception\DbException
  455. */
  456. public function enable($record = [])
  457. {
  458. $id = input('param.ids');
  459. $menu = MenuModel::where('id', $id)->find();
  460. $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')';
  461. $this->setStatus('enable', ['menu_enable', 'admin_menu', $id, UID, $details]);
  462. }
  463. /**
  464. * 禁用节点
  465. * @param array $record 行为日志
  466. * @author 蔡伟明 <314013107@qq.com>
  467. * @throws \think\db\exception\DataNotFoundException
  468. * @throws \think\db\exception\ModelNotFoundException
  469. * @throws \think\exception\DbException
  470. */
  471. public function disable($record = [])
  472. {
  473. $id = input('param.ids');
  474. $menu = MenuModel::where('id', $id)->find();
  475. $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')';
  476. $this->setStatus('disable', ['menu_disable', 'admin_menu', $id, UID, $details]);
  477. }
  478. /**
  479. * 设置状态
  480. * @param string $type 类型
  481. * @param array $record 行为日志
  482. * @author 小乌 <82950492@qq.com>
  483. */
  484. public function setStatus($type = '', $record = [])
  485. {
  486. $id = input('param.ids');
  487. $status = $type == 'enable' ? 1 : 0;
  488. if (false !== MenuModel::where('id', $id)->setField('status', $status)) {
  489. Cache::clear();
  490. // 记录行为日志
  491. if (!empty($record)) {
  492. call_user_func_array('action_log', $record);
  493. }
  494. $this->success('操作成功');
  495. } else {
  496. $this->error('操作失败');
  497. }
  498. }
  499. }