laravel实战-通用后台管理系统-菜单管理和角色管理
学习心得
控制器部分的编码跟管理员管理很像, 有点机械的写完了.
西门老师说很难理解的递归实现, 自己感觉理解得还行, 就是很难用文字描述出来. 简单粗暴的解释, 就是方法自己调用自己, 但是方法处理的参数变了.
在全选/全消, 本级全选/全消的实现倒花了些时间. jQuery和js方面还得加强学习. 另外, 不需要钻牛角尖, 明明使用自定义属性
pid
就能轻松实现的功能, 偏偏要用上下级/兄弟选择器/过滤器来实现, 把自己绕晕了.在参数验证方面, 有些许疑惑. 比如, 想验证管理员账号, 只能是以字母开头, 后续只能接字母, 数字和下划线, 似乎没有现成的方法可以使用, 变量过滤器好像也无法实现. 自己实现, 正则又不会, 且不同的验证, 要写很多验证规则, 也很繁琐. 请教老师, 有没有什么
composer
插件能简化参数验证的代码编写?
1. 菜单管理
菜单管理基本可以参照管理员管理的编码思路来编码.
根据菜单上下级关系, 设置不同缩进量的伪树形下拉菜单项列表实现
- 方法实现, 根据传入的父id(从父id=0开始)查找其子菜单项, 对其子菜单项, 再递归调用当前方法, 传入的父id变成子菜单项的id, 一直递归到查找不到子菜单(末级菜单)为止.
/**
* 根据菜单层级关系设置缩进量的伪树形结构下拉菜单项
*/
protected function getMenuTree($menus, $pid = 0, &$menuTree = [], $prefix = ' |-- ')
{
// 遍历所有菜单项
foreach ($menus as $menu) {
// 获取当前父id下的所有子选项
if ($menu['pid'] == $pid) {
// 带有缩进量的菜单名
$menu['show_title'] = $prefix . $menu['title'];
// 加入到下拉菜单项数组
$menuTree[] = $menu;
// 下一级子选项, 增加6个空格缩进量
$nextPrefix = str_repeat(' ', 6) . $prefix;
// 递归获取当前菜单项的子菜单
$this->getMenuTree($menus, $menu['mid'], $menuTree, $nextPrefix);
}
}
return $menuTree;
}
2. 角色管理
layui监听某个复选框选择事件的方法:
需监听的复选框添加
lay-filter="filter_tag"
属性, 给它添加一个区别于其他复选框的标签.使用layui的form组件的on方法, 绑定具有标签名为
filter_tag
的复选框的选择事件及其处理脚本方法.
<!-- 其他布局元素... -->
<div class="layui-form-item">
<label for="all_rights" class="layui-form-label">
<input type="checkbox" name="all" id="all" class="layui-input" title="菜单全选" lay-filter="all" lay-skin="primary">
</label>
</div>
<!-- 其他布局元素... -->
<!-- 绑定事件处理方法 -->
<script>
layui.use(['layer', 'form'], function() {
var layer = layui.layer;
var form = layui.form;
$ = layui.jquery;
form.on('checkbox(sel_all)', function(data) {
var val = data.elem.checked;
// 一定要用prop, 用attr会有bug(jquery的bug)
$(data.elem).parent().parent().find('input[name="rights[]"]').prop('checked', val);
form.render('checkbox');
})
// 其他处理逻辑...
}
</script>
全选/全消当前菜单的所有子菜单
把当前菜单项的选择情况, 赋值给其子菜单项即可.
获取当前父菜单的所有子菜单的方法
- 方法1: 子菜单增加一个自定义属性
pid
值为父id, 使用$('input[type="checkbox"][pid="父id"]')
获取. - 方法2: 根据当前父菜单和子菜单的层级关系, 使用上下文选择器/过滤器获取.
- 很明显, 方法1更简单优雅. 注意: 赋值语句一定要用
prop('checked', true/false)
方法, 使用attr('checked', true/false)
会出现只有前两次点击生效, 第三次点击开始无效的bug.
- 方法1: 子菜单增加一个自定义属性
- 选中/取消选中子菜单时, 判断兄弟菜单项是否全都已选中, 若是, 则把父菜单的选中情况设置为选中; 否则设为取消选中.
- 思路: 获取所有选中的兄弟复选框, 获取所有兄弟复选框. 如果两者的数量相等, 代表所有兄弟复选框全都已选中, 把父菜单的复选框选中. 否则,把父菜单的复选框取消选中.
3. 代码清单
3.1 菜单管理
视图文件代码
- 1-菜单列表
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>菜单列表</title>
<link rel="stylesheet" href="/static/plugin/layui/css/layui.css">
<script src="/static/plugin/layui/layui.js"></script>
</head>
<body>
<div style="text-align: center; color: #666;">
<h2>菜单列表</h2>
</div>
@if(!empty($pid))
<div style="float: left; height: 50px; line-height: 50px; padding: 0 10px;">
<button class="layui-btn layui-btn-primary layui-btn-sm" onclick="returnBack({{$dad_menu['pid']}})">返回上一级</button>
</div>
@endif
<div style="float: right; height: 50px; line-height: 50px; padding: 0 10px;">
<button class="layui-btn layui-btn-success layui-btn-sm" onclick="add()">新增</button>
</div>
<table class="layui-table">
<thead>
<tr style="text-align: center">
<th>排序</th>
<th>ID</th>
<th>图标</th>
<th>菜单名称</th>
<th>控制器</th>
<th>方法</th>
<th>是否隐藏</th>
<th>菜单状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@csrf
@foreach($menus as $menu)
<tr>
<td>{{$menu['ord']}}</td>
<td>{{$menu['mid']}}</td>
<td>{{$menu['icon']}}</td>
<td>{{$menu['title']}}</td>
<td>{{$menu['controller']}}</td>
<td>{{$menu['action']}}</td>
<td>{{$menu['ishidden'] ? '隐藏' : '显示'}}</td>
<td>{{$menu['status'] == 0 ? '正常' : '禁用'}}</td>
<td>
<button class="layui-btn layui-btn-primary layui-btn-xs" onclick="getSonMenu({{$menu['mid']}})">子菜单</button>
<button class="layui-btn layui-btn-xs" onclick="edit({{$menu['mid']}})">修 改</button>
@if($menu['status'] == 0)
<button class="layui-btn layui-btn-danger layui-btn-xs" onclick="delOrResume(1, {{$menu['mid']}}, '{{$menu['title']}}')">禁 用</button>
@else
<button class="layui-btn layui-btn-normal layui-btn-xs" onclick="delOrResume(0, {{$menu['mid']}}, '{{$menu['title']}}')">恢 复</button>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</body>
<script>
layui.use(['layer'], function() {
layer = layui.layer;
$ = layui.jquery;
});
function edit(id) {
layer.open({
type: 2,
title: '修改管理员信息',
shadeClose: false,
// 应该是阴影区域的透明度
shade: 0.8,
// 分别是宽,高
area: ['400px', '560px'],
content: '/admin/menu/edit?mid=' + id,
btn: ["提交"],
yes: function(index, layero) {
var body = layer.getChildFrame('body', index);
// 得到iframe页的窗口对象
var iframeWin = window[layero.find('iframe')[0]['name']];
// 执行iframe页的方法: iframeWin.要调用的方法名();
iframeWin.save();
}
});
}
function getSonMenu(pid) {
window.location.href = "?pid=" + pid;
}
// 返回上一级: 参数是父菜单的父id
function returnBack(ppid) {
window.location.href="?pid=" + ppid;
}
function add() {
layer.open({
type: 2,
title: '添加新菜单',
// 点击弹出窗口外的阴影区域是否关闭弹出窗口
shadeClose: false,
shade: 0.8,
area: ['400px', '560px'],
content: '/admin/menu/add?pid={{empty($pid) ? 0 : $pid}}' //iframe的url
, btn: ['保存']
// 对应按钮区第一个按钮的执行脚本.
, yes: function(index, layero) {
var body = layer.getChildFrame('body', index);
// 得到iframe页的窗口对象
var iframeWin = window[layero.find('iframe')[0]['name']];
// 执行iframe页的方法: iframeWin.要调用的方法名();
iframeWin.save();
}
});
}
function delOrResume(status, id, title) {
var msg = status == 1 ? '禁用' : '恢复';
//询问框
layer.confirm('确定要' + msg + title + '吗?', {
btn: ['确定','取消'] //按钮
}, function(){
var _token = $('input[name="_token"]').val();
$.post('/admin/menu/del_resume', {id: id, resume: status, _token: _token}, function(res) {
if(res.status != undefined && res.status == "0") {
layer.msg(res.message, {icon: 1});
setTimeout(() => {
window.location.reload();
}, 1000);
} else if(res.status != undefined) {
layer.alert(res.message, {icon: 2});
} else {
layer.alert('操作失败', {icon: 2});
}
// layer.alert(res);
}, 'json');
}, function() {
});
}
</script>
</html>
- 2-增加菜单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>添加菜单</title>
<link rel="stylesheet" href="/static/plugin/layui/css/layui.css">
<script src="/static/plugin/layui/layui.js"></script>
<style>
body {
padding: 10px;
}
.cms-label {
text-align-last: justify;
}
</style>
</head>
<body>
<div class="layui-form">
@csrf
<div class="layui-form-item">
<label for="pid" class="layui-form-label cms-label">上级菜单</label>
<div class="layui-input-inline">
<select name="pid" id="pid" value="{{empty($pid) ? 0 : $pid}}">
<option value="0" selected>一级菜单</option>
@foreach($menuTree as $menu)
<option value="{{$menu['mid']}}" {{$menu['mid'] == $pid ? 'selected' : ''}}>{!!$menu['show_title']!!}
<option>
@endforeach
</select>
</div>
</div>
<div class="layui-form-item">
<label for="title" class="layui-form-label cms-label">菜单名称</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="title" id="title">
</div>
</div>
<div class="layui-form-item">
<label for="controller" class="layui-form-label cms-label">控制器</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="controller" id="controller">
</div>
</div>
<div class="layui-form-item">
<label for="action" class="layui-form-label cms-label">方法名</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="action" id="action">
</div>
</div>
<div class="layui-form-item">
<label for="ishidden" class="layui-form-label cms-label">是否隐藏</label>
<div class="layui-inpu-inline">
<input type="checkbox" name="ishidden" id="ishidden" title="隐藏">
</div>
</div>
<div class="layui-form-item">
<label for="status" class="layui-form-label cms-label">状态</label>
<div class="layui-inpu-inline">
<input type="checkbox" name="status" id="status" title="禁用">
</div>
</div>
<div class="layui-form-item">
<label for="icon" class="layui-form-label cms-label">菜单图标</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="icon" id="icon">
</div>
</div>
<div class="layui-form-item">
<label for="ord" class="layui-form-label cms-label">菜单排序</label>
<div class="layui-input-inline">
<input type="number" name="ord" id="ord" class="layui-input" value="0" min="0">
</div>
</div>
<!-- <div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn" onclick="save()">保存</button>
</div>
</div> -->
</div>
</body>
<script>
layui.use(['layer', 'form'], function() {
layer = layui.layer;
form = layui.form;
$ = layui.jquery;
});
function save() {
// 数据
var data = {
_token: $('input[name="_token"]').val(),
pid: $.trim($('select[name="pid"]').val()),
title: $.trim($('input[name="title"]').val()),
controller: $.trim($('input[name="controller"]').val()),
action: $.trim($('#action').val()),
icon: $.trim($('#icon').val()),
status: $('#status').prop('checked') ? 1 : 0,
ord: $('#ord').val(),
ishidden: $('#ishidden').prop('checked') ? 1 : 0
};
// 判断数据有消息
var res = dataCheck(data);
if(res.status == 1) {
return layer.alert(res.message, {icon: 2});
}
// 保存数据
$.post('/admin/menu/save', data, function(res){
if(res.status != undefined && res.status == '0') {
layer.msg(res.message, {icon: 1});
setTimeout(() => {
window.parent.location.reload();
}, 1000);
} else if(res.status != undefined) {
return layer.alert(res.message, {icon: 2});
} else {
return layer.alert('提交保存失败');
}
}, 'json');
}
function dataCheck(data) {
var res = {
status: 1
};
if(data.pid == undefined || data.pid == '') {
res.message = '上级菜单id不能为空';
return res;
}
if(data.title == undefined || data.title == '') {
res.message = '菜单标题不能为空';
return res;
}
if(data.controller == undefined || data.controller == '') {
res.message = "控制器不能为空";
return res;
}
if(data.ord == undefined || data.ord == '') {
res.message = '排序不能为空';
return res;
}
if(isNaN(data.ord)) {
res.message = "排序必须为数字";
return res;
}
return {
status: 0
};
}
</script>
</html>
- 3- 修改菜单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>修改菜单</title>
<link rel="stylesheet" href="/static/plugin/layui/css/layui.css">
<script src="/static/plugin/layui/layui.js"></script>
<style>
body {
padding: 10px;
}
.cms-label {
text-align-last: justify;
}
</style>
</head>
<body>
<div class="layui-form">
@csrf
<input type="hidden" name="mid" id="mid" value="{{$menu['mid']}}">
<div class="layui-form-item">
<label for="pid" class="layui-form-label cms-label">上级菜单</label>
<div class="layui-input-inline">
<select name="pid" id="pid" value="{{$menu['pid']}}">
<option value="0" selected>一级菜单</option>
@foreach($menuTree as $menuT)
<option value="{{$menuT['mid']}}" {{$menu['pid'] == $menuT['mid'] ? 'selected' : ''}}>{!!$menuT['show_title']!!}
<option>
@endforeach
</select>
</div>
</div>
<div class="layui-form-item">
<label for="title" class="layui-form-label cms-label">菜单名称</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="title" id="title" value="{{$menu['title']}}">
</div>
</div>
<div class="layui-form-item">
<label for="controller" class="layui-form-label cms-label">控制器</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="controller" id="controller" value="{{$menu['controller']}}">
</div>
</div>
<div class="layui-form-item">
<label for="action" class="layui-form-label cms-label">方法名</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="action" id="action" value="{{$menu['action']}}">
</div>
</div>
<div class="layui-form-item">
<label for="ishidden" class="layui-form-label cms-label">是否隐藏</label>
<div class="layui-inpu-inline">
<input type="checkbox" name="ishidden" id="ishidden" title="隐藏" {{$menu['ishidden'] ? 'checked' : ''}}>
</div>
</div>
<div class="layui-form-item">
<label for="status" class="layui-form-label cms-label">是否禁用</label>
<div class="layui-inpu-inline">
<input type="checkbox" name="status" id="status" title="禁用" {{$menu['status'] ? 'checked' : ''}}>
</div>
</div>
<div class="layui-form-item">
<label for="icon" class="layui-form-label cms-label">菜单图标</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="icon" id="icon" value="{{$menu['icon']}}">
</div>
</div>
<div class="layui-form-item">
<label for="ord" class="layui-form-label cms-label">菜单排序</label>
<div class="layui-input-inline">
<input type="number" name="ord" id="ord" class="layui-input" value="{{$menu['ord']}}" min="0">
</div>
</div>
</div>
</body>
<script>
layui.use(['layer', 'form'], function() {
layer = layui.layer;
form = layui.form;
$ = layui.jquery;
});
function save() {
// 数据
var data = {
_token: $('input[name="_token"]').val(),
mid: $.trim($('input[name="mid"]').val()),
pid: $.trim($('select[name="pid"]').val()),
title: $.trim($('input[name="title"]').val()),
controller: $.trim($('input[name="controller"]').val()),
action: $.trim($('#action').val()),
icon: $.trim($('#icon').val()),
status: $('#status').prop('checked') ? 1 : 0,
ord: $('#ord').val(),
ishidden: $('#ishidden').prop('checked') ? 1 : 0
};
// 判断数据有消息
var res = dataCheck(data);
if(res.status == 1) {
return layer.alert(res.message, {icon: 2});
}
// 保存数据
$.post('/admin/menu/update', data, function(res){
if(res.status != undefined && res.status == '0') {
layer.msg(res.message, {icon: 1});
setTimeout(() => {
window.parent.location.reload();
}, 1000);
} else if(res.status != undefined) {
return layer.alert(res.message, {icon: 2});
} else {
return layer.alert('提交保存失败');
}
}, 'json');
}
// 验证数据有效性
function dataCheck(data) {
var res = {
status: 1
};
if(data.pid == undefined || data.pid == '') {
res.message = '上级菜单id不能为空';
return res;
}
if(data.title == undefined || data.title == '') {
res.message = '菜单标题不能为空';
return res;
}
if(data.controller == undefined || data.controller == '') {
res.message = "控制器不能为空";
return res;
}
if(data.ord == undefined || data.ord == '') {
res.message = '排序不能为空';
return res;
}
if(isNaN(data.ord)) {
res.message = "排序必须为数字";
return res;
}
return {
status: 0
};
}
</script>
</html>
- 服务端
- 4- 菜单中间件
<?php
namespace App\Http\Controllers\admins;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class Menu extends Controller
{
/**
* 当前层级菜单列表
*/
public function index(Request $req)
{
// 获取父id, 如果没有传参, 则返回null, 强转null为整型, 则转为0.
$pid = (int) $req->pid;
// 入参的父id对应的0菜单项
$data['menus'] = DB::table('admin_menu')->where('pid', $pid)->orderBy('controller')->orderBy('ord', 'desc')->lists();
// 父菜单
$data['dad_menu'] = DB::table('admin_menu')->where('mid', $pid)->getFirst();
$data['pid'] = $pid;
return view('/admins/menu/index', $data);
}
/**
* 禁用/恢复菜单项
*/
public function delOrResume(Request $req)
{
$mid = $req->id;
$status = $req->resume;
// 菜单id有效性判断
if (!filter_var($mid, FILTER_VALIDATE_INT, ['option' => ['min_range' => 1]])) {
return json_decode(['status' => 1, 'message' => '无效的菜单id']);
};
// 1=禁用; 0=启用
if ($status)
$status = 1;
else
$status = 0;
$opera = $status ? '禁用' : '恢复';
// 开启QueryLog
// DB::connection()->enableQueryLog();
// 执行
$res = DB::table('admin_menu')->where('mid', $mid)->update(['status' => $status]);
// 输出sql
// dump(DB::getQueryLog());die;
if ($res) {
return json_encode(['status' => 0, 'message' => $opera . '成功!']);
} else {
return json_encode(['status' => 1, 'message' => $opera . '失败!']);
}
}
/**
* 添加菜单项页面
*/
public function add(Request $req)
{
$menus = DB::table('admin_menu')->lists();
// 获取根据层级关系进行缩进的菜单列表
$data['menuTree'] = $this->getMenuTree($menus);
$data['pid'] = $req->pid;
return view('/admins/menu/add', $data);
}
/**
* 新增保存
*/
public function save(Request $req)
{
// 要保存的菜单数据
$data = [
'title' => trim($req->title),
'controller' => trim($req->controller),
'action' => trim($req->action),
'icon' => trim($req->icon),
'status' => intval(boolval(trim($req->status))),
'ord' => intval(trim($req->ord)),
'pid' => intval(trim($req->pid)),
'ishidden' => intval(boolval(trim($req->ishidden))),
'status' => intval(trim($req->status))
];
// 验证数据有效性
$checked = $this->checkInput($data);
if ($checked['status']) {
return json_encode($checked);
}
// 返回插入的记录的id
$res = DB::table('admin_menu')->insertGetId($data);
// 开发人员默认拥有所有权限, 所以把新增的权限加入到开发人员的权限字段中
$group = DB::table('admin_group')->where('gid', 1)->getFirst();
$rights = json_decode($group['rights'], true);
$rights[] = $res;
$group['rights'] = '[' . implode(',', $rights) . ']';
DB::table('admin_group')->where('gid', 1)->update($group);
if ($res) {
return json_encode(['status' => 0, 'message' => '创建成功']);
}
return json_encode(['status' => 1, 'message' => '创建失败']);
}
public function edit(Request $req)
{
$mid = $req->mid;
if (!is_numeric($mid) || $mid < 1) {
return response('无效的菜单ID: ' . $mid, 200);
}
// 查询待编辑的菜单项
$menu = DB::table('admin_menu')->where('mid', $mid)->getFirst();
if (!$menu) {
return response('菜单项不存在', 200);
}
$data['menu'] = $menu;
// 获取有缩进量的菜单下拉列表项
$menus = DB::table('admin_menu')->lists();
$data['menuTree'] = $this->getMenuTree($menus);
return view('/admins/menu/edit', $data);
}
public function update(Request $req)
{
// 要保存的菜单数据
$data = [
'mid' => trim($req->mid),
'title' => trim($req->title),
'controller' => trim($req->controller),
'action' => trim($req->action),
'icon' => trim($req->icon),
'status' => intval(boolval(trim($req->status))),
'ord' => intval(trim($req->ord)),
'pid' => intval(trim($req->pid)),
'ishidden' => intval(boolval(trim($req->ishidden)))
];
// 验证数据有效性
$checked = $this->checkInput($data, true);
if ($checked['status']) {
return json_encode($checked);
}
$res = DB::table('admin_menu')->where('mid', $data['mid'])->update($data);
if ($res) {
return json_encode(['status' => 0, 'message' => '修改成功']);
}
return json_encode(['status' => 1, 'message' => '修改失败']);
}
/**
* 数据验证方法
*/
protected function checkInput($data, $checkId = false)
{
$res = ['status' => 1];
if($checkId) {
if (empty($data['mid'])) {
$res['message'] = '菜单主键不能为空';
return $res;
}
}
if (empty($data['title'])) {
$res['message'] = '菜单标题不能为空';
return $res;
}
if (empty($data['controller'])) {
$res['message'] = '控制器名称不能为空';
return $res;
}
return ['status' => 0];
}
/**
* 根据菜单层级关系设置缩进量的伪树形结构下拉菜单项
*/
protected function getMenuTree($menus, $pid = 0, &$menuTree = [], $prefix = ' |-- ')
{
// 遍历所有菜单项
foreach ($menus as $menu) {
// 获取当前父id下的所有子选项
if ($menu['pid'] == $pid) {
// 带有缩进量的菜单名
$menu['show_title'] = $prefix . $menu['title'];
// 加入到下拉菜单项数组
$menuTree[] = $menu;
// 下一级子选项, 增加6个空格缩进量
$nextPrefix = str_repeat(' ', 6) . $prefix;
// 递归获取当前菜单项的子菜单
$this->getMenuTree($menus, $menu['mid'], $menuTree, $nextPrefix);
}
}
return $menuTree;
}
}
3.2 用户组管理
视图文件代码
- 1- 用户组列表
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>角色组列表</title>
<link rel="stylesheet" href="/static/plugin/layui/css/layui.css">
<script src="/static/plugin/layui/layui.js"></script>
</head>
<body>
<div style="text-align: center; color: #666;">
<h2>角色组列表</h2>
</div>
<div style="float: right; height: 50px; line-height: 50px; padding: 0 10px;">
<button class="layui-btn layui-btn-success layui-btn-sm" onclick="add()">新增</button>
</div>
<table class="layui-table">
<thead>
<tr>
<th>ID</th>
<th>角色组名称</th>
<th>授权菜单</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach($groups as $group)
<tr>
<td>{{$group['gid']}}</td>
<td style="min-width: 100px;">{{$group['title']}}</td>
<td>{{$group['right_title']}}</td>
<td style="min-width: 100px;">
<span class="layui-btn layui-btn-success layui-btn-xs" onclick="setRights({{$group['gid']}})">授权</span>
<span class="layui-btn layui-btn-warm layui-btn-xs">停用</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</body>
<script>
layui.use(['layer', 'form'], function() {
layer = layui.layer;
form = layui.form;
$ = layui.jquery;
});
function add() {
layer.open({
type: 2,
title: '添加角色组',
// 点击弹出窗口外的阴影区域是否关闭弹出窗口
shadeClose: false,
shade: 0.8,
area: ['400px', '85%'],
content: '/admin/groups/add' //iframe的url
, btn: ['保存']
, yes: function(index, layero) {
var body = layer.getChildFrame('body', index);
// 得到iframe页的窗口对象
var iframeWin = window[layero.find('iframe')[0]['name']];
// 执行iframe页的方法: iframeWin.要调用的方法名();
iframeWin.save();
}
});
}
function setRights(gid) {
layer.open({
type: 2,
title: '角色组授权',
shadeClose: false,
shade: 0.8,
area: ['400px', '650px'],
content: '/admin/groups/edit?gid=' + gid,
btn: ['提交'],
yes: function(index, layero) {
var body = layer.getChildFrame('body', index);
var iframeWin = window[layero.find('iframe')[0]['name']];
iframeWin.save();
}
});
}
</script>
</html>
- 2- 修改用户组/用户组授权
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>角色组授权</title>
<link rel="stylesheet" href="/static/plugin/layui/css/layui.css">
<script src="/static/plugin/layui/layui.js"></script>
<style>
body {padding: 10px;}
.sel_all{
padding-top: 0px;
}
</style>
</head>
<body>
<div class="layui-form">
@csrf
<input type="hidden" name="gid" value="{{$group['gid']}}">
<div class="layui-form-item">
<label for="title" class="layui-form-label">角色组名</label>
<div class="layui-input-inline">
<span class="layui-input">{{$group['title']}}</span>
</div>
</div>
<div class="layui-form-item">
<label for="all_rights" class="layui-form-label">
<input type="checkbox" name="all" id="all" class="layui-input" title="菜单全选" lay-filter="all" lay-skin="primary">
</label>
</div>
@foreach($menus as $menu)
<div class="layui-form-item">
<label for="" class="layui-form-label sel_all">
<input type="checkbox" name="sel_all" class="layui-input" lay-skin="primary" title="本级全选" lay-filter="sel_all">
</label>
@if(!empty($menu['child_menus']))
<div class="layui-input-inline">
<input type="checkbox" {{in_array($menu['mid'], $group['rights']) ? 'checked' : ''}} name="rights[]" data-id="{{$menu['mid']}}" title="{{$menu['title']}}" class="layui-input" lay-skin="primary" lay-filter="sel_one">
<br>
@foreach($menu['child_menus'] as $cmenu)
<input type="checkbox" {{in_array($cmenu['mid'], $group['rights']) ? 'checked' : ''}} name="rights[]" data-id="{{$cmenu['mid']}}" title="{{$cmenu['title']}}" class="layui-input" lay-skin="primary" lay-filter="sel_one">
@endforeach
</div>
@endif
</div>
@endforeach
</div>
</body>
<script>
layui.use(['layer', 'form'], function() {
var layer = layui.layer;
var form = layui.form;
$ = layui.jquery;
form.on('checkbox(sel_all)', function(data) {
var val = data.elem.checked;
// 一定要用prop, 用attr会有bug(jquery的bug)
$(data.elem).parent().parent().find('input[name="rights[]"]').prop('checked', val);
form.render('checkbox');
})
// 子菜单项选装事件
form.on('checkbox(sel_one)', function(data) {
// 当前复选框的选中情况
var val = data.elem.checked;
// $(data.elem).attr('checked', val);
// 获取所有选中的兄弟复选框
var checked = $(data.elem).parent().find('input[type="checkbox"]:checked');
// 获取所有兄弟复选框
var allOne = $(data.elem).parent().find('input[type="checkbox"]');
if(checked.length == allOne.length) {
$(data.elem).parent().prev().children().filter('input').prop('checked', true);
} else {
$(data.elem).parent().prev().children().filter('input').prop('checked', false);
}
form.render('checkbox');
});
form.on('checkbox(all)', function(data) {
var val = data.elem.checked;
$('.layui-form-item:nth-child(n+2) input[type="checkbox"]').prop('checked', val);
form.render('checkbox');
});
});
function save() {
var gid = $.trim($('input[name="gid"]').val());
if(gid == "") {
return layer.alert('用户组id出错', {icon: 2});
}
// 把选中的复选框对应的菜单id放入数组中
var rights = [];
$('input[name="rights[]"]:checked').each(function(index, checkbox) {
rights.push($(checkbox).data('id'));
});
if(rights.length < 1) {
return layer.alert('请选择权限', {icon: 2});
}
var _token = $('input[name="_token"]').val();
var data = {
gid: gid,
_token: _token,
// title: title,
rights: rights
};
$.ajax({
url: '/admin/groups/update',
method: 'POST',
data: data,
dataType: 'json',
success: function(res) {
if(res.status != undefined && res.status == 0) {
layer.msg(res.message, {icon: 1});
setTimeout(() => {
window.parent.location.reload();
}, 1000);
} else if(res.status != undefined) {
return layer.alert(res.message, {icon: 2});
} else {
return layer.alert('保存失败', {icon: 2});
}
}
});
}
</script>
</html>
- 3- 新增用户组
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>新增角色组</title>
<link rel="stylesheet" href="/static/plugin/layui/css/layui.css">
<script src="/static/plugin/layui/layui.js"></script>
<style>
body {padding: 10px;}
.sel_all{
padding-top: 0px;
}
</style>
</head>
<body>
<div class="layui-form">
@csrf
<div class="layui-form-item">
<label for="title" class="layui-form-label">角色组名</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="title" id="title">
</div>
</div>
<div class="layui-form-item">
<label for="all_rights" class="layui-form-label">
<input type="checkbox" name="all" id="all" class="layui-input" title="菜单全选" lay-filter="all" lay-skin="primary">
</label>
</div>
@foreach($menus as $menu)
<div class="layui-form-item">
<label for="" class="layui-form-label sel_all">
<input type="checkbox" name="sel_all" class="layui-input" lay-skin="primary" title="本级全选" lay-filter="sel_all">
</label>
@if(!empty($menu['child_menus']))
<div class="layui-input-inline">
<input type="checkbox" name="rights[]" data-id="{{$menu['mid']}}" title="{{$menu['title']}}" class="layui-input" lay-skin="primary" lay-filter="sel_one">
<br>
@foreach($menu['child_menus'] as $cmenu)
<input type="checkbox" name="rights[]" data-id="{{$cmenu['mid']}}" title="{{$cmenu['title']}}" class="layui-input" lay-skin="primary" lay-filter="sel_one">
@endforeach
</div>
@endif
</div>
@endforeach
</div>
</body>
<script>
layui.use(['layer', 'form'], function() {
var layer = layui.layer;
var form = layui.form;
$ = layui.jquery;
// 本级所有子菜单全选/全消
form.on('checkbox(sel_all)', function(data) {
var val = data.elem.checked;
// 一定要用prop, 用attr会有bug(jquery的bug)
$(data.elem).parent().parent().find('input[name="rights[]"]').prop('checked', val);
form.render('checkbox');
})
// 子菜单选中/取消选中
form.on('checkbox(sel_one)', function(data) {
var val = data.elem.checked;
// $(data.elem).attr('checked', val);
var notChecked = $(data.elem).parent().find('input[type="checkbox"]:checked');
var allOne = $(data.elem).parent().find('input[type="checkbox"]');
if(notChecked.length == allOne.length) {
$(data.elem).parent().prev().children().filter('input').prop('checked', true);
} else {
$(data.elem).parent().prev().children().filter('input').prop('checked', false);
}
form.render('checkbox');
});
// 全选/全消所有菜单
form.on('checkbox(all)', function(data) {
var val = data.elem.checked;
$('.layui-form-item:nth-child(n+2) input[type="checkbox"]').prop('checked', val);
form.render('checkbox');
});
});
function save() {
// 用户组名称
var title = $.trim($('input[name="title"]').val());
if(title == undefined || title == "") {
return layer.alert('请输入用户组名称', {icon: 2});
}
// 获得授权的权限id数组
var rights = [];
$('input[name="rights[]"]:checked').each(function(index, checkbox) {
rights.push($(checkbox).data('id'));
});
if(rights.length < 1) {
return layer.alert('请选择权限', {icon: 2});
}
// token不能漏
var _token = $('input[name="_token"]').val();
var data = {
_token: _token,
title: title,
rights: rights
};
// 提交请求
$.ajax({
url: '/admin/groups/save',
method: 'POST',
data: data,
dataType: 'json',
success: function(res) {
if(res.status != undefined && res.status == 0) {
layer.msg(res.message, {icon: 1});
setTimeout(() => {
window.parent.location.reload();
}, 1000);
} else if(res.status != undefined) {
return layer.alert(res.message, {icon: 2});
} else {
return layer.alert('保存失败', {icon: 2});
}
}
});
}
</script>
</html>
服务端代码
- 1- 用户组控制器
<?php
namespace App\Http\Controllers\admins;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
class Groups extends Controller {
public function index() {
// 角色列表
$groups = DB::table('admin_group')->lists();
// 菜单列表
$menus = DB::table('admin_menu')->where('status', 0)->orderBy('pid')->orderBy('mid')->keyval('mid', 'title');
foreach($groups as &$group) {
// 把json格式的字符串转为菜单id数组
$rights = json_decode($group['rights'], true);
// 拼接已授权的菜单字符串
$rightTitles = '';
foreach($rights as $right) {
// 有效的权限id才拼接
if(key_exists($right, $menus))
$rightTitles .= ($menus[$right] . ',');
}
$group['right_title'] = substr($rightTitles, 0, -1);
}
return view('/admins/groups/index', ['groups' => $groups]);
}
public function add() {
return view('/admins/groups/add', ['menus' => $this->getMenus()]);
}
function save(Request $req) {
$title = $req->title;
$rights = $req->rights;
if(empty($title)) {
return json_encode(['status' => 1, 'message' => '请录入用户组名称']);
}
if(!is_array($rights) || count($rights) < 1) {
return json_encode(['status' => 1, `message` => '请选择权限']);
}
$data = [
'title' => $title,
// $req传过来的参数都是字符串/字符串数组, 所以不能用json_encode()方法转换.
'rights' => '[' . implode(',', $rights) . ']'
];
$res = DB::table('admin_group')->insert($data);
if($res) {
return json_encode(['status' => 0, 'message' => '用户组添加成功']);
} else {
return json_encode(['status' => 1, `message` => '用户组添加失败']);
}
}
function update(Request $req) {
$gid = $req->gid;
// $title = $req->title;
$rights = $req->rights;
if(empty($gid)) {
return json_encode(['status' => 1, 'message' => '请录入用户组名称']);
}
if(!is_array($rights) || count($rights) < 1) {
return json_encode(['status' => 1, `message` => '请选择权限']);
}
$data = [
// 'title' => $title,
'rights' => '[' . implode(',', $rights) . ']'
];
$res = DB::table('admin_group')->where('gid', $gid)->update($data);
if($res) {
return json_encode(['status' => 0, 'message' => '权限分配成功']);
} else {
return json_encode(['status' => 1, `message` => '权限分配失败']);
}
}
function edit(Request $req) {
// 要编辑的用户组id
$gid = $req->gid;
if(!is_numeric($gid) || $gid < 1) {
return json_encode(['status' => 1, 'message' => '无效的用户组ID']);
}
// 查询要编辑的用户组数据
$group = DB::table('admin_group')->where('gid', $gid)->getFirst();
if(!$group) {
return json_encode(['status' => 1, 'message' => '没有该用户组']);
}
// 该用户组已被授权的菜单id, 转为数组
$group['rights'] = json_decode($group['rights'], true);
$data['group'] = $group;
// 有层级关系的菜单列表\
$data['menus'] = $this->getMenus();
return view('/admins/groups/edit', $data);
}
/**
* 获取菜单列表
*/
protected function getMenus() {
// 一级菜单
$menu1sts = DB::table('admin_menu')->where('status', 0)->where('pid', 0)->keyval('mid');
// 所有菜单
$menus = DB::table('admin_menu')->where('status', 0)->keyval('mid');
// 遍历所有菜单
foreach($menus as $menu) {
$pid = $menu['pid'];
// 找到父级菜单
if(key_exists($pid, $menu1sts)) {
// 为父级菜单添加'child_menus'元素
if(!isset($menu1sts[$pid]['child_menus'])) $menu1sts[$pid]['child_menus'] = [];
$menu1sts[$pid]['child_menus'][] = $menu;
}
}
return $menu1sts;
}
}