🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 登录 先看效果: ![2015-07-03/5595db3b5f057](http://box.kancloud.cn/2015-07-03_5595db3b5f057.png) 后台的权限比较大,什么资源都能管。 有的时候,为了安全考虑 很多产品把后台设计的很复杂。比方说,密码复杂度、验证码、错误几次输入密码后,锁死账号、子管理员账号不同权限等。 为了简单的实现安全,我后台干脆不用表存管理员账号,配置中写死。这样就不存在说注入的问题。也不会实现多管理员登录。然后密码弄稍微复杂的自己能记忆的密码。 账号:freelog 密码:jay2015 写在admin模块配置注释里防止忘了。 由于登录页没有导航这样公共的模块,所以所有模板代码直接写在一个html里。 我们看下代码: ~~~ <!DOCTYPE HTML> <!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> <!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]--> <!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]--> <!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]--> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1"> <meta name="renderer" content="webkit"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>{:C('WEB_SITE_TITLE')}</title> <script type="text/javascript" src="__BOWER__/jquery/dist/jquery.js"></script> <!--[if lt IE 9]> <script src="__STATIC__/html5shiv.js"></script> <script src="__STATIC__/respond.js"></script> <![endif]--> <script type="text/javascript" src="__BOWER__/bootstrap/dist/js/bootstrap.min.js"></script> <script type="text/javascript"> var url = ''; </script> <script type="text/javascript" src="__JS__/admin.js"></script> <link rel="stylesheet" type="text/css" href="__BOWER__/bootstrap/dist/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="__CSS__/admin_custom.css"> </head> <body> <!--[if lt IE 8]> <div class="browsehappy">当前网页 <strong>不支持</strong> 你正在使用的浏览器. 为了正常的访问, 请 <a href="http://browsehappy.com/">升级你的浏览器</a>.</div> <![endif]--> <div class="container-fluid"> <div class="row-fluid"> <div class="col-md-5"></div> <div class="col-md-2"> <form class="login-form" method="post" action="{:U('System/check')}"> <fieldset> <h3 class="welcome"><i class="login-logo"></i>freelog</h3> <div class="form-group"> <div class="controls"> <input type="text" name="loginName" placeholder="用户名" class="form-control" required title="请填写用户名"> </div> </div> <div class="form-group"> <div class="controls"> <input type="password" name="loginPwd" placeholder="密码" class="form-control" required title="请填写密码"> </div> </div> <div class="form-group"> <div class="controls"> <button class="btn-block primary ajax-post" type="submit">登录</button> <a class="btn-link" href="/">返回首页</a> </div> </div> </fieldset> </form> </div> <div class="col-md-5"></div> </div> </div> </body> </html> ~~~ ~~~ <!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> ~~~ 这样的写法叫浏览器条件注释,专门针对ie低版本的写法。 ~~~ <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1"> ~~~ 是为了保证兼容性,针对ie8的写法,意思是装了chrome的系统,ie打开时用chrome渲染,没装继续用ie的edge引擎渲染。 ~~~ <script type="text/javascript" src="__BOWER__/jquery/dist/jquery.js"></script> <!--[if lt IE 9]> <script src="__STATIC__/html5shiv.js"></script> <script src="__STATIC__/respond.js"></script> <![endif]--> <script type="text/javascript" src="__BOWER__/bootstrap/dist/js/bootstrap.min.js"></script> <script type="text/javascript"> var url = ''; </script> <script type="text/javascript" src="__JS__/admin.js"></script> <link rel="stylesheet" type="text/css" href="__BOWER__/bootstrap/dist/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="__CSS__/admin_custom.css"> ~~~ 引入一些常规的jquery、bootstrap组件和自己后台用的admin.js。 ~~~ <!--[if lt IE 8]> <div class="browsehappy">当前网页 <strong>不支持</strong> 你正在使用的浏览器. 为了正常的访问, 请 <a href="http://browsehappy.com/">升级你的浏览器</a>.</div> <![endif]--> ~~~ 做了一个ie8向下的提示更换浏览器的处理。让ie6、7去见鬼吧。 ~~~ <div class="container-fluid"> <div class="row-fluid"> <div class="col-md-5"></div> <div class="col-md-2"> <form class="login-form" method="post" action="{:U('System/check')}"> <fieldset> <h3 class="welcome"><i class="login-logo"></i>freelog</h3> <div class="form-group"> <div class="controls"> <input type="text" name="loginName" placeholder="用户名" class="form-control" required title="请填写用户名"> </div> </div> <div class="form-group"> <div class="controls"> <input type="password" name="loginPwd" placeholder="密码" class="form-control" required title="请填写密码"> </div> </div> <div class="form-group"> <div class="controls"> <button class="btn-block primary ajax-post" type="submit">登录</button> <a class="btn-link" href="/">返回首页</a> </div> </div> </fieldset> </form> </div> <div class="col-md-5"></div> </div> </div> ~~~ 这里,用了bootstrap的网格流动布局。为了让登录框居中, 我按照bootstrap默认表单宽度,大概算出它占2列,然后左右留 各5个空白的列。 当屏幕小于5列宽时就会 表单撑开,全宽,达到响应式。 ![2015-07-05/559897c7df75d](http://box.kancloud.cn/2015-07-05_559897c7df75d.png) 更小宽度也是如此。 ![2015-07-05/559897e0dd49f](http://box.kancloud.cn/2015-07-05_559897e0dd49f.png) 由于只是后台,没分太细的宽度,如果要匹配不同宽度,需要div上加 col-sm col-sm 之类的类去控制。后台登录页面没必要做太细。 后台登录逻辑和前台差不多,只不过不用查库了,直接配置里的键去比较表单项。 ~~~ /* 登录验证 */ public function check() { //接收数据 $loginName = trim(I('post.loginName')); $loginPwd = trim(I('post.loginPwd')); if (C('ADMIN.LOGIN_NAME') == $loginName) { if (md5($loginPwd) == C('ADMIN.PWD')) { $user = array( 'admin_id' => 1, 'admin_name' => $loginName, 'login_time' => NOW_TIME, //上次登录时间 ); //设置登录SESSION session(C('USER_AUTH_KEY'), $user); session(C('USER_AUTH_SIGN_KEY'), user_auth_sign($user)); $this->success('登录成功', U('System/index')); } else { $this->error('密码错误'); } } else { $this->error('该管理员不存在'); } } ~~~ 里面的签名方法复用了OneThink的。 然后注意的一点是 NOW_TIME 常量。每次获取当前时间戳用time() 多次用就浪费效率了。 所以TP定义了一个常量供框架使用。 `System/check` 比`User/login` 难让黑客们猜到。 # 导航 登录过后,就可以看见除了登录页面外,每个页面都有的导航 ![2015-07-05/55989d05a979a](http://box.kancloud.cn/2015-07-05_55989d05a979a.png) 我是在bootstrap3 默认的带下拉菜单的导航基础上加上反转背景色、加上子菜单箭头和高亮当前导航实现的效果。 ~~~ <div class="navbar navbar-inverse"> <div class="navbar-inner"> <div class="container-fluid"> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="{:U('System/index')}">首页 <span class="sr-only">(current)</span></a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">资源 <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <li><a href="{:U('Resource/tags')}">标签</a></li> <li><a href="{:U('Resource/pics')}">图片库</a></li> <li><a href="{:U('Resource/files')}">文件库</a></li> </ul> </li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">文章 <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <li><a href="{:U('Post/text')}">文章</a></li> <li><a href="{:U('Post/picture')}">图片</a></li> <li><a href="{:U('Post/music')}">音乐</a></li> <li><a href="{:U('Post/video')}">视频</a></li> </ul> </li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">微信 <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <li><a href="{:U('Weixin/index')}">服务器</a></li> <li><a href="{:U('Weixin/menu')}">菜单</a></li> </ul> </li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">配置 <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <li><a href="{:U('Config/group?id=1')}">网站设置</a></li> <li><a href="{:U('Config/index')}">配置列表</a></li> </ul> </li> </ul> <ul class="nav navbar-nav navbar-right"> <li class="divider-vertical"></li> <li><a href="javascript:;">{:get_admin_name()}</a></li> <li class="divider-vertical"></li> <li><a href="{:U('System/cleancache')}" class="ajax-get">清除缓存</a></li> <li class="divider-vertical"></li> <li><a href="{:U('System/logout')}">登出</a></li> <li class="divider-vertical"></li> <li><a href="/">网站</a></li> </ul> </div> <!-- /.nav-collapse --> </div> </div> <!-- /navbar-inner --> </div> ~~~ ![高亮效果](http://box.kancloud.cn/2015-07-05_55989d87ca32e.png) 高亮代码实现参考了OneThink的后台高亮。 先js获取当前url: ~~~ var url = window.location.pathname + window.location.search; url = url.replace(/(\/(p)\/\d+)|(&p=\d+)|(\/(id)\/\d+)|(&id=\d+)|(\/(group)\/\d+)|(&group=\d+)/, ""); url = url.replace('.html', ''); ~~~ 然后admin.js里定义一个高亮菜单的函数: ~~~ function highlight_menu(url) { $('.nav li').removeClass('active'); $('.nav .dropdown-menu>li>a[href*="'+url+'"]').parent().addClass('active').parents('.dropdown').addClass('active'); $('.nav-collapse .nav li a[href*="'+url+'"]').parent().addClass('active'); } ~~~ admin.js最后, 初始话时调用这个函数。 ~~~ $(function(){ //导航子页面高亮选中 highlight_menu(url); }); ~~~ # 配置 在我遇到typecho 时,我就被它的简洁所打败了。它的后台也是如此。因此,在我开发OneBlog时,我就用OneThink的功能,然后移植typecho的后台风格,包括登录页之类的。 像这样: ![2015-07-05/5598a16156584](http://box.kancloud.cn/2015-07-05_5598a16156584.png) 配置我是直接移植OneThink的模板加控制器。前面大家前台也看到了,同样的初始化里,读取数据库配置,合并到配置数组里去。 后台的模板代码我就不帖了,和OneBlog里的差不多。只不过继承的父模板不一样。 主要说一下保存。 OneBlog 里的配置,我是copy OneThink里的 控制器方法。 在freelog里,我们不是写了api了吗? 我就把那个配置里的保存方法,给他整合到api里面了。其他和OneBlog一样,列表、编辑页读数据什么的。 用过OneThink的开发人员都清楚,OneThink里的配置保存有两种,一种是所有配置保存,一种是单独配置项的保存。 ![全部配置](http://box.kancloud.cn/2015-07-05_5598a3333300d.png) ![某一项配置](http://box.kancloud.cn/2015-07-05_5598a356b256f.png) ![全部配置保存](http://box.kancloud.cn/2015-07-05_5598a3a283079.png) 全部配置保存的html里保存的是个多维数组,然后提交到 api.php/config/save方法里。 而单独配置的保存,我还是api的config,只不过提交时表单里带id,键名不是数组了,是配置项的对应键,如sort。 ![2015-07-05/5598a42869472](http://box.kancloud.cn/2015-07-05_5598a42869472.png) ![2015-07-05/5598a4412b180](http://box.kancloud.cn/2015-07-05_5598a4412b180.png) 因此,api里,config不能像普通表的curd那么去做。 我们回去看api的EmptyController ![2015-07-05/5598a49a8d3bb](http://box.kancloud.cn/2015-07-05_5598a49a8d3bb.png) 先定义了其他处理的资源,config。 然后控制器里单独实现config方法: ~~~ public function config($name = 0){ $model = D('Config'); $result = true; $data = array(); $code = 404; $url = ''; switch ($this->_method){ case 'head': break; case 'option': break; case 'get': // 列出资源 if('list' == $name){ $data = $model->select(); }else{ $id = intval($name); $data = $model->find($id); } if($model->getError() || $model->getDbError()){ $result = false; }else{ $code = 200; } break; case 'put': if('save' == $name){ // 批量更新资源 $config = I('put.config'); if(empty($config)){ $result = false; $data = '表单为空'; }else{ if($config && is_array($config)){ foreach ($config as $name => $value) { $map = array('name' => $name); $model->where($map)->setField('value', $value); } } S('DB_CONFIG_DATA',null); $code = 200; } }else{ $puts = $model->create(I('put.')); if(false === $puts){ $result = false; $data = $model->getError(); }else{ $id = $puts['id']; if($find = $model->find($id)){ $result = false !== $model->save($puts); $code = $result? 200: 404; }else{ $result = false; $data = "record not found"; $code = 412; } } } break; case 'post': // 新增资源 $posts = $model->create(); if(false == $posts){ $data = $model->getError(); $result = false; }else{ $id = $model->add(); if(!$id){ $result = false; }else{ $code = 201; $data = $id; $url = '/admin.php/Config/index'; } } break; case 'delete':// 删除资源 // parse_str(file_get_contents('php://input'), $_DELETE); // slog($_DELETE); $id = array_unique((array)I('get.id',0)); slog($id); if ( empty($id) ) { $code = 404; $data = '请选择要操作的数据'; }else{ $code = 200; $map = array('id' => array('in', $id) ); if(M('Config')->where($map)->delete()){ S('DB_CONFIG_DATA',null); //记录行为 $url = '/admin.php/Config/index'; $data = '删除成功'; } else { $code = 412; $result = false; $data = '删除失败!'; } } break; } if($result){ $this->success($data, $code, $url); }else{ $this->error($data, $code, $url); } } ~~~ 先看更新: ~~~ if('save' == $name){ // 批量更新资源 $config = I('put.config'); if(empty($config)){ $result = false; $data = '表单为空'; }else{ if($config && is_array($config)){ foreach ($config as $name => $value) { $map = array('name' => $name); $model->where($map)->setField('value', $value); } } $code = 200; } }else{ $puts = $model->create(I('put.')); if(false === $puts){ $result = false; $data = $model->getError(); }else{ $id = $puts['id']; if($find = $model->find($id)){ $result = false !== $model->save($puts); $code = $result? 200: 404; }else{ $result = false; $data = "record not found"; $code = 412; } } S('DB_CONFIG_DATA',null); } ~~~ 通过$name 是否为save,区分是批量更新还是单个更新。单个更新和之前所有数据表资源的更新一样。而批量更新,获取的是 I('put.config') 多维数组,并且通过遍历去更新每一个: ~~~ foreach ($config as $name => $value) { $map = array('name' => $name); $model->where($map)->setField('value', $value); } ~~~ 最后清除一下缓存。 # 资源 资源导航包括了文件、图片和标签。 我以文件为例,其他的都差不多,只是模板里个别字段不一样。 ![2015-07-05/55994a872cfab](http://box.kancloud.cn/2015-07-05_55994a872cfab.png) 模板resource/files.html代码: ~~~ <extend name="Public/base" /> <block name="body"> <div class="main-title location"> <h2>文件</h2> </div> <div class="data-table table-striped"></div> <div class="typecho-table-wrap"> <table class="table table-hover" id="del_table"> <thead> <tr> <th>ID</th> <th>原始名</th> <th>后缀</th> <th>路径</th> <th>大小</th> <th>上传时间</th> </tr> </thead> <tbody> <notempty name="list"> <volist name="list" id="file"> <tr> <td>{$file.id}</td> <td>{$file.savename}</td> <td>{$file.ext}</td> <td><a href="{$file.path}" target="_blank">{$file.path}</a></td> <td>{$file.size|format_bytes}</td> <td>{$file.create_time|date="Y-m-d h:i:s",###}</td> </tr> </volist> <else/> <td colspan="6" class="text-center"> aOh! 暂时还没有内容! </td> </notempty> </tbody> </table> </div> <!-- 分页 --> <div class="pagination"> <div class="pull-right"> {$_page} </div> </div> </block> ~~~ 处理了大小的格式化显示,和创建时间格式化。 控制器代码也简单: ~~~ <?php namespace Admin\Controller; use Think\Controller; class ResourceController extends CommonController { //标签 public function tags(){ /* 查询条件初始化 */ $map = array('status' => 1); if(isset($_GET['title'])){ $map['title'] = array('like', '%'.(string)I('title').'%'); } $this->meta_title = '标签'; $this->_list(array('source' => 'Tags', 'map' => $map, 'order' => '`id`')); } //图片 public function pics(){ /* 查询条件初始化 */ $map = array('status' => 1); $this->meta_title = '图片'; $this->_list(array('source' => 'Picture', 'map' => $map, 'order' => '`id`')); } public function files(){ /* 查询条件初始化 */ $map = array('status' => 1); $this->meta_title = '文件'; $this->_list(array('source' => 'File', 'map' => $map, 'order' => '`id`')); } } ~~~ 图片和标签只不过换了资源名,并且标签加了title搜索。 这一切复用了common控制器里的_list方法。 因为文件、图片、标签都是用户产生,本来后台就不应该有删除的行为。因为删除了文章里关联的数据就会有问题。 显示出来是为了方便审查,万一用户上传了黄色图片和音频怎么办。 读取时按照status=1处理的。到时候加删除也简单,通过删除链接和ajax类更新该记录为status=0就好了。 # 文章 文章,为了方便管理,将四种类型通过菜单区分管理列表。 ![2015-07-05/55994d96cf877](http://box.kancloud.cn/2015-07-05_55994d96cf877.png) 为了实现动态菜单, 控制器里方法也是通过空操纵实现: ~~~ <?php namespace Admin\Controller; use Think\Controller; class PostController extends CommonController { public function _empty($action = 'text'){ if(!in_array($action, array('text', 'picture', 'video', 'music'))) $this->error('错误的文章类型'); /* 查询条件初始化 */ $map = array(); $map['type'] = $action; if($search = I('get.title', '', 'trim')){ $map['_string'] = "`title` LIKE '%{$search}%' OR `description` LIKE '%{$search}' OR FIND_IN_SET('{$search}', tags)"; } $this->meta_title = '文章管理'; $this->_list(array('source' => 'Post', 'map' => $map, 'order' => '`id`', 'tpl'=>strtolower($action))); } } ~~~ 不过比之前标签搜索,提供多了一些字段,如描述、和标签。模板也是动态的。如文章模板。 ~~~ <extend name="Public/base" /> <block name="body"> <div class="main-title location"> <h2>文章</h2> </div> <div class="data-table table-striped"> <div class="clearfix"> <!-- 高级搜索 --> <div class="search-form pull-right clearfix form-inline"> <div class="input-group mb10"> <form action="{:U()}" method="GET"> <input type="text" name="title" class="form-control text-s" value="{:I('title')}" placeholder="请输入标签名称"> <span class="input-group-btn"> <button class="btn btn-default"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button> </span> </form> </div><!-- /input-group --> </div> </div> <div class="typecho-table-wrap"> <table class="table table-hover" id="del_table"> <thead> <tr> <th>ID</th> <th>标题</th> <th>描述</th> <th>作者</th> <th>标签</th> <th>浏览数</th> <th>最后更新时间</th> <th>操作</th> </tr> </thead> <tbody> <notempty name="list"> <volist name="list" id="post"> <tr> <td>{$post.id}</td> <td>{$post.title}</td> <td>{$post.description}</td> <td>{$post.author}</td> <td>{$post.tags}</td> <td>{$post.views}</td> <td>{$post.update_at}</td> <td><a href="/api.php/post?id={$post.id}" class="ajax-delete confirm">删除</a></td> </tr> </volist> <else/> <td colspan="6" class="text-center"> aOh! 暂时还没有内容! </td> </notempty> </tbody> </table> </div> <!-- 分页 --> <div class="pagination"> <div class="pull-right"> {$_page} </div> </div> </block> ~~~ 这里为了以后扩展方便预留了。每个类别文章没用一个模板,是为了以后实现不同的预览和编辑着想,先只显示基础数据吧。 至于搜索,一个get表单搞定: ~~~ <form action="{:U()}" method="GET"> <input type="text" name="title" class="form-control text-s" value="{:I('title')}" placeholder="请输入标签名称"> <span class="input-group-btn"> <button class="btn btn-default"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button> </span> </form> ~~~ 至于删除,和前台一样,交给api、ajax-delete。 `<a href="/api.php/post?id={$post.id}" class="ajax-delete confirm">删除</a>` # 微信 本来微信想做很多功能,实现像公众平台一样的功能(多媒体管理,消息查看)。可是后来尝试发现,个人开发者未认证用户,没那些高级接口的权限。 所以干脆,只做了2个功能页:微信服务器ip列表,和微信平台(iframe实现,要怎么管理自己登录去吧)。 ## 服务器ip ![效果](http://box.kancloud.cn/2015-07-05_5599509c92098.png) 直接看控制器 : ~~~ <?php namespace Admin\Controller; use Think\Controller; use Com\Wechat; use Com\WechatAuth; class WeixinController extends CommonController { //初始化方法 protected function _initialize() { if(!C('WEIXIN.APPID')){ $this->error('请先配置好微信'); } } //首页 public function index(){ $wechatauth = new WechatAuth(C('WEIXIN.APPID'), C('WEIXIN.SECRET'), C('')); $access_token = $wechatauth->getAccessToken(); $result = $wechatauth->getServerIp($access_token); if(isset($result['errcode'])) $this->error($result['errmsg']); slog($result); $this->assign('ip', $result['ip_list']); $this->display(); } //菜单 public function menu(){ $this->display(); } } ~~~ weixin/index.html 代码: ~~~ <extend name="Public/base" /> <block name="body"> <div class="col-md-4"></div> <div class="col-md-4"> <table class="table table-hover"> <thead><th>微信服务器IP:</th></thead> <tbody> <volist name="ip" id="vo"> <tr><td>{$vo}</td></tr> </volist> </tbody> </table> </div> <div class="col-md-4"></div> </block> ~~~ 我也是盲人摸象,摸索出WechatAuth类的使用。并且,自己扩展了一个getServerIp的方法: ~~~ public function getServerIp($token){ return $this->api('getcallbackip', array('access_token'=>$token)); } ~~~ 后面微信开发里会细讲。 ## 菜单 没啥技术含量,但是思路不错: ~~~ <extend name="Public/base" /> <block name="body"> <iframe width="100%" height="700" src="https://mp.weixin.qq.com/"></iframe> </block> ~~~