浏览代码

新增searchList使用Ajax进行渲染
新增独立的管理员日志管理
新增Ajax-Bootstrap-Select插件
新增Shortcut快捷操作列表栏
修复语言包标识为数字时的BUG
移除管理员列表中的密码字段
完善表格完整示例
修复tagsinput选中后显示为数字的BUG

Karson 8 年之前
父节点
当前提交
40667995b1
共有 40 个文件被更改,包括 650 次插入192 次删除
  1. 1 1
      application/admin/command/Crud/stubs/add.stub
  2. 1 1
      application/admin/command/Crud/stubs/edit.stub
  3. 3 2
      application/admin/command/Install/fastadmin.sql
  4. 17 20
      application/admin/common.php
  5. 2 6
      application/admin/controller/Ajax.php
  6. 1 1
      application/admin/controller/Index.php
  7. 1 0
      application/admin/controller/auth/Admin.php
  8. 148 0
      application/admin/controller/auth/Adminlog.php
  9. 4 0
      application/admin/library/Auth.php
  10. 2 2
      application/admin/view/auth/admin/add.html
  11. 1 1
      application/admin/view/auth/admin/edit.html
  12. 22 0
      application/admin/view/auth/adminlog/detail.html
  13. 21 0
      application/admin/view/auth/adminlog/index.html
  14. 1 1
      application/admin/view/auth/group/add.html
  15. 1 1
      application/admin/view/auth/group/edit.html
  16. 3 3
      application/admin/view/auth/rule/add.html
  17. 2 2
      application/admin/view/auth/rule/edit.html
  18. 5 5
      application/admin/view/common/header.html
  19. 2 2
      application/admin/view/example/bootstraptable/detail.html
  20. 2 0
      application/admin/view/example/bootstraptable/index.html
  21. 7 2
      application/admin/view/index/login.html
  22. 1 1
      application/admin/view/layout/default.html
  23. 3 3
      application/admin/view/page/add.html
  24. 2 2
      application/admin/view/page/edit.html
  25. 2 0
      application/common.php
  26. 10 10
      application/common/controller/Backend.php
  27. 2 1
      bower.json
  28. 20 0
      public/assets/css/backend.css
  29. 1 1
      public/assets/css/backend.min.css
  30. 22 8
      public/assets/js/backend.js
  31. 76 0
      public/assets/js/backend/auth/adminlog.js
  32. 16 11
      public/assets/js/backend/example/bootstraptable.js
  33. 10 3
      public/assets/js/backend/general/profile.js
  34. 34 1
      public/assets/js/backend/index.js
  35. 2 42
      public/assets/js/backend/page.js
  36. 22 17
      public/assets/js/bootstrap-table-commonsearch.js
  37. 1 0
      public/assets/js/require-backend.js
  38. 101 35
      public/assets/js/require-backend.min.js
  39. 54 7
      public/assets/js/require-form.js
  40. 24 0
      public/assets/less/backend.less

+ 1 - 1
application/admin/command/Crud/stubs/add.stub

@@ -1,4 +1,4 @@
-<form id="add-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action="">
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
 
 {%addList%}
     <div class="form-group hide layer-footer">

+ 1 - 1
application/admin/command/Crud/stubs/edit.stub

@@ -1,4 +1,4 @@
-<form id="edit-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action="">
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
 
 {%editList%}
     <div class="form-group hide layer-footer">

文件差异内容过多而无法显示
+ 3 - 2
application/admin/command/Install/fastadmin.sql


+ 17 - 20
application/admin/common.php

@@ -1,6 +1,5 @@
 <?php
 
-use app\admin\library\Auth;
 use app\common\model\Category;
 use app\common\model\Configvalue;
 use fast\Form;
@@ -99,32 +98,30 @@ function build_category_select($name, $type, $selected = null, $attr = [])
 
 /**
  * 生成表格操作按钮栏
- * @param array $btns
+ * @param array $btns 按钮组
+ * @param array $attr 按钮属性值
  * @return string
  */
-function build_toolbar($btns = NULL)
+function build_toolbar($btns = NULL, $attr = [])
 {
     $btns = $btns ? $btns : ['refresh', 'add', 'edit', 'delete'];
     $btns = is_array($btns) ? $btns : explode(',', $btns);
-    $addbtn = __('Add');
-    $editbtn = __('Edit');
-    $deletebtn = __('Delete');
+    $btnAttr = [
+        'refresh' => ['javascript:;', 'btn btn-primary btn-refresh', 'fa fa-refresh', ''],
+        'add'     => ['javascript:;', 'btn btn-success btn-add', 'fa fa-plus', __('Add')],
+        'edit'    => ['javascript:;', 'btn btn-success btn-edit btn-disabled disabled', 'fa fa-pencil', __('Edit')],
+        'delete'     => ['javascript:;', 'btn btn-danger btn-del btn-disabled disabled', 'fa fa-trash', __('Delete')],
+    ];
+    $btnAttr = array_merge($btnAttr, $attr);
     $html = [];
-    if (in_array('refresh', $btns))
+    foreach ($btns as $k => $v)
     {
-        $html[] = '<a class="btn btn-primary btn-refresh" ><i class="fa fa-refresh"></i></a>';
-    }
-    if (in_array('add', $btns))
-    {
-        $html[] = '<a class="btn btn-success btn-add" ><i class="fa fa-plus"></i> ' . $addbtn . '</a>';
-    }
-    if (in_array('edit', $btns))
-    {
-        $html[] = '<a class="btn btn-success btn-edit btn-disabled disabled" ><i class="fa fa-pencil"></i> ' . $editbtn . '</a>';
-    }
-    if (in_array('delete', $btns))
-    {
-        $html[] = '<a class="btn btn-danger btn-del btn-disabled disabled" ><i class="fa fa-trash"></i> ' . $deletebtn . '</a>';
+        if (!isset($btnAttr[$v]))
+        {
+            continue;
+        }
+        list($href, $class, $icon, $text) = $btnAttr[$v];
+        $html[] = '<a href="' . $href . '" class="' . $class . '" ><i class="' . $icon . '"></i> ' . $text . '</a>';
     }
     return implode(' ', $html);
 }

+ 2 - 6
application/admin/controller/Ajax.php

@@ -37,7 +37,8 @@ class Ajax extends Backend
             return;
         }
         $searchfield = 'name';
-        $field = substr($field, 0, -3);
+        $fieldArr = explode('_', $field);
+        $field = $fieldArr[0];
         switch ($field)
         {
             case 'category':
@@ -56,11 +57,6 @@ class Ajax extends Backend
                 ->field("id,{$searchfield} AS name")
                 ->select();
 
-        foreach ($searchlist as $k => &$v)
-        {
-            $v['name'] = $v['name'] . "[id:{$v['id']}]";
-        }
-        unset($v);
         $this->code = 1;
         $this->data = ['searchlist' => $searchlist];
     }

+ 1 - 1
application/admin/controller/Index.php

@@ -76,7 +76,7 @@ class Index extends Backend
             $result = $this->auth->login($username, $password, $keeplogin ? 86400 : 0);
             if ($result === true)
             {
-                $this->success(__('Login successful'), $url, ['url' => $url]);
+                $this->success(__('Login successful'), $url, ['url' => $url, 'id' => $this->auth->id, 'username' => $username, 'avatar' => $this->auth->avatar]);
                 return;
             }
             else

+ 1 - 0
application/admin/controller/auth/Admin.php

@@ -66,6 +66,7 @@ class Admin extends Backend
             $list = $this->model
                     ->where($where)
                     ->where('id', 'in', $childrenAdminIds)
+                    ->field(['password', 'salt', 'token'], true)
                     ->order($sort, $order)
                     ->limit($offset, $limit)
                     ->select();

+ 148 - 0
application/admin/controller/auth/Adminlog.php

@@ -0,0 +1,148 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use app\common\controller\Backend;
+use fast\Tree;
+
+/**
+ * 管理员日志
+ *
+ * @icon fa fa-users
+ * @remark 管理员可以查看自己所拥有的权限的管理员日志
+ */
+class Adminlog extends Backend
+{
+
+    protected $model = null;
+    //当前登录管理员所有子节点组别
+    protected $childrenIds = [];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('AdminLog');
+
+        $groups = $this->auth->getGroups();
+
+        // 取出所有分组
+        $grouplist = model('AuthGroup')->all(['status' => 'normal']);
+        $objlist = [];
+        foreach ($groups as $K => $v)
+        {
+            // 取出包含自己的所有子节点
+            $childrenlist = Tree::instance()->init($grouplist)->getChildren($v['id'], TRUE);
+            $obj = Tree::instance()->init($childrenlist)->getTreeArray($v['pid']);
+            $objlist = array_merge($objlist, Tree::instance()->getTreeList($obj));
+        }
+        $groupdata = [];
+        foreach ($objlist as $k => $v)
+        {
+            $groupdata[$v['id']] = $v['name'];
+        }
+        $this->childrenIds = array_keys($groupdata);
+        $this->view->assign('groupdata', $groupdata);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if ($this->request->isAjax())
+        {
+            $childrenAdminIds = model('AuthGroupAccess')
+                    ->field('uid')
+                    ->where('group_id', 'in', $this->childrenIds)
+                    ->column('uid');
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                    ->where($where)
+                    ->where('admin_id', 'in', $childrenAdminIds)
+                    ->order($sort, $order)
+                    ->count();
+
+            $list = $this->model
+                    ->where($where)
+                    ->where('admin_id', 'in', $childrenAdminIds)
+                    ->order($sort, $order)
+                    ->limit($offset, $limit)
+                    ->select();
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+    
+    /**
+     * 详情
+     */
+    public function detail($ids)
+    {
+        $row = $this->model->get(['id' => $ids]);
+        if (!$row)
+            $this->error(__('No Results were found'));
+        $this->view->assign("row", $row->toArray());
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     * @internal
+     */
+    public function add()
+    {
+        $this->code = -1;
+    }
+
+    /**
+     * 编辑
+     * @internal
+     */
+    public function edit($ids = NULL)
+    {
+        $this->code = -1;
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        $this->code = -1;
+        if ($ids)
+        {
+            $childrenGroupIds = $this->childrenIds;
+            $adminList = $this->model->where('id', 'in', $ids)->where('admin_id', 'in', function($query) use($childrenGroupIds) {
+                        $query->name('auth_group_access')->field('uid');
+                    })->select();
+            if ($adminList)
+            {
+                $deleteIds = [];
+                foreach ($adminList as $k => $v)
+                {
+                    $deleteIds[] = $v->id;
+                }
+                if ($deleteIds)
+                {
+                    $this->model->destroy($deleteIds);
+                    $this->code = 1;
+                }
+            }
+        }
+
+        return;
+    }
+
+    /**
+     * 批量更新
+     * @internal
+     */
+    public function multi($ids = "")
+    {
+        // 管理员禁止批量操作
+        $this->code = -1;
+    }
+
+}

+ 4 - 0
application/admin/library/Auth.php

@@ -221,6 +221,10 @@ class Auth extends \fast\Auth
         if ($path_rule_id)
         {
             $this->breadcrumb = Tree::instance()->init($this->rules)->getParents($path_rule_id, true);
+            foreach ($this->breadcrumb as $k => &$v)
+            {
+                $v['url'] = url($v['name']);
+            }
         }
         return $this->breadcrumb;
     }

+ 2 - 2
application/admin/view/auth/admin/add.html

@@ -20,13 +20,13 @@
     <div class="form-group">
         <label for="nickname" class="control-label col-xs-12 col-sm-2">{:__('Nickname')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="text" class="form-control" id="nickname" name="row[nickname]" value="" data-rule="required" />
+            <input type="text" class="form-control" id="nickname" name="row[nickname]" value="" data-rule="required;username" />
         </div>
     </div>
     <div class="form-group">
         <label for="password" class="control-label col-xs-12 col-sm-2">{:__('Password')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="password" class="form-control" id="password" name="row[password]" value="" data-rule="required" />
+            <input type="password" class="form-control" id="password" name="row[password]" value="" data-rule="required;password" />
         </div>
     </div>
     <div class="form-group">

+ 1 - 1
application/admin/view/auth/admin/edit.html

@@ -26,7 +26,7 @@
     <div class="form-group">
         <label for="password" class="control-label col-xs-12 col-sm-2">{:__('Password')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="password" class="form-control" id="password" name="row[password]" value="" />
+            <input type="password" class="form-control" id="password" name="row[password]" value="" data-rule="password" />
         </div>
     </div>
     <div class="form-group">

+ 22 - 0
application/admin/view/auth/adminlog/detail.html

@@ -0,0 +1,22 @@
+<table class="table table-striped">
+    <thead>
+        <tr>
+            <th>标题</th>
+            <th>内容</th>
+        </tr>
+    </thead>
+    <tbody>
+        {volist name="row" id="vo"  }
+            <tr>
+                <td>{$key}</td>
+                <td>{$vo}</td>
+            </tr>
+        {/volist}
+    </tbody>
+</table>
+<div class="hide layer-footer">
+    <label class="control-label col-xs-12 col-sm-2"></label>
+    <div class="col-xs-12 col-sm-8">
+        <button type="reset" class="btn btn-primary btn-embossed btn-close" onclick="Layer.closeAll();">{:__('Close')}</button>
+    </div>
+</div>

+ 21 - 0
application/admin/view/auth/adminlog/index.html

@@ -0,0 +1,21 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar('refresh,delete')}
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover" width="100%">
+
+                    </table>
+
+
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 1 - 1
application/admin/view/auth/group/add.html

@@ -9,7 +9,7 @@
     <div class="form-group">
         <label for="username" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="text" class="form-control" id="name" name="row[name]" value="" required="required" />
+            <input type="text" class="form-control" id="name" name="row[name]" value="" data-rule="required" />
         </div>
     </div>
     <div class="form-group">

+ 1 - 1
application/admin/view/auth/group/edit.html

@@ -9,7 +9,7 @@
     <div class="form-group">
         <label for="username" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="text" class="form-control" id="name" name="row[name]" value="{$row.name}" required="required" />
+            <input type="text" class="form-control" id="name" name="row[name]" value="{$row.name}" data-rule="required" />
         </div>
     </div>
     <div class="form-group">

+ 3 - 3
application/admin/view/auth/rule/add.html

@@ -18,13 +18,13 @@
     <div class="form-group">
         <label for="name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="text" class="form-control" id="name" name="row[name]" value="" required="required" />
+            <input type="text" class="form-control" id="name" name="row[name]" value="" data-rule="required" />
         </div>
     </div>
     <div class="form-group">
         <label for="module" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="text" class="form-control" id="title" name="row[title]" value="" required="required" />
+            <input type="text" class="form-control" id="title" name="row[title]" value="" data-rule="required" />
         </div>
     </div>
     <div class="form-group">
@@ -39,7 +39,7 @@
     <div class="form-group">
         <label for="weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="text" class="form-control" id="weigh" name="row[weigh]" value="0" required="required" />
+            <input type="text" class="form-control" id="weigh" name="row[weigh]" value="0" data-rule="required" />
         </div>
     </div>
     <div class="form-group">

+ 2 - 2
application/admin/view/auth/rule/edit.html

@@ -14,13 +14,13 @@
     <div class="form-group">
         <label for="name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="text" class="form-control" id="name" name="row[name]" value="{$row.name}" required="required" />
+            <input type="text" class="form-control" id="name" name="row[name]" value="{$row.name}" data-rule="required" />
         </div>
     </div>
     <div class="form-group">
         <label for="action" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="text" class="form-control" id="title" name="row[title]" value="{$row.title}" required="required" />
+            <input type="text" class="form-control" id="title" name="row[title]" value="{$row.title}" data-rule="required" />
         </div>
     </div>
     <div class="form-group">

+ 5 - 5
application/admin/view/common/header.html

@@ -68,10 +68,10 @@
                 <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-language"></i></a>
                 <ul class="dropdown-menu">
                     <li class="{$config['language']=='zh-cn'?'active':''}">
-                        <a href="?lang=zh-cn">简体中文</a>
+                        <a href="?ref=addtabs&lang=zh-cn">简体中文</a>
                     </li>
                     <li class="{$config['language']=='en'?'active':''}">
-                        <a href="?lang=en">English</a>
+                        <a href="?ref=addtabs&lang=en">English</a>
                     </li>
                 </ul>
             </li>
@@ -100,13 +100,13 @@
                     <li class="user-body">
                         <div class="row">
                             <div class="col-xs-4 text-center">
-                                <a href="#">Followers</a>
+                                <a href="http://www.fastadmin.net" target="_blank">官网</a>
                             </div>
                             <div class="col-xs-4 text-center">
-                                <a href="#">Sales</a>
+                                <a href="http://forum.fastadmin.net" target="_blank">论坛</a>
                             </div>
                             <div class="col-xs-4 text-center">
-                                <a href="#">Friends</a>
+                                <a href="http://doc.fastadmin.net" target="_blank">文档</a>
                             </div>
                         </div>
                     </li>

+ 2 - 2
application/admin/view/example/bootstraptable/detail.html

@@ -1,8 +1,8 @@
 <table class="table table-striped">
     <thead>
         <tr>
-            <th>标题</th>
-            <th>内容</th>
+            <th>{:__('Title')}</th>
+            <th>{:__('Content')}</th>
         </tr>
     </thead>
     <tbody>

+ 2 - 0
application/admin/view/example/bootstraptable/index.html

@@ -7,6 +7,8 @@
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
                         {:build_toolbar('refresh,delete')}
+                        <a class="btn btn-info btn-disabled disabled btn-selected" href="javascript:;"><i class="fa fa-leaf"></i> 获取选中项</a>
+                        <a class="btn btn-warning btn-singlesearch" href="javascript:;"><i class="fa fa-leaf"></i> 单独设置搜索条件</a>
                         <div class="dropdown btn-group">
                             <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> <?= __('More') ?></a>
                             <ul class="dropdown-menu text-left" role="menu">

+ 7 - 2
application/admin/view/index/login.html

@@ -9,6 +9,9 @@
                 background:url('http://img.infinitynewtab.com/wallpaper/{:date("Ymd")%4000}.jpg');
                 background-size:cover;
             }
+            a {
+                color:#fff;
+            }
             .login-panel{margin-top:150px;}
             .login-screen {
                 max-width:400px;
@@ -70,16 +73,18 @@
                         <div class="login-form">
                             <img id="profile-img" class="profile-img-card" src="__CDN__/assets/img/avatar.png" />
                             <p id="profile-name" class="profile-name-card"></p>
+                            
                             <form action="" method="post" id="login-form">
+                                <div id="errtips" class="hide"></div>
                                 {:token()}
                                 <div class="input-group">
                                     <div class="input-group-addon"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></div>
-                                    <input type="text" class="form-control" id="pd-form-username" placeholder="" name="username" autocomplete="off" value="admin" />
+                                    <input type="text" class="form-control" id="pd-form-username" placeholder="{:__('Username')}" name="username" autocomplete="off" value="" data-rule="{:__('Username')}:required;username" />
                                 </div>
 
                                 <div class="input-group">
                                     <div class="input-group-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></div>
-                                    <input type="password" class="form-control" id="pd-form-password" placeholder="" name="password" autocomplete="off" value="123456" />
+                                    <input type="password" class="form-control" id="pd-form-password" placeholder="{:__('Password')}" name="password" autocomplete="off" value="" data-rule="{:__('Password')}:required;password" />
                                 </div>
 
                                 <div class="form-group">

+ 1 - 1
application/admin/view/layout/default.html

@@ -24,7 +24,7 @@
                                 </ol>
                                 <ol class="breadcrumb pull-right">
                                     {foreach $breadcrumb as $vo}
-                                    <li>{$vo.title}</li>
+                                    <li><a href="javascript:;" data-url="{$vo.url}">{$vo.title}</a></li>
                                     {/foreach}
                                 </ol>
                             </div>

+ 3 - 3
application/admin/view/page/add.html

@@ -2,13 +2,13 @@
     <div class="form-group">
         <label for="c-category_id" class="control-label col-xs-12 col-sm-2">{:__('Category_id')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-category_id" class="form-control typeahead" name="row[category_id]" type="text" value="0">
+            <input id="c-category_id" class="form-control typeahead" name="row[category_id]" type="text" value="0" data-rule="required">
         </div>
     </div>
     <div class="form-group">
         <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-title" class="form-control" name="row[title]" type="text" value="">
+            <input id="c-title" class="form-control" name="row[title]" type="text" value="" data-rule="required" >
         </div>
     </div>
     <div class="form-group">
@@ -27,7 +27,7 @@
         <label for="c-image" class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
         <div class="col-xs-12 col-sm-8">
             <div class="form-inline">
-                <input id="c-image" class="form-control" size="50" name="row[image]" type="text" value="">
+                <input id="c-image" class="form-control" size="50" name="row[image]" type="text" value="" data-rule="required">
                 <span><button id="plupload-image" class="btn btn-danger plupload" data-input-id="c-image" data-preview-id="p-image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                 <span><button type="button" id="fachoose-image" class="btn btn-primary fachoose" data-multiple="false" data-input-id="c-image"><i class="fa fa-list-ul"></i> {:__('Choose')}</button></span>
                 <ul class="row list-inline plupload-preview" id="p-image"></ul>

+ 2 - 2
application/admin/view/page/edit.html

@@ -3,13 +3,13 @@
     <div class="form-group">
         <label for="c-category_id" class="control-label col-xs-12 col-sm-2">{:__('Category_id')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-category_id" class="form-control typeahead" name="row[category_id]" type="text" value="{$row.category_id}">
+            <input id="c-category_id" class="form-control typeahead" name="row[category_id]" type="text" value="{$row.category_id}" data-rule="required">
         </div>
     </div>
     <div class="form-group">
         <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-title" class="form-control" name="row[title]" type="text" value="{$row.title}">
+            <input id="c-title" class="form-control" name="row[title]" type="text" value="{$row.title}" data-rule="required" >
         </div>
     </div>
     <div class="form-group">

+ 2 - 0
application/common.php

@@ -17,6 +17,8 @@ if (!function_exists('__'))
      */
     function __($name, $vars = [], $lang = '')
     {
+        if (is_numeric($name))
+            return $name;
         if (!is_array($vars))
         {
             $vars = func_get_args();

+ 10 - 10
application/common/controller/Backend.php

@@ -91,16 +91,6 @@ class Backend extends Controller
         // 定义是否AJAX请求
         !defined('IS_AJAX') && define('IS_AJAX', $this->request->isAjax());
 
-        // 非选项卡时重定向
-        if (!$this->request->isPost() && !IS_AJAX && !IS_ADDTABS && !IS_DIALOG && input("ref") == 'addtabs')
-        {
-            $url = preg_replace_callback("/([\?|&]+)ref=addtabs(&?)/i", function($matches) {
-                return $matches[2] == '&' ? $matches[1] : '';
-            }, $this->request->url());
-            $this->redirect('index/index', [], 302, ['referer' => $url]);
-            exit;
-        }
-
         $this->auth = Auth::instance();
 
         // 设置当前请求的URI
@@ -125,6 +115,16 @@ class Backend extends Controller
                 }
             }
         }
+        
+        // 非选项卡时重定向
+        if (!$this->request->isPost() && !IS_AJAX && !IS_ADDTABS && !IS_DIALOG && input("ref") == 'addtabs')
+        {
+            $url = preg_replace_callback("/([\?|&]+)ref=addtabs(&?)/i", function($matches) {
+                return $matches[2] == '&' ? $matches[1] : '';
+            }, $this->request->url());
+            $this->redirect('index/index', [], 302, ['referer' => $url]);
+            exit;
+        }
 
         // 设置面包屑导航数据
         $breadcrumb = $this->auth->getBreadCrumb($path);

+ 2 - 1
bower.json

@@ -33,7 +33,8 @@
     "typeahead.js": "^0.11.1",
     "bootstrap-tagsinput": "^0.8.0",
     "nice-validator": "^1.0.10",
-    "art-template": "^3.0.1"
+    "art-template": "^3.0.1",
+    "ajax-bootstrap-select": "^1.3.8"
   },
   "devDependencies": {
     "dragsort": "https://github.com/karsonzhang/dragsort.git",

+ 20 - 0
public/assets/css/backend.css

@@ -46,6 +46,12 @@ body {
   /*width: 70%;*/
   max-width: 885px;
 }
+.form-group .bootstrap-tagsinput {
+  display: inherit;
+}
+.form-group .bootstrap-tagsinput span.twitter-typeahead {
+  width: auto;
+}
 #header {
   background: #fff;
   box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(0, 0, 0, 0.05);
@@ -77,6 +83,16 @@ body {
   top: 62px;
   right: 12px;
 }
+.bootstrap-select .status {
+  background: #f0f0f0;
+  clear: both;
+  color: #999;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 1;
+  margin-bottom: -5px;
+  padding: 10px 20px;
+}
 /*
  * RIBBON
  */
@@ -279,6 +295,10 @@ body {
 .sidebar-menu > li .badge {
   margin-top: 0;
 }
+.sidebar-collapse .user-panel > .image img {
+  width: 25px;
+  height: 25px;
+}
 #treeview .jstree-container-ul .jstree-node {
   display: block;
   clear: both;

文件差异内容过多而无法显示
+ 1 - 1
public/assets/css/backend.min.css


+ 22 - 8
public/assets/js/backend.js

@@ -215,13 +215,7 @@ define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang', 'moment'], function ($
                 $colorNums = colorArr.length;
                 badgeList = {};
                 $.each(params, function (k, v) {
-                    if (k.indexOf('/') > -1)
-                    {
-                        $url = Backend.api.fixurl(k);
-                    } else
-                    {
-                        $url = k;
-                    }
+                    $url = Backend.api.fixurl(k);
 
                     if ($.isArray(v))
                     {
@@ -238,7 +232,7 @@ define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang', 'moment'], function ($
                     badgeList[$url] = $nums > 0 ? '<small class="' + $class + ' pull-right bg-' + $color + '">' + $nums + '</small>' : '';
                 });
                 $.each(badgeList, function (k, v) {
-                    var anchor = top.window.$(".treeview li a[addtabs][url='" + k + "']");
+                    var anchor = top.window.$("li a[addtabs][url='" + k + "']");
                     if (anchor) {
                         top.window.$(".pull-right-container", anchor).html(v);
                         top.window.$(".nav-addtabs li a[node-id='" + anchor.attr("addtabs") + "'] .pull-right-container").html(v);
@@ -356,10 +350,30 @@ define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang', 'moment'], function ($
                 Backend.api.open(Backend.api.fixurl($(this).attr('href')), $(this).attr('title'));
                 e.preventDefault();
             });
+            //点击包含.btn-addtabs的元素时事件
             $(document).on('click', '.btn-addtabs,.addtabsit', function (e) {
                 Backend.api.addtabs($(this).attr("href"), $(this).attr("title"));
                 e.preventDefault();
             });
+            //点击加入到Shortcut
+            $(document).on('click', '#ribbon ol li:last a[data-url]', function (e) {
+                e.preventDefault();
+                var fastjump = top.window.$(".fastmenujump");
+                if (fastjump) {
+                    var url = $(this).data("url");
+                    var text = $(this).text();
+                    if (fastjump.find("option[value='" + url + "']").size() == 0) {
+                        fastjump.append("<option value='" + url + "'>" + $(this).text() + "</option>");
+                        var shortcut = localStorage.getItem("shortcut");
+                        shortcut = shortcut ? JSON.parse(shortcut) : {};
+                        shortcut[url] = text;
+                        localStorage.setItem("shortcut", JSON.stringify(shortcut));
+                        Toastr.success(__('Operation completed'));
+                    }
+                } else {
+                    Toastr.error(__('Operation failed'));
+                }
+            });
         }
     };
     //将Layer暴露到全局中去

+ 76 - 0
public/assets/js/backend/auth/adminlog.js

@@ -0,0 +1,76 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'auth/adminlog/index',
+                    add_url: '',
+                    edit_url: '',
+                    del_url: 'auth/adminlog/del',
+                    multi_url: 'auth/adminlog/multi',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                columns: [
+                    [
+                        {field: 'state', checkbox: true, },
+                        {field: 'id', title: 'ID', operate: false},
+                        {field: 'username', title: __('Username'), formatter: Table.api.formatter.search},
+                        {field: 'title', title: __('Title'), operate: 'LIKE %...%', placeholder: '模糊搜索,*表示任意字符', style: 'width:200px'},
+                        {field: 'url', title: __('Url'), align: 'left', formatter: Controller.api.formatter.url},
+                        {field: 'ip', title: __('IP'), events: Controller.api.events.ip, formatter: Controller.api.formatter.ip},
+                        {field: 'browser', title: __('Browser'), operate: false, events: Controller.api.events.browser, formatter: Controller.api.formatter.browser},
+                        {field: 'createtime', title: __('Create time'), formatter: Table.api.formatter.datetime, operate: 'BETWEEN', type: 'datetime', addclass: 'datetimepicker', data: 'data-date-format="YYYY-MM-DD HH:mm:ss"'},
+                        {field: 'operate', title: __('Operate'), events: Controller.api.events.operate, formatter: Controller.api.formatter.operate}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            },
+            formatter: {
+                url: function (value, row, index) {
+                    return '<div class="input-group input-group-sm" style="width:250px;"><input type="text" class="form-control input-sm" value="' + value + '"><span class="input-group-btn input-group-sm"><a href="' + value + '" target="_blank" class="btn btn-default btn-sm"><i class="fa fa-link"></i></a></span></div>';
+                },
+                ip: function (value, row, index) {
+                    return '<a class="btn btn-xs btn-ip bg-success"><i class="fa fa-map-marker"></i> ' + value + '</a>';
+                },
+                browser: function (value, row, index) {
+                    return '<a class="btn btn-xs btn-browser">' + row.useragent.split(" ")[0] + '</a>';
+                },
+                operate: function (value, row, index) {
+                    return '<a class="btn btn-info btn-xs btn-detail">' + __('Detail') + '</a> '
+                            + Table.api.formatter.operate(value, row, index, $("#table"));
+                },
+            },
+            events: {
+                ip: {
+                    'click .btn-ip': function (e, value, row, index) {
+                        var options = $("#table").bootstrapTable('getOptions');
+                        //这里我们手动将数据填充到表单然后提交
+                        $("#commonSearchContent_" + options.idTable + " form [name='ip']").val(value);
+                        $("#commonSearchContent_" + options.idTable + " form").trigger('submit');
+                    }
+                },
+                operate: $.extend({
+                    'click .btn-detail': function (e, value, row, index) {
+                        Backend.api.open('auth/adminlog/detail/ids/' + row['id'], __('Detail'));
+                    }
+                }, Table.api.events.operate)
+            }
+        }
+    };
+    return Controller;
+});

+ 16 - 11
public/assets/js/backend/example/bootstraptable.js

@@ -22,17 +22,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     [
                         {field: 'state', checkbox: true, },
                         {field: 'id', title: 'ID', operate: false},
-                        //使用Table.api.formatter.search可直接响应搜索
+                        //直接响应搜索
                         {field: 'username', title: __('Username'), formatter: Table.api.formatter.search},
-                        {field: 'title', title: __('Title'),
-                            operate: 'LIKE %...%',
-                            placeholder: '模糊搜索,*表示任意字符',
-                            style: 'width:200px',
-                            process: function (value, arg) {
-                                return value.replace(/\*/g, '%');
-                            }
-                        },
-                        {field: 'url', title: __('Url'), align: 'left', formatter: Controller.api.formatter.url},
+                        //模糊搜索
+                        {field: 'title', title: __('Title'), operate: 'LIKE %...%', placeholder: '模糊搜索,*表示任意字符', style: 'width:200px'},
+                        //通过Ajax渲染searchList
+                        {field: 'url', title: __('Url'), align: 'left', searchList: $.getJSON('ajax/typeahead?search=a&field=row[user_id]'), formatter: Controller.api.formatter.url},
                         //点击IP时同时执行搜索此IP,同时普通搜索使用下拉列表的形式
                         {field: 'ip', title: __('IP'), searchList: ['127.0.0.1', '127.0.0.2'], events: Controller.api.events.ip, formatter: Controller.api.formatter.ip},
                         //browser是一个不存在的字段
@@ -54,6 +49,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
 
             // 为表格绑定事件
             Table.api.bindevent(table);
+
+            //指定搜索条件
+            $(document).on("click", ".btn-singlesearch", function () {
+                table.bootstrapTable('refresh', {query: {filter: JSON.stringify({url: '/admin/index/login.html'}), op: JSON.stringify({url: '='})}});
+            });
+
+            //获取选中项
+            $(document).on("click", ".btn-selected", function () {
+                Layer.alert(JSON.stringify(table.bootstrapTable('getSelections')));
+            });
         },
         add: function () {
             Controller.api.bindevent();
@@ -80,7 +85,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     //返回字符串加上Table.api.formatter.operate的结果
                     //默认需要按需显示排序/编辑/删除按钮,则需要在Table.api.formatter.operate将table传入
                     //传入了table以后如果edit_url为空则不显示编辑按钮,如果del_url为空则不显显删除按钮
-                    return '<a class="btn btn-info btn-xs btn-detail">' + __('Detail') + '</a> '
+                    return '<a class="btn btn-info btn-xs btn-detail"><i class="fa fa-list"></i> ' + __('Detail') + '</a> '
                             + Table.api.formatter.operate(value, row, index, $("#table"));
                 },
             },

+ 10 - 3
public/assets/js/backend/general/profile.js

@@ -26,12 +26,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'upload'], function (
                     [
                         {field: 'id', title: 'ID'},
                         {field: 'title', title: __('Title')},
-                        {field: 'url', title: __('Url'), align: 'left', formatter: Table.api.formatter.url},
+                        {field: 'url', title: __('Url'), align: 'left', formatter: Controller.api.formatter.url},
                         {field: 'ip', title: __('ip')},
-                        {field: 'username', title: __('Userame')},
                         {field: 'createtime', title: __('Createtime'), formatter: Table.api.formatter.datetime},
                     ]
-                ]
+                ],
+                commonSearch: false
             });
 
             // 为表格绑定事件
@@ -47,6 +47,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'upload'], function (
                 var url = Backend.api.cdnurl(response.url);
                 $(".profile-user-img").prop("src", url);
             };
+        },
+        api: {
+            formatter: {
+                url: function (value, row, index) {
+                    return '<div class="input-group input-group-sm" style="width:250px;"><input type="text" class="form-control input-sm" value="' + value + '"><span class="input-group-btn input-group-sm"><a href="' + value + '" target="_blank" class="btn btn-default btn-sm"><i class="fa fa-link"></i></a></span></div>';
+                },
+            },
         }
     };
     return Controller;

+ 34 - 1
public/assets/js/backend/index.js

@@ -140,6 +140,17 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
                 }
             });
 
+            var shortcut = localStorage.getItem("shortcut");
+            shortcut = shortcut ? JSON.parse(shortcut) : {};
+            $.each(shortcut, function (i, j) {
+                $("select.fastmenujump").append("<option value='" + i + "'>" + j + "</option>");
+            });
+            $(document).on("change", "select.fastmenujump", function () {
+                if (!$(this).val())
+                    return;
+                Backend.api.addtabs($(this).val(), $("option:selected", this).text());
+            });
+
             //绑定tabs事件
             $('#nav').addtabs({iframeHeight: "100%"});
 
@@ -189,7 +200,7 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
                 //Fix the problem with right sidebar and layout boxed
                 if (cls == "layout-boxed")
                     AdminLTE.controlSidebar._fix($(".control-sidebar-bg"));
-                if ($('body').hasClass('fixed') && cls == 'fixed') {
+                if ($('body').hasClass('fixed') && cls == 'fixed' && false) {
                     AdminLTE.pushMenu.expandOnHover();
                     AdminLTE.layout.activate();
                 }
@@ -328,7 +339,29 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
             $(window).resize();
         },
         login: function () {
+            var lastlogin = localStorage.getItem("lastlogin");
+            if (lastlogin) {
+                lastlogin = JSON.parse(lastlogin);
+                $("#profile-img").attr("src", Backend.api.cdnurl(lastlogin.avatar));
+                $("#pd-form-username").val(lastlogin.username);
+            }
+
+            //让错误提示框居中
+            Backend.config.toastr.positionClass = "toast-top-center";
+
+            //本地验证未通过时提示
+            $("#login-form").data("validator-options", {
+                invalid: function (form, errors) {
+                    $.each(errors, function (i, j) {
+                        Toastr.error(j);
+                    });
+                },
+                target: '#errtips'
+            });
+
+            //为表单绑定事件
             Form.api.bindevent($("#login-form"), null, function (data) {
+                localStorage.setItem("lastlogin", JSON.stringify({id: data.id, username: data.username, avatar: data.avatar}));
                 location.href = Backend.api.fixurl(data.url);
             });
         }

+ 2 - 42
public/assets/js/backend/page.js

@@ -25,14 +25,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         {field: 'state', checkbox: true},
                         {field: 'id', title: __('Id'), operate: false},
                         {field: 'category_id', title: __('Category_id'), operate: '='},
-                        {field: 'title', title: __('Title'), 
-                            operate: 'LIKE %...%', 
-                            placeholder: '标题,模糊搜索,*表示任意字符', 
-                            style: 'width:200px',
-                            process: function (value, arg) {
-                                return value.replace(/\*/g, '%'); //仅演示用法
-                            }
-                        },
+                        {field: 'title', title: __('Title'), operate: 'LIKE %...%', placeholder: '关键字,模糊搜索'},
                         {field: 'keywords', title: __('Keywords'), operate: 'LIKE %...%', placeholder: '关键字,模糊搜索'},
                         {field: 'flag', title: __('Flag'), formatter: Table.api.formatter.flag, operate: false},
                         {field: 'image', title: __('Image'), formatter: Table.api.formatter.image, operate: false},
@@ -42,7 +35,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         {field: 'weigh', title: __('Weigh'), operate: false},
                         {field: 'status', title: __('Status'), formatter: Table.api.formatter.status, searchList: {'normal': __('Normal'), 'hidden': __('Hidden')}, style: 'min-width:100px;'},
                         {field: 'createtime', title: __('Create Time'), formatter: Table.api.formatter.datetime, operate: 'BETWEEN', type: 'datetime', addclass: 'datetimepicker', data: 'data-date-format="YYYY-MM-DD"'},
-                        {field: 'operate', title: __('Operate'), events: Controller.api.events.operate, formatter: Controller.api.formatter.operate}
+                        {field: 'operate', title: __('Operate'), events: Table.api.events.operate, formatter: Table.api.formatter.operate}
                     ]
                 ],
                 //普通搜索
@@ -61,40 +54,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         },
         api: {
             bindevent: function () {
-                $("form[role=form]").validator({
-                    rules: {
-                        mobile: [/^1[3-9]\d{9}$/, "请填写有效的手机号"],
-                        chinese: [/^[\u0391-\uFFE5]+$/, "请填写中文字符"],
-                        // 使用函数定义规则
-                        phone: function (elem, param) {
-                            return /^1[3458]\d{9}$/.test($(elem).val()) || '请检查手机号格式';
-                        },
-                        image: function (elem, param) {
-                            return /^\/(.*)\.(jpg|jpeg|png|gif)$/.test($(elem).val()) || '请上传有并行的图片文件';
-                        }
-                    },
-                    messages: {
-                    },
-                    fields: {
-                        'row[title]': "required;length(3~16)",
-                        'row[image]': "required;image",
-                        'row[views]': "required;range[0~100]",
-                        'row[content]': "required"
-                    },
-                });
                 Form.api.bindevent($("form[role=form]"));
-            },
-            formatter: {
-                operate: function (value, row, index) {
-                    return '<a class="btn btn-info btn-xs btn-detail">' + __('Detail') + '</a> ' + Table.api.formatter.operate(value, row, index);
-                },
-            },
-            events: {
-                operate: $.extend({
-                    'click .btn-detail': function (e, value, row, index) {
-                        Backend.api.open("page/detail/" + value, __('Detail'));
-                    }
-                }, Table.api.events.operate)
             }
         }
     };

+ 22 - 17
public/assets/js/bootstrap-table-commonsearch.js

@@ -76,16 +76,26 @@
                 ColumnsForSearch.push(vObjCol);
                 htmlForm.push('<div class="form-group" style="margin:5px">');
                 htmlForm.push(sprintf('<label for="%s" class="control-label" style="padding:0 10px">%s</label>', vObjCol.field, vObjCol.title));
-                //htmlForm.push('<div class="col-sm-2">');
-                //htmlForm.push(sprintf('<select class="form-control" name="field-%s" data-name="%s">%s</select>', vObjCol.field, vObjCol.field, selectHtml));
                 vObjCol.operate = (typeof vObjCol.operate === 'undefined' || $.inArray(vObjCol.operate, opList) === -1) ? '=' : vObjCol.operate;
                 htmlForm.push(sprintf('<input type="hidden" class="form-control operate" name="field-%s" data-name="%s" value="%s" readonly>', vObjCol.field, vObjCol.field, vObjCol.operate));
-                //htmlForm.push('</div>');
 
-                //htmlForm.push('<div class="col-sm-8">');
                 var style = typeof vObjCol.style === 'undefined' ? '' : sprintf('style="%s"', vObjCol.style);
                 if (vObjCol.searchList) {
-                    if (typeof vObjCol.searchList == 'function') {
+                    if (typeof vObjCol.searchList === 'object' && typeof vObjCol.searchList.then === 'function') {
+                        htmlForm.push(sprintf('<select class="form-control" name="%s" %s>%s</select>', vObjCol.field, style, sprintf('<option value="">%s</option>', that.options.formatCommonChoose())));
+                        (function (vObjCol, options) {
+                            $.when(vObjCol.searchList).done(function (ret) {
+                                if (ret.data && ret.data.searchlist && $.isArray(ret.data.searchlist)) {
+                                    var optionList = [];
+                                    $.each(ret.data.searchlist, function (key, value) {
+                                        var isSelect = value.id === vObjCol.defaultValue ? 'selected' : '';
+                                        optionList.push(sprintf("<option value='" + value.id + "' %s>" + value.name + "</option>", isSelect));
+                                    });
+                                    $("#commonSearchForm_" + options.idTable + " select[name='" + vObjCol.field + "']").append(optionList.join(''));
+                                }
+                            });
+                        })(vObjCol, that.options);
+                    } else if (typeof vObjCol.searchList == 'function') {
                         htmlForm.push(vObjCol.searchList.call(this, vObjCol));
                     } else {
                         var isArray = vObjCol.searchList.constructor === Array;
@@ -112,7 +122,6 @@
                     }
                 }
 
-                //htmlForm.push('</div>');
                 htmlForm.push('</div>');
             }
         }
@@ -273,17 +282,13 @@
             }
         });
 
-        var searchquery = getSearchQuery(this);
-        this.options.queryParams = function (params) {
-            return {
-                search: params.search,
-                sort: params.sort,
-                order: params.order,
-                filter: JSON.stringify(searchquery.filter),
-                op: JSON.stringify(searchquery.op),
-                offset: params.offset,
-                limit: params.limit,
-            };
+        var searchQuery = getSearchQuery(this);
+        var queryParams = this.options.queryParams;
+        this.options.queryParams = function () {
+            var params = queryParams.apply(this, arguments);
+            params.filter = JSON.stringify($.extend(params.filter || {}, searchQuery.filter));
+            params.op = JSON.stringify($.extend(params.op || {}, searchQuery.op));
+            return params;
         };
 
     };

+ 1 - 0
public/assets/js/require-backend.js

@@ -29,6 +29,7 @@ require.config({
         'bootstrap-dialog': '../libs/bootstrap3-dialog/dist/js/bootstrap-dialog.min',
         'bootstrap-datetimepicker': '../libs/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min',
         'bootstrap-select': '../libs/bootstrap-select/dist/js/bootstrap-select.min',
+        'bootstrap-select-ajax': '../libs/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.min',
         'bootstrap-table': '../libs/bootstrap-table/dist/bootstrap-table.min',
         'bootstrap-table-export': '../libs/bootstrap-table/dist/extensions/export/bootstrap-table-export.min',
         'bootstrap-table-mobile': '../libs/bootstrap-table/dist/extensions/mobile/bootstrap-table-mobile',

+ 101 - 35
public/assets/js/require-backend.min.js

@@ -43,6 +43,7 @@ require.config({
         'bootstrap-dialog': '../libs/bootstrap3-dialog/dist/js/bootstrap-dialog.min',
         'bootstrap-datetimepicker': '../libs/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min',
         'bootstrap-select': '../libs/bootstrap-select/dist/js/bootstrap-select.min',
+        'bootstrap-select-ajax': '../libs/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.min',
         'bootstrap-table': '../libs/bootstrap-table/dist/bootstrap-table.min',
         'bootstrap-table-export': '../libs/bootstrap-table/dist/extensions/export/bootstrap-table-export.min',
         'bootstrap-table-mobile': '../libs/bootstrap-table/dist/extensions/mobile/bootstrap-table-mobile',
@@ -6634,13 +6635,7 @@ define('backend',['jquery', 'bootstrap', 'toastr', 'layer', 'lang', 'moment'], f
                 $colorNums = colorArr.length;
                 badgeList = {};
                 $.each(params, function (k, v) {
-                    if (k.indexOf('/') > -1)
-                    {
-                        $url = Backend.api.fixurl(k);
-                    } else
-                    {
-                        $url = k;
-                    }
+                    $url = Backend.api.fixurl(k);
 
                     if ($.isArray(v))
                     {
@@ -6657,7 +6652,7 @@ define('backend',['jquery', 'bootstrap', 'toastr', 'layer', 'lang', 'moment'], f
                     badgeList[$url] = $nums > 0 ? '<small class="' + $class + ' pull-right bg-' + $color + '">' + $nums + '</small>' : '';
                 });
                 $.each(badgeList, function (k, v) {
-                    var anchor = top.window.$(".treeview li a[addtabs][url='" + k + "']");
+                    var anchor = top.window.$("li a[addtabs][url='" + k + "']");
                     if (anchor) {
                         top.window.$(".pull-right-container", anchor).html(v);
                         top.window.$(".nav-addtabs li a[node-id='" + anchor.attr("addtabs") + "'] .pull-right-container").html(v);
@@ -6775,10 +6770,30 @@ define('backend',['jquery', 'bootstrap', 'toastr', 'layer', 'lang', 'moment'], f
                 Backend.api.open(Backend.api.fixurl($(this).attr('href')), $(this).attr('title'));
                 e.preventDefault();
             });
+            //点击包含.btn-addtabs的元素时事件
             $(document).on('click', '.btn-addtabs,.addtabsit', function (e) {
                 Backend.api.addtabs($(this).attr("href"), $(this).attr("title"));
                 e.preventDefault();
             });
+            //点击加入到Shortcut
+            $(document).on('click', '#ribbon ol li:last a[data-url]', function (e) {
+                e.preventDefault();
+                var fastjump = top.window.$(".fastmenujump");
+                if (fastjump) {
+                    var url = $(this).data("url");
+                    var text = $(this).text();
+                    if (fastjump.find("option[value='" + url + "']").size() == 0) {
+                        fastjump.append("<option value='" + url + "'>" + $(this).text() + "</option>");
+                        var shortcut = localStorage.getItem("shortcut");
+                        shortcut = shortcut ? JSON.parse(shortcut) : {};
+                        shortcut[url] = text;
+                        localStorage.setItem("shortcut", JSON.stringify(shortcut));
+                        Toastr.success(__('Operation completed'));
+                    }
+                } else {
+                    Toastr.error(__('Operation failed'));
+                }
+            });
         }
     };
     //将Layer暴露到全局中去
@@ -7234,7 +7249,7 @@ return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)
         var vModal = sprintf("<div id=\"commonSearchContent_%s\" class=\"common-search-table %s\">", that.options.idTable, that.options.searchFormVisible ? "" : "hidden");
         vModal += vFormCommon.join('');
         vModal += "</div>";
-        $("#myTabContent").before($(vModal));
+        that.$container.prepend($(vModal));
 
         var form = $("#commonSearchForm" + "_" + that.options.idTable);
 
@@ -7292,16 +7307,26 @@ return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)
                 ColumnsForSearch.push(vObjCol);
                 htmlForm.push('<div class="form-group" style="margin:5px">');
                 htmlForm.push(sprintf('<label for="%s" class="control-label" style="padding:0 10px">%s</label>', vObjCol.field, vObjCol.title));
-                //htmlForm.push('<div class="col-sm-2">');
-                //htmlForm.push(sprintf('<select class="form-control" name="field-%s" data-name="%s">%s</select>', vObjCol.field, vObjCol.field, selectHtml));
                 vObjCol.operate = (typeof vObjCol.operate === 'undefined' || $.inArray(vObjCol.operate, opList) === -1) ? '=' : vObjCol.operate;
                 htmlForm.push(sprintf('<input type="hidden" class="form-control operate" name="field-%s" data-name="%s" value="%s" readonly>', vObjCol.field, vObjCol.field, vObjCol.operate));
-                //htmlForm.push('</div>');
 
-                //htmlForm.push('<div class="col-sm-8">');
                 var style = typeof vObjCol.style === 'undefined' ? '' : sprintf('style="%s"', vObjCol.style);
                 if (vObjCol.searchList) {
-                    if (typeof vObjCol.searchList == 'function') {
+                    if (typeof vObjCol.searchList === 'object' && typeof vObjCol.searchList.then === 'function') {
+                        htmlForm.push(sprintf('<select class="form-control" name="%s" %s>%s</select>', vObjCol.field, style, sprintf('<option value="">%s</option>', that.options.formatCommonChoose())));
+                        (function (vObjCol, options) {
+                            $.when(vObjCol.searchList).done(function (ret) {
+                                if (ret.data && ret.data.searchlist && $.isArray(ret.data.searchlist)) {
+                                    var optionList = [];
+                                    $.each(ret.data.searchlist, function (key, value) {
+                                        var isSelect = value.id === vObjCol.defaultValue ? 'selected' : '';
+                                        optionList.push(sprintf("<option value='" + value.id + "' %s>" + value.name + "</option>", isSelect));
+                                    });
+                                    $("#commonSearchForm_" + options.idTable + " select[name='" + vObjCol.field + "']").append(optionList.join(''));
+                                }
+                            });
+                        })(vObjCol, that.options);
+                    } else if (typeof vObjCol.searchList == 'function') {
                         htmlForm.push(vObjCol.searchList.call(this, vObjCol));
                     } else {
                         var isArray = vObjCol.searchList.constructor === Array;
@@ -7328,7 +7353,6 @@ return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)
                     }
                 }
 
-                //htmlForm.push('</div>');
                 htmlForm.push('</div>');
             }
         }
@@ -7397,7 +7421,7 @@ return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)
                     var value = $("[name='" + name + "']:checked").val();
                 }
             } else {
-                var value = (typeof vObjCol.process === 'function') ? vObjCol.process(obj.val()) : obj.val();
+                var value = (typeof vObjCol.process === 'function') ? vObjCol.process(obj.val()) : (sym == 'LIKE %...%' ? obj.val().replace(/\*/g, '%') : obj.val());
             }
             if (value == '' && sym.indexOf("NULL") == -1) {
                 return true;
@@ -7489,17 +7513,13 @@ return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)
             }
         });
 
-        var searchquery = getSearchQuery(this);
-        this.options.queryParams = function (params) {
-            return {
-                search: params.search,
-                sort: params.sort,
-                order: params.order,
-                filter: JSON.stringify(searchquery.filter),
-                op: JSON.stringify(searchquery.op),
-                offset: params.offset,
-                limit: params.limit,
-            };
+        var searchQuery = getSearchQuery(this);
+        var queryParams = this.options.queryParams;
+        this.options.queryParams = function () {
+            var params = queryParams.apply(this, arguments);
+            params.filter = JSON.stringify($.extend(params.filter || {}, searchQuery.filter));
+            params.op = JSON.stringify($.extend(params.op || {}, searchQuery.op));
+            return params;
         };
 
     };
@@ -8757,7 +8777,6 @@ define('upload',['jquery', 'bootstrap', 'backend', 'plupload', 'dragsort', 'temp
 
         that.each(function() {
             var instance = $(this).data(NS);
-
             if (instance) {
                 if ( isString(options) ) {
                     if ( options.charAt(0) === '_' ) return;
@@ -10938,7 +10957,7 @@ define('form',['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'
                 return false;
             },
             bindevent: function (form, onBeforeSubmit, onAfterSubmit) {
-                form.validator({
+                form.validator($.extend({
                     validClass: 'has-success',
                     invalidClass: 'has-error',
                     bindClassTo: '.form-group',
@@ -10972,12 +10991,48 @@ define('form',['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'
                         });
                         return false;
                     }
-                });
+                }, form.data("validator-options") || {}));
 
                 //绑定select元素事件
                 if ($(".selectpicker", form).size() > 0) {
-                    require(['bootstrap-select'], function () {
-                        $('.selectpicker', form).selectpicker();
+                    require(['bootstrap-select', 'bootstrap-select-ajax'], function () {
+                        var selectlist = $('.selectpicker', form).selectpicker();
+                        $.each(selectlist, function () {
+                            var that = this;
+                            if ($(this).data("live-search")) {
+                                $(this).ajaxSelectPicker({
+                                    ajax: {
+                                        url: 'ajax/selectpicker',
+                                        beforeSend: function (xhr, setting) {
+                                            setting.url = Backend.api.fixurl(setting.url);
+                                        },
+                                        data: function () {
+                                            var params = {
+                                                search: '{{{q}}}',
+                                                field: $(that).attr("name")
+                                            };
+                                            return params;
+                                        },
+                                        dataType: 'json'
+                                    },
+                                    locale: {
+                                        emptyTitle: 'Search...'
+                                    },
+                                    preprocessData: function (ret) {
+                                        var list = [];
+                                        if (ret.hasOwnProperty('data') && ret.data) {
+                                            var len = ret.data.length;
+                                            for (var i = 0; i < len; i++) {
+                                                var curr = ret.data[i];
+                                                list.push(curr);
+                                            }
+                                        }
+                                        return list;
+                                    },
+                                    preserveSelected: true
+                                });
+                            }
+                        });
                     });
                 }
 
@@ -10994,11 +11049,12 @@ define('form',['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'
                 if ($(".typeahead").size() > 0 || $(".tagsinput").size() > 0) {
                     require(['bloodhound'], function () {
                         var remotesource = function (input) {
+                            var url = $(input).data("url") ? $(input).data("url") : "ajax/typeahead";
                             return new Bloodhound({
                                 datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'),
                                 queryTokenizer: Bloodhound.tokenizers.whitespace,
                                 remote: {
-                                    url: 'ajax/typeahead?search=%QUERY&field=' + $(input).attr("name"),
+                                    url: url + '?search=%QUERY&field=' + $(input).attr("name"),
                                     wildcard: '%QUERY',
                                     transform: function (ret) {
                                         return ret.data.searchlist;
@@ -11037,11 +11093,12 @@ define('form',['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'
                                 $('.tagsinput', form).each(function () {
                                     $(this).tagsinput({
                                         freeInput: false,
+                                        itemValue: 'id',
+                                        itemText: 'name',
                                         typeaheadjs: {
                                             name: 'tagsinput',
                                             limit: 20,
                                             displayKey: 'name',
-                                            valueKey: 'id',
                                             source: remotesource(this),
                                             templates: {
                                                 empty: '<li class="notfound">' + __('No matches found') + '</li>',
@@ -11052,7 +11109,6 @@ define('form',['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'
                                         }
                                     });
                                 });
-                                $('.bootstrap-tagsinput .twitter-typeahead').css('display', 'inline');
                             });
                         }
                     });
@@ -11088,6 +11144,16 @@ define('form',['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'
                         $(".summernote", form).summernote({
                             height: 250,
                             lang: 'zh-CN',
+                            fontNames: [
+                                'Arial', 'Arial Black', 'Serif', 'Sans', 'Courier',
+                                'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact', 'Lucida Grande',
+                                "Open Sans", "Hiragino Sans GB", "Microsoft YaHei",
+                                '微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆',
+                            ],
+                            fontNamesIgnoreCheck: [
+                                "Open Sans", "Microsoft YaHei",
+                                '微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆'
+                            ],
                             dialogsInBody: true,
                             callbacks: {
                                 onChange: function (contents) {

+ 54 - 7
public/assets/js/require-form.js

@@ -67,7 +67,7 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'], func
                 return false;
             },
             bindevent: function (form, onBeforeSubmit, onAfterSubmit) {
-                form.validator({
+                form.validator($.extend({
                     validClass: 'has-success',
                     invalidClass: 'has-error',
                     bindClassTo: '.form-group',
@@ -101,12 +101,48 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'], func
                         });
                         return false;
                     }
-                });
+                }, form.data("validator-options") || {}));
 
                 //绑定select元素事件
                 if ($(".selectpicker", form).size() > 0) {
-                    require(['bootstrap-select'], function () {
-                        $('.selectpicker', form).selectpicker();
+                    require(['bootstrap-select', 'bootstrap-select-ajax'], function () {
+                        var selectlist = $('.selectpicker', form).selectpicker();
+                        $.each(selectlist, function () {
+                            var that = this;
+                            if ($(this).data("live-search")) {
+                                $(this).ajaxSelectPicker({
+                                    ajax: {
+                                        url: 'ajax/selectpicker',
+                                        beforeSend: function (xhr, setting) {
+                                            setting.url = Backend.api.fixurl(setting.url);
+                                        },
+                                        data: function () {
+                                            var params = {
+                                                search: '{{{q}}}',
+                                                field: $(that).attr("name")
+                                            };
+                                            return params;
+                                        },
+                                        dataType: 'json'
+                                    },
+                                    locale: {
+                                        emptyTitle: 'Search...'
+                                    },
+                                    preprocessData: function (ret) {
+                                        var list = [];
+                                        if (ret.hasOwnProperty('data') && ret.data) {
+                                            var len = ret.data.length;
+                                            for (var i = 0; i < len; i++) {
+                                                var curr = ret.data[i];
+                                                list.push(curr);
+                                            }
+                                        }
+                                        return list;
+                                    },
+                                    preserveSelected: true
+                                });
+                            }
+                        });
                     });
                 }
 
@@ -123,11 +159,12 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'], func
                 if ($(".typeahead").size() > 0 || $(".tagsinput").size() > 0) {
                     require(['bloodhound'], function () {
                         var remotesource = function (input) {
+                            var url = $(input).data("url") ? $(input).data("url") : "ajax/typeahead";
                             return new Bloodhound({
                                 datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'),
                                 queryTokenizer: Bloodhound.tokenizers.whitespace,
                                 remote: {
-                                    url: 'ajax/typeahead?search=%QUERY&field=' + $(input).attr("name"),
+                                    url: url + '?search=%QUERY&field=' + $(input).attr("name"),
                                     wildcard: '%QUERY',
                                     transform: function (ret) {
                                         return ret.data.searchlist;
@@ -166,11 +203,12 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'], func
                                 $('.tagsinput', form).each(function () {
                                     $(this).tagsinput({
                                         freeInput: false,
+                                        itemValue: 'id',
+                                        itemText: 'name',
                                         typeaheadjs: {
                                             name: 'tagsinput',
                                             limit: 20,
                                             displayKey: 'name',
-                                            valueKey: 'id',
                                             source: remotesource(this),
                                             templates: {
                                                 empty: '<li class="notfound">' + __('No matches found') + '</li>',
@@ -181,7 +219,6 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'], func
                                         }
                                     });
                                 });
-                                $('.bootstrap-tagsinput .twitter-typeahead').css('display', 'inline');
                             });
                         }
                     });
@@ -217,6 +254,16 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'], func
                         $(".summernote", form).summernote({
                             height: 250,
                             lang: 'zh-CN',
+                            fontNames: [
+                                'Arial', 'Arial Black', 'Serif', 'Sans', 'Courier',
+                                'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact', 'Lucida Grande',
+                                "Open Sans", "Hiragino Sans GB", "Microsoft YaHei",
+                                '微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆',
+                            ],
+                            fontNamesIgnoreCheck: [
+                                "Open Sans", "Microsoft YaHei",
+                                '微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆'
+                            ],
                             dialogsInBody: true,
                             callbacks: {
                                 onChange: function (contents) {

+ 24 - 0
public/assets/less/backend.less

@@ -67,6 +67,13 @@ body {
     max-width:885px;
 }
 
+.form-group .bootstrap-tagsinput {
+    display: inherit;
+    span.twitter-typeahead {
+        width:auto;
+    }
+}
+
 #header {
     background: #fff;
     box-shadow: 0 2px 2px rgba(0,0,0,.05),0 1px 0 rgba(0,0,0,.05);
@@ -102,6 +109,17 @@ body {
     right:12px;
 }
 
+.bootstrap-select .status {
+    background: #f0f0f0;
+    clear: both;
+    color: #999;
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 1;
+    margin-bottom: -5px;
+    padding: 10px 20px;
+}
+
 /*
  * RIBBON
  */
@@ -326,6 +344,12 @@ body {
         margin-top:0;
     }
 }
+.sidebar-collapse {
+    .user-panel > .image img{
+        width:25px;
+        height:25px;
+    }
+}
 #treeview {
     .jstree-container-ul .jstree-node{
         display:block;clear:both;