Forráskód Böngészése

新增兼容无前缀表生成CRUD
新增CRUD自定义参数
新增左侧菜单可控制显示功能
新增部分表格操作示例
新增管理员列表可查看所属组别
新增multi方法可自定义过滤的字段
新增表格内元素.btn-change的类功能
视图文件表单提交按钮新增disabled
完善邮件发送和测试功能
修复上传在特殊情况下会返回错误的BUG
修复弹出框在特定情况下被遮挡的BUG
修复在默认页设置错误导致后台异常的BUG

Karson 8 éve
szülő
commit
26f95f19bd
54 módosított fájl, 588 hozzáadás és 265 törlés
  1. 140 38
      application/admin/command/Crud.php
  2. 1 1
      application/admin/command/Crud/stubs/add.stub
  3. 1 1
      application/admin/command/Crud/stubs/edit.stub
  4. 2 2
      application/admin/command/Crud/stubs/model.stub
  5. 2 2
      application/admin/command/Crud/stubs/relationmodel.stub
  6. 35 6
      application/admin/controller/Ajax.php
  7. 20 3
      application/admin/controller/auth/Admin.php
  8. 1 10
      application/admin/controller/auth/Rule.php
  9. 11 1
      application/admin/controller/example/Bootstraptable.php
  10. 21 14
      application/admin/controller/general/Config.php
  11. 1 0
      application/admin/lang/zh-cn.php
  12. 13 12
      application/admin/lang/zh-cn/general/attachment.php
  13. 35 32
      application/admin/lang/zh-cn/general/config.php
  14. 5 1
      application/admin/library/traits/Backend.php
  15. 1 1
      application/admin/view/auth/admin/add.html
  16. 1 1
      application/admin/view/auth/admin/edit.html
  17. 1 1
      application/admin/view/auth/group/add.html
  18. 1 1
      application/admin/view/auth/group/edit.html
  19. 1 1
      application/admin/view/auth/rule/add.html
  20. 1 1
      application/admin/view/auth/rule/edit.html
  21. 1 1
      application/admin/view/category/add.html
  22. 1 1
      application/admin/view/category/edit.html
  23. 2 0
      application/admin/view/example/bootstraptable/index.html
  24. 1 1
      application/admin/view/general/attachment/edit.html
  25. 2 2
      application/admin/view/general/config/index.html
  26. 1 1
      application/admin/view/general/configvalue/add.html
  27. 1 1
      application/admin/view/general/configvalue/edit.html
  28. 1 1
      application/admin/view/general/crontab/add.html
  29. 1 1
      application/admin/view/general/crontab/edit.html
  30. 1 1
      application/admin/view/page/add.html
  31. 1 1
      application/admin/view/page/edit.html
  32. 1 1
      application/admin/view/user/third/add.phtml
  33. 1 1
      application/admin/view/user/third/edit.phtml
  34. 1 1
      application/admin/view/user/user/add.phtml
  35. 1 1
      application/admin/view/user/user/edit.phtml
  36. 1 1
      application/admin/view/version/add.html
  37. 1 1
      application/admin/view/version/edit.html
  38. 1 1
      application/admin/view/wechat/autoreply/add.html
  39. 1 1
      application/admin/view/wechat/autoreply/edit.html
  40. 1 1
      application/admin/view/wechat/config/add.html
  41. 1 1
      application/admin/view/wechat/config/edit.html
  42. 1 1
      application/admin/view/wechat/response/add.html
  43. 1 1
      application/admin/view/wechat/response/edit.html
  44. 5 0
      application/common/controller/Backend.php
  45. 174 69
      application/common/library/Email.php
  46. 3 8
      public/assets/js/backend.js
  47. 1 0
      public/assets/js/backend/auth/admin.js
  48. 33 17
      public/assets/js/backend/auth/rule.js
  49. 23 1
      public/assets/js/backend/example/bootstraptable.js
  50. 4 11
      public/assets/js/backend/general/config.js
  51. 1 1
      public/assets/js/backend/index.js
  52. 0 1
      public/assets/js/bootstrap-table-commonsearch.js
  53. 3 2
      public/assets/js/require-form.js
  54. 22 3
      public/assets/js/require-table.js

+ 140 - 38
application/admin/command/Crud.php

@@ -27,22 +27,32 @@ class Crud extends Command
     /**
      * Enum类型识别为单选框的结尾字符,默认会识别为单选下拉列表
      */
-    protected $enumRadioSuffix = 'data';
+    protected $enumRadioSuffix = ['data', 'state', 'status'];
 
     /**
      * Set类型识别为复选框的结尾字符,默认会识别为多选下拉列表
      */
-    protected $setCheckboxSuffix = 'data';
+    protected $setCheckboxSuffix = ['data', 'state', 'status'];
 
     /**
-     * Int类型识别为日期时间的结尾字符,默认会识别为数字文本框
+     * Int类型识别为日期时间的结尾字符,默认会识别为日期文本框
      */
-    protected $intDateSuffix = 'time';
+    protected $intDateSuffix = ['time'];
 
     /**
      * 开关后缀
      */
-    protected $switchSuffix = 'switch';
+    protected $switchSuffix = ['switch'];
+
+    /**
+     * Selectpage对应的后缀
+     */
+    protected $selectpageSuffix = ['_id', '_ids'];
+
+    /**
+     * Selectpage多选对应的后缀
+     */
+    protected $selectpagesSuffix = ['_ids'];
 
     /**
      * 以指定字符结尾的字段格式化函数
@@ -98,6 +108,16 @@ class Crud extends Command
                 ->addOption('mode', 'o', Option::VALUE_OPTIONAL, 'relation table mode,hasone or belongsto', 'belongsto')
                 ->addOption('delete', 'd', Option::VALUE_OPTIONAL, 'delete all files generated by CRUD', null)
                 ->addOption('menu', 'u', Option::VALUE_OPTIONAL, 'create menu when CRUD completed', null)
+                ->addOption('setcheckboxsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate checkbox component with suffix', null)
+                ->addOption('enumradiosuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate radio component with suffix', null)
+                ->addOption('imagefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate image component with suffix', null)
+                ->addOption('filefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate file component with suffix', null)
+                ->addOption('intdatesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate date component with suffix', null)
+                ->addOption('switchsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate switch component with suffix', null)
+                ->addOption('selectpagesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate selectpage component with suffix', null)
+                ->addOption('selectpagessuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate multiple selectpage component with suffix', null)
+                ->addOption('sortfield', null, Option::VALUE_OPTIONAL, 'sort field', null)
+                ->addOption('editorclass', null, Option::VALUE_OPTIONAL, 'automatically generate editor class', null)
                 ->setDescription('Build CRUD controller and model from table');
     }
 
@@ -130,6 +150,47 @@ class Crud extends Command
         $relationForeignKey = $input->getOption('relationforeignkey');
         //主键
         $relationPrimaryKey = $input->getOption('relationprimarykey');
+        //复选框后缀
+        $setcheckboxsuffix = $input->getOption('setcheckboxsuffix');
+        //单选框后缀
+        $enumradiosuffix = $input->getOption('enumradiosuffix');
+        //图片后缀
+        $imagefield = $input->getOption('imagefield');
+        //文件后缀
+        $filefield = $input->getOption('filefield');
+        //日期后缀
+        $intdatesuffix = $input->getOption('intdatesuffix');
+        //开关后缀
+        $switchsuffix = $input->getOption('switchsuffix');
+        //selectpage后缀
+        $selectpagesuffix = $input->getOption('selectpagesuffix');
+        //selectpage多选后缀
+        $selectpagessuffix = $input->getOption('selectpagessuffix');
+        //排序字段
+        $sortfield = $input->getOption('sortfield');
+        //编辑器Class
+        $editorclass = $input->getOption('editorclass');
+        if ($setcheckboxsuffix)
+            $this->setCheckboxSuffix = $setcheckboxsuffix;
+        if ($enumradiosuffix)
+            $this->enumRadioSuffix = $enumradiosuffix;
+        if ($imagefield)
+            $this->imageField = $imagefield;
+        if ($filefield)
+            $this->fileField = $filefield;
+        if ($intdatesuffix)
+            $this->intDateSuffix = $intdatesuffix;
+        if ($switchsuffix)
+            $this->switchSuffix = $switchsuffix;
+        if ($selectpagesuffix)
+            $this->selectpageSuffix = $selectpagesuffix;
+        if ($selectpagessuffix)
+            $this->selectpagesSuffix = $selectpagessuffix;
+        if ($editorclass)
+            $this->editorClass = $editorclass;
+        if ($sortfield)
+            $this->sortField = $sortfield;
+
         //如果有启用关联模式
         if ($relation && !in_array($mode, ['hasone', 'belongsto']))
         {
@@ -140,14 +201,22 @@ class Crud extends Command
         $prefix = Config::get('database.prefix');
 
         //检查主表
-        $tableName = $prefix . $table;
+        $tableName = $table;
+        $modelTableType = 'table';
         $tableInfo = Db::query("SHOW TABLE STATUS LIKE '{$tableName}'", [], TRUE);
         if (!$tableInfo)
         {
-            throw new Exception("table not found");
+            $tableName = $prefix . $table;
+            $modelTableType = 'name';
+            $tableInfo = Db::query("SHOW TABLE STATUS LIKE '{$tableName}'", [], TRUE);
+            if (!$tableInfo)
+            {
+                throw new Exception("table not found");
+            }
         }
         $tableInfo = $tableInfo[0];
-
+        
+        $relationModelTableType = 'table';
         //检查关联表
         if ($relation)
         {
@@ -155,7 +224,13 @@ class Crud extends Command
             $relationTableInfo = Db::query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], TRUE);
             if (!$relationTableInfo)
             {
-                throw new Exception("relation table not found");
+                $relationTableName = $relation;
+                $relationModelTableType = 'name';
+                $relationTableInfo = Db::query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], TRUE);
+                if (!$relationTableInfo)
+                {
+                    throw new Exception("relation table not found");
+                }
             }
         }
 
@@ -438,8 +513,9 @@ class Crud extends Command
                     {
                         $fieldName = $inputType == 'checkbox' ? $fieldName .= "[]" : $fieldName;
                         $attrArr['name'] = "row[{$fieldName}]";
-                        $itemArr = $this->getLangArray($itemArr, FALSE);
+
                         $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $inputType);
+                        $itemArr = $this->getLangArray($itemArr, FALSE);
                         //添加一个获取器
                         $this->getAttr($getAttrArr, $field, $inputType);
                         $this->appendAttr($appendAttrList, $field);
@@ -478,20 +554,21 @@ class Crud extends Command
                     {
                         $search = $replace = '';
                         //特殊字段为关联搜索
-                        if (substr($field, -3) == '_id' || substr($field, -4) == '_ids')
+                        if ($this->isMatchSuffix($field, $this->selectpageSuffix))
                         {
                             $inputType = 'text';
                             $defaultValue = '';
                             $attrArr['data-rule'] = 'required';
                             $cssClassArr[] = 'selectpage';
                             $attrArr['data-db-table'] = substr($field, 0, strripos($field, '_'));
+                            //如果是类型表需要特殊处理下
                             if ($attrArr['data-db-table'] == 'category')
                             {
                                 $attrArr['data-params'] = '##replacetext##';
                                 $search = '"##replacetext##"';
                                 $replace = '\'{"custom[type]":"' . $table . '"}\'';
                             }
-                            if (substr($field, -4) == '_ids')
+                            if ($this->isMatchSuffix($field, $this->selectpagesSuffix))
                             {
                                 $attrArr['data-multiple'] = 'true';
                             }
@@ -508,13 +585,9 @@ class Crud extends Command
                         $step = array_intersect($cssClassArr, ['selectpage']) ? 0 : $step;
                         $attrArr['class'] = implode(' ', $cssClassArr);
                         $isUpload = false;
-                        foreach (array_merge($this->imageField, $this->fileField) as $m => $n)
+                        if ($this->isMatchSuffix($field, array_merge($this->imageField, $this->fileField)))
                         {
-                            if (preg_match("/{$n}$/i", $field))
-                            {
-                                $isUpload = true;
-                                break;
-                            }
+                            $isUpload = true;
                         }
                         //如果是步长则加上步长
                         if ($step)
@@ -556,11 +629,8 @@ class Crud extends Command
                         $javascriptList[] = "{checkbox: true}";
                     }
                     //构造JS列信息
-                    $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE']);
-                    if ($inputType && in_array($inputType, ['select', 'checkbox', 'radio']))
-                    {
-                        $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], '_text');
-                    }
+                    $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], $inputType && in_array($inputType, ['select', 'checkbox', 'radio']) ? '_text' : '');
+
                     //排序方式,如果有指定排序字段,否则按主键排序
                     $order = $field == $this->sortField ? $this->sortField : $order;
                 }
@@ -631,7 +701,9 @@ class Crud extends Command
                 'createTime'              => in_array('createtime', $fieldArr) ? "'createtime'" : 'false',
                 'updateTime'              => in_array('updatetime', $fieldArr) ? "'updatetime'" : 'false',
                 'modelTableName'          => $table,
+                'modelTableType'          => $modelTableType,
                 'relationModelTableName'  => $relation,
+                'relationModelTableType'  => $relationModelTableType,
                 'relationModelName'       => $relationModelName,
                 'relationWith'            => '',
                 'relationMethod'          => '',
@@ -842,8 +914,12 @@ EOD;
                 $itemArr = [$field => $fieldLang];
                 foreach (explode(',', $item) as $k => $v)
                 {
-                    list($key, $value) = explode('=', $v);
-                    $itemArr[$field . ' ' . $key] = $value;
+                    $valArr = explode('=', $v);
+                    if (count($valArr) == 2)
+                    {
+                        list($key, $value) = $valArr;
+                        $itemArr[$field . ' ' . $key] = $value;
+                    }
                 }
             }
             else
@@ -910,8 +986,12 @@ EOD;
             $itemArr = [];
             foreach (explode(',', $item) as $k => $v)
             {
-                list($key, $value) = explode('=', $v);
-                $itemArr[$key] = $field . ' ' . $key;
+                $valArr = explode('=', $v);
+                if (count($valArr) == 2)
+                {
+                    list($key, $value) = $valArr;
+                    $itemArr[$key] = $field . ' ' . $key;
+                }
             }
         }
         else
@@ -964,22 +1044,22 @@ EOD;
         }
         $fieldsName = $v['COLUMN_NAME'];
         // 指定后缀说明也是个时间字段
-        if (preg_match("/{$this->intDateSuffix}$/i", $fieldsName))
+        if ($this->isMatchSuffix($fieldsName, $this->intDateSuffix))
         {
             $inputType = 'datetime';
         }
         // 指定后缀结尾且类型为enum,说明是个单选框
-        if (preg_match("/{$this->enumRadioSuffix}$/i", $fieldsName) && $v['DATA_TYPE'] == 'enum')
+        if ($this->isMatchSuffix($fieldsName, $this->enumRadioSuffix) && $v['DATA_TYPE'] == 'enum')
         {
             $inputType = "radio";
         }
         // 指定后缀结尾且类型为set,说明是个复选框
-        if (preg_match("/{$this->setCheckboxSuffix}$/i", $fieldsName) && $v['DATA_TYPE'] == 'set')
+        if ($this->isMatchSuffix($fieldsName, $this->setCheckboxSuffix) && $v['DATA_TYPE'] == 'set')
         {
             $inputType = "checkbox";
         }
         // 指定后缀结尾且类型为char或tinyint且长度为1,说明是个Switch复选框
-        if (preg_match("/{$this->switchSuffix}$/i", $fieldsName) && ($v['COLUMN_TYPE'] == 'tinyint(1)' || $v['COLUMN_TYPE'] == 'char(1)') && $v['COLUMN_DEFAULT'] !== '' && $v['COLUMN_DEFAULT'] !== null)
+        if ($this->isMatchSuffix($fieldsName, $this->switchSuffix) && ($v['COLUMN_TYPE'] == 'tinyint(1)' || $v['COLUMN_TYPE'] == 'char(1)') && $v['COLUMN_DEFAULT'] !== '' && $v['COLUMN_DEFAULT'] !== null)
         {
             $inputType = "switch";
         }
@@ -987,6 +1067,25 @@ EOD;
     }
 
     /**
+     * 判断是否符合指定后缀
+     * @param string $field 字段名称
+     * @param mixed $suffixArr 后缀
+     * @return boolean
+     */
+    protected function isMatchSuffix($field, $suffixArr)
+    {
+        $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr);
+        foreach ($suffixArr as $k => $v)
+        {
+            if (preg_match("/{$v}$/i", $field))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * 获取表单分组数据
      * @param string $field
      * @param string $content
@@ -1014,13 +1113,9 @@ EOD;
     protected function getImageUpload($field, $content)
     {
         $filter = '';
-        foreach ($this->imageField as $k => $v)
+        if ($this->isMatchSuffix($field, $this->imageField))
         {
-            if (preg_match("/{$v}$/i", $field))
-            {
-                $filter = ' data-mimetype="image/*"';
-                break;
-            }
+            $filter = ' data-mimetype="image/*"';
         }
         $multiple = substr($field, -1) == 's' ? ' data-multiple="true"' : ' data-multiple="false"';
         $preview = $filter ? ' data-preview-id="p-' . $field . '"' : '';
@@ -1064,9 +1159,16 @@ EOD;
                 }
             }
         }
+        $formatter = $extend ? '' : $formatter;
         if ($extend)
+        {
             $html .= ", operate:false";
-        if ($formatter && !$extend)
+            if ($datatype == 'set')
+            {
+                $formatter = 'label';
+            }
+        }
+        if ($formatter)
             $html .= ", formatter: Table.api.formatter." . $formatter . "}";
         else
             $html .= "}";

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

@@ -4,7 +4,7 @@
     <div class="form-group layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -4,7 +4,7 @@
     <div class="form-group layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 2 - 2
application/admin/command/Crud/stubs/model.stub

@@ -6,8 +6,8 @@ use think\Model;
 
 class {%modelName%} extends Model
 {
-    // 表名,不含前缀
-    protected $name = '{%modelTableName%}';
+    // 表名
+    protected ${%modelTableType%} = '{%modelTableName%}';
     
     // 自动写入时间戳字段
     protected $autoWriteTimestamp = {%modelAutoWriteTimestamp%};

+ 2 - 2
application/admin/command/Crud/stubs/relationmodel.stub

@@ -6,7 +6,7 @@ use think\Model;
 
 class {%relationModelName%} extends Model
 {
-    // 表名,不含前缀
-    protected $name = '{%relationModelTableName%}';
+    // 表名
+    protected ${%relationModelTableType%} = '{%relationModelTableName%}';
     
 }

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

@@ -153,6 +153,7 @@ class Ajax extends Backend
     public function roletree()
     {
         $this->loadlang('auth/group');
+
         $model = model('AuthGroup');
         $id = $this->request->post("id");
         $pid = $this->request->post("pid");
@@ -165,22 +166,50 @@ class Ajax extends Backend
         if (($pid || $parentgroupmodel) && (!$id || $currentgroupmodel))
         {
             $id = $id ? $id : NULL;
+            $ruleList = collection(model('AuthRule')->order('weigh', 'desc')->select())->toArray();
             //读取父类角色所有节点列表
-            $parentrulelist = model('AuthRule')->all(in_array('*', explode(',', $parentgroupmodel->rules)) ? NULL : $parentgroupmodel->rules);
+            $parentRuleList = [];
+            if (in_array('*', explode(',', $parentgroupmodel->rules)))
+            {
+                $parentRuleList = $ruleList;
+            }
+            else
+            {
+                $parent_rule_ids = explode(',', $parentgroupmodel->rules);
+                foreach ($ruleList as $k => $v)
+                {
+                    if (in_array($v['id'], $parent_rule_ids))
+                    {
+                        $parentRuleList[] = $v;
+                    }
+                }
+            }
+
+            //当前所有正常规则列表
+            Tree::instance()->init($ruleList);
+
             //读取当前角色下规则ID集合
             $admin_rule_ids = $this->auth->getRuleIds();
+            //是否是超级管理员
             $superadmin = $this->auth->isSuperAdmin();
+            //当前拥有的规则ID集合
             $current_rule_ids = $id ? explode(',', $currentgroupmodel->rules) : [];
 
-            if (!$id || !in_array($pid, Tree::instance()->init($model->all(['status' => 'normal']))->getChildrenIds($id, TRUE)))
+            if (!$id || !in_array($pid, Tree::instance()->getChildrenIds($id, TRUE)))
             {
-                //构造jstree所需的数据
+                $ruleList = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0), 'name');
+                $hasChildrens = [];
+                foreach ($ruleList as $k => $v)
+                {
+                    if ($v['haschild'])
+                        $hasChildrens[] = $v['id'];
+                }
                 $nodelist = [];
-                foreach ($parentrulelist as $k => $v)
+                foreach ($parentRuleList as $k => $v)
                 {
                     if (!$superadmin && !in_array($v['id'], $admin_rule_ids))
                         continue;
-                    $state = array('selected' => !$v['ismenu'] && in_array($v['id'], $current_rule_ids));
+                    $state = array('selected' => in_array($v['id'], $current_rule_ids) && !in_array($v['id'], $hasChildrens));
                     $nodelist[] = array('id' => $v['id'], 'parent' => $v['pid'] ? $v['pid'] : '#', 'text' => $v['title'], 'type' => 'menu', 'state' => $state);
                 }
                 $this->code = 1;
@@ -206,7 +235,7 @@ class Ajax extends Backend
     {
         $this->code = -1;
         $file = $this->request->file('file');
-        if (!$file)
+        if (empty($file))
         {
             $this->msg = "未上传文件或超出服务器上传限制";
             return;

+ 20 - 3
application/admin/controller/auth/Admin.php

@@ -52,10 +52,21 @@ class Admin extends Backend
     {
         if ($this->request->isAjax())
         {
-            $childrenAdminIds = model('AuthGroupAccess')
-                    ->field('uid')
+            $groupData = model('AuthGroup')->where('status', 'normal')->column('id,name');
+
+            $childrenAdminIds = [];
+            $authGroupList = model('AuthGroupAccess')
+                    ->field('uid,group_id')
                     ->where('group_id', 'in', $this->childrenIds)
-                    ->column('uid');
+                    ->select();
+            
+            $adminGroupName = [];
+            foreach ($authGroupList as $k => $v)
+            {
+                $childrenAdminIds[] = $v['uid'];
+                if (isset($groupData[$v['group_id']]))
+                    $adminGroupName[$v['uid']][$v['group_id']] = $groupData[$v['group_id']];
+            }
             list($where, $sort, $order, $offset, $limit) = $this->buildparams();
             $total = $this->model
                     ->where($where)
@@ -70,6 +81,12 @@ class Admin extends Backend
                     ->order($sort, $order)
                     ->limit($offset, $limit)
                     ->select();
+            foreach ($list as $k => &$v)
+            {
+                $groups = isset($adminGroupName[$v['id']]) ? $adminGroupName[$v['id']] : [];
+                $v['groups'] = implode(',', array_keys($groups));
+                $v['groups_text'] = implode(',', array_values($groups));
+            }
             $result = array("total" => $total, "rows" => $list);
 
             return json($result);

+ 1 - 10
application/admin/controller/auth/Rule.php

@@ -17,6 +17,7 @@ class Rule extends Backend
 
     protected $model = null;
     protected $rulelist = [];
+    protected $multiFields = 'ismenu,status';
 
     public function _initialize()
     {
@@ -133,14 +134,4 @@ class Rule extends Backend
         return;
     }
 
-    /**
-     * 批量更新
-     * @internal
-     */
-    public function multi($ids = "")
-    {
-        // 节点禁止批量操作
-        $this->code = -1;
-    }
-
 }

+ 11 - 1
application/admin/controller/example/Bootstraptable.php

@@ -14,6 +14,7 @@ class Bootstraptable extends Backend
 {
 
     protected $model = null;
+    protected $noNeedRight = ['change', 'detail'];
 
     public function _initialize()
     {
@@ -44,7 +45,7 @@ class Bootstraptable extends Backend
         }
         return $this->view->fetch();
     }
-    
+
     /**
      * 详情
      */
@@ -57,4 +58,13 @@ class Bootstraptable extends Backend
         return $this->view->fetch();
     }
 
+    /**
+     * 变更
+     * @internal
+     */
+    public function change()
+    {
+        $this->code = 1;
+    }
+
 }

+ 21 - 14
application/admin/controller/general/Config.php

@@ -1,8 +1,7 @@
 <?php
 
 namespace app\admin\controller\general;
- 
-use think\Config as tpConfig;
+
 use app\common\library\Email;
 use app\common\controller\Backend;
 
@@ -235,20 +234,28 @@ class Config extends Backend
         }
     }
 
-
+    /**
+     * 发送测试邮件
+     * @internal
+     */
     public function emailtest()
     {
-        $content = '<table style="width: 99.8%; "><tbody><tr><td id="QQMAILSTATIONERY" style="background:url(https://rescdn.qqmail.com/zh_CN/htmledition/images/xinzhi/bg/a_07.jpg) repeat-x #e4ebf5; min-height:550px; padding: 100px 55px 200px;">这是一封测试邮件,用于测试邮件配置是否正常!</td></tr></tbody></table>';
-
-        $site = tpConfig::get("site");
+        $receiver = $this->request->request("receiver");
         $email = new Email;
-        $mailArr = Array();
-        $mailArr['mTo'] = $site['mail_from'];       //收件人
-        $mailArr['subject'] = '这是一封测试邮件';    //邮件主题
-        $mailArr['content'] = $content;             //邮件内容(html)
-        $mailArr['fromNic'] = 'Fastadmin系统邮件';   //发件人昵称[可省略]
-        $mailArr['toNic'] = '亲爱的用户';            //收件人昵称[可省略]貌似无效
-        $data = $email->sendMail($mailArr['mTo'],$mailArr['subject'],$mailArr['content'],$mailArr['fromNic'],$mailArr['toNic']);
-        return json(['data'=>$data,'code'=>200,'message'=>'操作完成']);
+        $result = $email
+                ->to($receiver)
+                ->subject(__("This is a test mail"))
+                ->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . __('This is a test mail content') . '</div>')
+                ->send();
+        if ($result)
+        {
+            $this->code = 1;
+        }
+        else
+        {
+            $this->code = -1;
+            $this->msg = $email->getError();
+        }
     }
+
 }

+ 1 - 0
application/admin/lang/zh-cn.php

@@ -14,6 +14,7 @@ return [
     'Add'                                                   => '添加',
     'Edit'                                                  => '编辑',
     'Delete'                                                => '删除',
+    'Detail'                                                => '详情',
     'Move'                                                  => '移动',
     'Name'                                                  => '名称',
     'Status'                                                => '状态',

+ 13 - 12
application/admin/lang/zh-cn/general/attachment.php

@@ -1,16 +1,17 @@
 <?php
 
 return [
-    'Url'         => '物理路径',
-    'Imagewidth'  => '宽度',
-    'Imageheight' => '宽度',
-    'Imagetype'   => '图片类型',
-    'Imageframes' => '图片帧数',
-    'Preview'     => '预览',
-    'Filesize'    => '文件大小',
-    'Mimetype'    => 'Mime类型',
-    'Extparam'    => '透传数据',
-    'Createtime'  => '创建日期',
-    'Uploadtime'  => '上传时间',
-    'Storage'     => '存储引擎'
+    'Url'                  => '物理路径',
+    'Imagewidth'           => '宽度',
+    'Imageheight'          => '宽度',
+    'Imagetype'            => '图片类型',
+    'Imageframes'          => '图片帧数',
+    'Preview'              => '预览',
+    'Filesize'             => '文件大小',
+    'Mimetype'             => 'Mime类型',
+    'Extparam'             => '透传数据',
+    'Createtime'           => '创建日期',
+    'Uploadtime'           => '上传时间',
+    'Storage'              => '存储引擎',
+    'Upload by summernote' => '从编辑器上传'
 ];

+ 35 - 32
application/admin/lang/zh-cn/general/config.php

@@ -1,36 +1,39 @@
 <?php
 
 return [
-    'Name'               => '变量名',
-    'Tip'                => '提示信息',
-    'Group'              => '分组',
-    'Type'               => '类型',
-    'Title'              => '变量标题',
-    'Value'              => '变量值',
-    'Basic'              => '基础配置',
-    'Email'              => '邮件配置',
-    'Attachment'         => '附件配置',
-    'User'               => '会员配置',
-    'Example'            => '示例分组',
-    'Extend'             => '扩展属性',
-    'String'             => '字符',
-    'Text'               => '文本',
-    'Number'             => '数字',
-    'Date'               => '日期',
-    'Time'               => '时间',
-    'Datetime'           => '日期时间',
-    'Image'              => '图片',
-    'Images'             => '图片(多)',
-    'File'               => '文件',
-    'Files'              => '文件(多)',
-    'Select'             => '列表',
-    'Selects'            => '列表(多选)',
-    'Checkbox'           => '复选',
-    'Radio'              => '单选',
-    'Array'              => '数组',
-    'Array key'          => '键名',
-    'Array value'        => '键值',
-    'Content'            => '数据列表',
-    'Rule'               => '校验规则',
-    'Name already exist' => '变量名称已经存在',
+    'Name'                        => '变量名',
+    'Tip'                         => '提示信息',
+    'Group'                       => '分组',
+    'Type'                        => '类型',
+    'Title'                       => '变量标题',
+    'Value'                       => '变量值',
+    'Basic'                       => '基础配置',
+    'Email'                       => '邮件配置',
+    'Attachment'                  => '附件配置',
+    'User'                        => '会员配置',
+    'Example'                     => '示例分组',
+    'Extend'                      => '扩展属性',
+    'String'                      => '字符',
+    'Text'                        => '文本',
+    'Number'                      => '数字',
+    'Date'                        => '日期',
+    'Time'                        => '时间',
+    'Datetime'                    => '日期时间',
+    'Image'                       => '图片',
+    'Images'                      => '图片(多)',
+    'File'                        => '文件',
+    'Files'                       => '文件(多)',
+    'Select'                      => '列表',
+    'Selects'                     => '列表(多选)',
+    'Checkbox'                    => '复选',
+    'Radio'                       => '单选',
+    'Array'                       => '数组',
+    'Array key'                   => '键名',
+    'Array value'                 => '键值',
+    'Content'                     => '数据列表',
+    'Rule'                        => '校验规则',
+    'Name already exist'          => '变量名称已经存在',
+    'Send a test message'         => '发送测试邮件',
+    'This is a test mail content' => '这是一封测试邮件,用于测试邮件配置是否正常!',
+    'This is a test mail'         => '这是一封测试邮件',
 ];

+ 5 - 1
application/admin/library/traits/Backend.php

@@ -163,7 +163,7 @@ trait Backend
             if ($this->request->has('params'))
             {
                 parse_str($this->request->post("params"), $values);
-                $values = array_intersect_key($values, array_flip(array('status')));
+                $values = array_intersect_key($values, array_flip(is_array($this->multiFields) ? $this->multiFields : explode(',', $this->multiFields)));
                 if ($values)
                 {
                     $count = $this->model->where($this->model->getPk(), 'in', $ids)->update($values);
@@ -172,6 +172,10 @@ trait Backend
                         $this->code = 1;
                     }
                 }
+                else
+                {
+                    $this->msg = __('You have no permission');
+                }
             }
             else
             {

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

@@ -38,7 +38,7 @@
     <div class="form-group hidden layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -38,7 +38,7 @@
     <div class="form-group hidden layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -30,7 +30,7 @@
     <div class="form-group hidden layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -30,7 +30,7 @@
     <div class="form-group hidden layer-footer">
         <label class="control-label col-xs-12 col-sm-2 col-xs-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -63,7 +63,7 @@
     <div class="form-group hidden layer-footer">
         <div class="col-xs-2"></div>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -59,7 +59,7 @@
     <div class="form-group hidden layer-footer">
         <div class="col-xs-2"></div>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/category/add.html

@@ -51,7 +51,7 @@
     <div class="form-group hidden layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -51,7 +51,7 @@
     <div class="form-group hidden layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -16,6 +16,8 @@
                                 <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
                             </ul>
                         </div>
+                        <a class="btn btn-success btn-change btn-start" data-params="action=start" data-url="example/bootstraptable/change" href="javascript:;"><i class="fa fa-play"></i> 启动</a>
+                        <a class="btn btn-danger btn-change btn-pause" data-params="action=pause" data-url="example/bootstraptable/change" href="javascript:;"><i class="fa fa-pause"></i> 暂停</a>
                     </div>
                     <table id="table" class="table table-striped table-bordered table-hover" width="100%">
 

+ 1 - 1
application/admin/view/general/attachment/edit.html

@@ -63,7 +63,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 2 - 2
application/admin/view/general/config/index.html

@@ -126,7 +126,7 @@
                                 <tr>
                                     <td></td>
                                     <td>
-                                        <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+                                        <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
                                         <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
                                     </td>
                                     <td></td>
@@ -205,7 +205,7 @@ key2|value2</textarea>
                     <div class="form-group">
                         <label class="control-label col-xs-12 col-sm-2"></label>
                         <div class="col-xs-12 col-sm-4">
-                            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+                            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
                             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
                         </div>
                     </div>

+ 1 - 1
application/admin/view/general/configvalue/add.html

@@ -44,7 +44,7 @@
     <div class="form-group hidden layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/general/configvalue/edit.html

@@ -46,7 +46,7 @@
     <div class="form-group hidden layer-footer">
         <div class="col-xs-2"></div>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/general/crontab/add.html

@@ -73,7 +73,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/general/crontab/edit.html

@@ -73,7 +73,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -73,7 +73,7 @@
     <div class="form-group layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -74,7 +74,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/user/third/add.phtml

@@ -56,7 +56,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/user/third/edit.phtml

@@ -57,7 +57,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/user/user/add.phtml

@@ -185,7 +185,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/user/user/edit.phtml

@@ -180,7 +180,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/version/add.html

@@ -57,7 +57,7 @@
     <div class="form-group layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

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

@@ -51,7 +51,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/wechat/autoreply/add.html

@@ -43,7 +43,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/wechat/autoreply/edit.html

@@ -44,7 +44,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/wechat/config/add.html

@@ -38,7 +38,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/wechat/config/edit.html

@@ -40,7 +40,7 @@
     <div class="form-group hide layer-footer">
         <label class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/wechat/response/add.html

@@ -32,7 +32,7 @@
     <div class="form-group {:input('get.callback')?'':'hidden layer-footer'}">
         <div class="col-xs-2"></div>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 1 - 1
application/admin/view/wechat/response/edit.html

@@ -36,7 +36,7 @@
     <div class="form-group hidden layer-footer">
         <div class="col-xs-2"></div>
         <div class="col-xs-12 col-sm-8">
-            <button type="submit" class="btn btn-success btn-embossed">{:__('OK')}</button>
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>

+ 5 - 0
application/common/controller/Backend.php

@@ -78,6 +78,11 @@ class Backend extends Controller
      * 是否开启模型场景验证
      */
     protected $modelSceneValidate = false;
+    
+    /**
+     * Multi方法可批量修改的字段
+     */
+    protected $multiFields = 'status';
 
     /**
      * 引入后台控制器的traits

+ 174 - 69
application/common/library/Email.php

@@ -6,81 +6,186 @@ use think\Config;
 
 class Email
 {
+
     /**
-     * 发送邮件
-     * @param string $mTo 收件人
-     * @param string $subject 邮件主题
-     * @param string $content 邮件内容(html)
-     * @param string $fromNic 发件人昵称
-     * @param string $toNic 收件人昵称
+     * 单例对象
+     */
+    protected static $instance;
+
+    /**
+     * phpmailer对象
+     */
+    protected $mail = [];
+
+    /**
+     * 错误内容
+     */
+    protected $_error = '';
+
+    /**
+     * 默认配置
+     */
+    public $options = [
+        'charset' => 'utf-8', //编码格式
+        'debug'   => 0, //调式模式
+    ];
+
+    /**
+     * 初始化
+     * @access public
+     * @param array $options 参数
+     * @return Email
+     */
+    public static function instance($options = [])
+    {
+        if (is_null(self::$instance))
+        {
+            self::$instance = new static($options);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 构造函数
+     * @param array $options
      */
-    public function sendMail($mTo='',$subject='',$content='',$fromNic='',$toNic='')
+    public function __construct($options = [])
     {
-        $site = Config::get("site");
-        $re = Vendor('phpmailer.phpmailer.PHPMailerAutoload');
-
-        $mail = new \PHPMailer ();
-
-        //$mail->SMTPDebug = 3;                               // Enable verbose debug output
-        $mail->isSMTP();                                      //smtp需要鉴权 这个必须是true
-        $mail->Host = $site['mail_smtp_host'];                //SMTP服务器地址
-        $mail->SMTPAuth = true;                               // Enable SMTP authentication
-        $mail->Username = $site['mail_smtp_user'];            // SMTP 用戶名
-        $mail->Password = $site['mail_smtp_pass'];            // SMTP 密碼
-        switch ($site['mail_verify_type'])                    // Enable TLS encryption, `ssl` also accepted
+        if ($config = Config::get('site'))
         {
-        case 1:
-          $mail->SMTPSecure = 'tls';
-          break;
-        case 2:
-          $mail->SMTPSecure = 'ssl';
-          break;
-        default:
-          $mail->SMTPSecure = '';
+            $this->options = array_merge($this->options, $config);
         }
-        $mail->Port = $site['mail_smtp_port'];                                      // 设置ssl连接smtp服务器的远程服务器端口号
-        $mail->setFrom($site['mail_from'], $fromNic);                               // [发件人],[昵称(可选)]
-        $mail->addAddress($mTo, $toNic);                                            // [收件人],[昵称(可选)]
-        //$mail->addReplyTo('xxxxxx@qq.com', 'Information');                        // 回复地址(可选)
-        //$mail->addCC('xxxxxx@qq.com');                                            //好像是密送
-        //$mail->addBCC('xxxxxx@qq.com');                                           //好像是密送B
-        // $mail->addAttachment('/var/tmp/file.tar.gz');                            // 添加附件
-        // $mail->addAttachment('/tmp/image.jpg', 'new.jpg');                       // 附件名选项
-        $mail->isHTML(true);                                                        //邮件正文是否为html编码
-        $mail->Subject = $subject;                                                  //添加邮件主题
-        $mail->Body    = $content;                                                  //邮件正文
-        //$mail->AltBody = 'This is the body in plain text for non-HTML mail clients';//附加信息,可以省略
-
-        switch ($site['mail_type'])
+        $this->options = array_merge($this->options, $options);
+        vendor('phpmailer.phpmailer.PHPMailerAutoload');
+        $securArr = [1 => 'tls', 2 => 'ssl'];
+
+        $this->mail = new \PHPMailer(true);
+        $this->mail->CharSet = $this->options['charset'];
+        $this->mail->SMTPDebug = $this->options['debug'];
+        $this->mail->isSMTP();
+        $this->mail->SMTPAuth = true;
+        $this->mail->Host = $this->options['mail_smtp_host'];
+        $this->mail->Username = $this->options['mail_smtp_user'];
+        $this->mail->Password = $this->options['mail_smtp_pass'];
+        $this->mail->SMTPSecure = isset($securArr[$this->options['mail_verify_type']]) ? $securArr[$this->options['mail_verify_type']] : '';
+        $this->mail->Port = $this->options['mail_smtp_port'];
+
+        //设置发件人
+        $this->from($this->options['mail_from']);
+    }
+
+    /**
+     * 设置邮件主题
+     * @param string $subject
+     * @return $this
+     */
+    public function subject($subject)
+    {
+        $this->options['subject'] = $subject;
+        return $this;
+    }
+
+    /**
+     * 设置发件人
+     * @param string $email
+     * @param string $name
+     * @return $this
+     */
+    public function from($email, $name = '')
+    {
+        $this->options['from'] = $email;
+        $this->options['from_name'] = $name;
+        return $this;
+    }
+
+    /**
+     * 设置收件人
+     * @param string $email
+     * @param string $name
+     * @return $this
+     */
+    public function to($email, $name = '')
+    {
+        $this->options['to'] = $email;
+        $this->options['to_name'] = $name;
+        return $this;
+    }
+
+    /**
+     * 设置邮件正文
+     * @param string $body
+     * @param boolean $ishtml
+     * @return $this
+     */
+    public function message($body, $ishtml = true)
+    {
+        $this->options['body'] = $body;
+        $this->options['ishtml'] = $ishtml;
+        return $this;
+    }
+
+    /**
+     * 获取最后产生的错误
+     */
+    public function getError()
+    {
+        return $this->_error;
+    }
+
+    protected function setError($error)
+    {
+        $this->_error = $error;
+    }
+
+    /**
+     * 发送邮件
+     * @return boolean
+     */
+    public function send()
+    {
+        $result = false;
+        switch ($this->options['mail_type'])
         {
-        case 1:
-            if(!$mail->send()) {//这里如果提交错误的smpt配置PHPmailer会卡住暂时不清楚为什么
-                $sendResult['text'] = $mail->ErrorInfo;
-                $sendResult['data'] = false;
-                return $sendResult;
-            } else {
-                $sendResult['text'] ='smtp发送成功';
-                $sendResult['data'] = true;
-                return $sendResult;
-            }
-            break;
-        case 2://使用mail方法发送邮件
-            $headers  = 'MIME-Version: 1.0' . "\r\n";
-            $headers .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
-            $headers .= 'To: '.$toNic.' <'.$mTo.'>' . "\r\n";//收件人
-            $headers .= 'From: '.$fromNic.' <'.$site['mail_from'].'>' . "\r\n";//发件人
-            $sendResult['data'] = mail($mTo, $subject, $content, $headers);
-            if ($sendResult['data']) {
-                $sendResult['text'] ='mail函数发送成功';
-            }else{
-                $sendResult['text'] ='mail函数发送失败';
-            }
-            return $sendResult;
-            break;
-        default:
-            $sendResult['data'] = false;
-            $sendResult['text'] ='已关闭邮件发送';
-            return $sendResult;
+            case 1:
+                //使用phpmailer发送
+                $this->mail->setFrom($this->options['from'], $this->options['from_name']);
+                $this->mail->addAddress($this->options['to'], $this->options['to_name']);
+                $this->mail->Subject = $this->options['subject'];
+                if ($this->options['ishtml'])
+                {
+                    $this->mail->msgHTML($this->options['body']);
+                }
+                else
+                {
+                    $this->mail->Body = $this->options['body'];
+                }
+                try
+                {
+                    $result = $this->mail->send();
+                }
+                catch (\phpmailerException $e)
+                {
+                    $this->setError($e->getMessage());
+                }
+
+                $this->setError($result ? '' : $this->mail->ErrorInfo);
+                break;
+            case 2:
+                //使用mail方法发送邮件
+                $headers = 'MIME-Version: 1.0' . "\r\n";
+                $headers .= "Content-type: text/html; charset=" . $this->options['charset'] . "\r\n";
+                $headers .= "To: {$this->options['to_name']} <{$this->options['to']}>\r\n"; //收件人
+                $headers .= "From: {$this->options['from_name']} <{$this->options['from']}>\r\n"; //发件人
+                $result = mail($this->options['mail_to'], $this->options['subject'], $this->options['body'], $headers);
+                $this->setError($result ? '' : error_get_last()['message']);
+                break;
+            default:
+                //邮件功能已关闭
+                $this->setError(__('Mail already closed'));
+                break;
         }
+        return $result;
     }
+
 }

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

@@ -119,15 +119,10 @@ define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang', 'moment'], function ($
                 });
             },
             open: function (url, title, options) {
-                title = title == undefined ? "" : title;
+                title = title ? title : "";
                 url = Backend.api.fixurl(url);
                 url = url + (url.indexOf("?") > -1 ? "&" : "?") + "dialog=1";
-                var area;
-                if ($(window).width() < 800) {
-                    area = ["95%", "95%"];
-                } else {
-                    area = ['800px', '600px'];
-                }
+                var area = [$(window).width() > 800 ? '800px' : '95%', $(window).height() > 600 ? '600px' : '95%'];
                 Backend.api.layer.open($.extend({
                     type: 2,
                     title: title,
@@ -188,7 +183,7 @@ define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang', 'moment'], function ($
                 var btnHeight = layero.find('.layui-layer-btn').outerHeight() || 0;
 
                 var oldheg = heg + titHeight + btnHeight;
-                var maxheg = 600;
+                var maxheg = $(window).height() < 600 ? $(window).height() : 600;
                 if (frame.outerWidth() < 768 || that.area[0].indexOf("%") > -1) {
                     maxheg = $(window).height();
                 }

+ 1 - 0
public/assets/js/backend/auth/admin.js

@@ -24,6 +24,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         {field: 'id', title: 'ID'},
                         {field: 'username', title: __('Username')},
                         {field: 'nickname', title: __('Nickname')},
+                        {field: 'groups_text', title: __('Group'), operate:false, formatter: Table.api.formatter.label},
                         {field: 'email', title: __('Email')},
                         {field: 'status', title: __("Status"), formatter: Table.api.formatter.status},
                         {field: 'logintime', title: __('Login time'), formatter: Table.api.formatter.datetime},

+ 33 - 17
public/assets/js/backend/auth/rule.js

@@ -24,13 +24,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     [
                         {field: 'state', checkbox: true, },
                         {field: 'id', title: 'ID'},
-                        {field: 'title', title: __('Title'), align: 'left'},
+                        {field: 'title', title: __('Title'), align: 'left', align: 'left', formatter: Controller.api.formatter.title},
                         {field: 'icon', title: __('Icon'), formatter: Controller.api.formatter.icon},
-                        {field: 'name', title: __('Name'), align: 'left'},
-                        {field: 'ismenu', title: __('Ismenu'), align: 'left'},
+                        {field: 'name', title: __('Name'), align: 'left', formatter: Controller.api.formatter.name},
                         {field: 'weigh', title: __('Weigh')},
                         {field: 'status', title: __('Status'), formatter: Table.api.formatter.status},
-                        {field: 'id', title: '<a href="javascript:;" class="btn btn-primary btn-xs btn-toggle"><i class="fa fa-chevron-down"></i></a>', operate: false, formatter: Controller.api.formatter.subnode},
+                        {field: 'ismenu', title: __('Ismenu'), align: 'center', formatter: Controller.api.formatter.menu},
+                        {field: 'id', title: '<a href="javascript:;" class="btn btn-success btn-xs btn-toggle"><i class="fa fa-chevron-up"></i></a>', operate: false, formatter: Controller.api.formatter.subnode},
                         {field: 'operate', title: __('Operate'), events: Table.api.events.operate, formatter: Table.api.formatter.operate}
                     ]
                 ],
@@ -44,19 +44,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
 
             //默认隐藏所有子节点
             table.on('post-body.bs.table', function (e, settings, json, xhr) {
-                $("a.btn[data-id][data-pid][data-pid!=0]").closest("tr").hide();
-            });
-            //显示隐藏子节点
-            $(document.body).on("click", ".btn-node-sub", function (e) {
-                var status = typeof status !== 'undefined' ? status : $(this).closest("tr").hasClass("selected");
-                $("a.btn[data-pid='" + $(this).data("id") + "']").each(function () {
-                    $(this).closest("tr").toggle(status).toggleClass("selected", status);
-                    $(this).closest("tr").find("input[type=checkbox]").prop("checked", status);
-                    // 展示全部子节点
-                    // $(this).trigger("click", status);
+                //$("a.btn[data-id][data-pid][data-pid!=0]").closest("tr").hide();
+                $(".btn-node-sub.disabled").closest("tr").hide();
+
+                //显示隐藏子节点
+                $(".btn-node-sub").off("click").on("click", function (e) {
+                    var status = $(this).data("shown") ? true : false;
+                    $("a.btn[data-pid='" + $(this).data("id") + "']").each(function () {
+                        $(this).closest("tr").toggle(!status);
+                    });
+                    $(this).data("shown", !status);
+                    return false;
                 });
-                return false;
+
             });
+            //展开隐藏一级
             $(document.body).on("click", ".btn-toggle", function (e) {
                 $("a.btn[data-id][data-pid][data-pid!=0].disabled").closest("tr").hide();
                 var that = this;
@@ -64,13 +66,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 $("i", that).toggleClass("fa-chevron-down", !show);
                 $("i", that).toggleClass("fa-chevron-up", show);
                 $("a.btn[data-id][data-pid][data-pid!=0]").not('.disabled').closest("tr").toggle(show);
+                $(".btn-node-sub[data-pid=0]").data("shown", show);
             });
+            //展开隐藏全部
             $(document.body).on("click", ".btn-toggle-all", function (e) {
                 var that = this;
                 var show = $("i", that).hasClass("fa-plus");
                 $("i", that).toggleClass("fa-plus", !show);
                 $("i", that).toggleClass("fa-minus", show);
-                $("a.btn[data-id][data-pid][data-pid!=0]").closest("tr").toggle(show);
+                $(".btn-node-sub.disabled").closest("tr").toggle(show);
+                $(".btn-node-sub").data("shown", show);
             });
         },
         add: function () {
@@ -81,11 +86,22 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
         },
         api: {
             formatter: {
+                title: function (value, row, index) {
+                    return !row.ismenu ? "<span class='text-muted'>" + value + "</span>" : value;
+                },
+                name: function (value, row, index) {
+                    return !row.ismenu ? "<span class='text-muted'>" + value + "</span>" : value;
+                },
+                menu: function (value, row, index) {
+                    return "<a href='javascript:;' class='btn btn-" + (value ? "info" : "default") + " btn-xs btn-change' data-id='"
+                            + row.id + "' data-params='ismenu=" + (value ? 0 : 1) + "'>" + (value ? __('Yes') : __('No')) + "</a>";
+                },
                 icon: function (value, row, index) {
                     return '<i class="' + value + '"></i>';
                 },
                 subnode: function (value, row, index) {
-                    return '<a href="javascript:;" data-id="' + row['id'] + '" data-pid="' + row['pid'] + '" class="btn btn-primary btn-xs ' + (row['haschild'] == 1 ? '' : 'disabled') + ' btn-node-sub"><i class="fa fa-sitemap"></i></a>';
+                    return '<a href="javascript:;" data-id="' + row['id'] + '" data-pid="' + row['pid'] + '" class="btn btn-xs '
+                            + (row['haschild'] == 1 ? 'btn-success' : 'btn-default disabled') + ' btn-node-sub"><i class="fa fa-sitemap"></i></a>';
                 }
             },
             bindevent: function () {

+ 23 - 1
public/assets/js/backend/example/bootstraptable.js

@@ -30,9 +30,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         //模糊搜索
                         {field: 'title', title: __('Title'), operate: 'LIKE %...%', placeholder: '模糊搜索,*表示任意字符', style: 'width:200px'},
                         //通过Ajax渲染searchList,也可以使用JSON数据
-                        {field: 'url', title: __('Url'), align: 'left', defaultValue:3, searchList: $.getJSON('ajax/typeahead?search=a&field=row[user_id]'), formatter: Controller.api.formatter.url},
+                        {field: 'url', title: __('Url'), align: 'left', defaultValue: 3, 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},
+                        //自定义栏位
+                        {field: 'custom', title: __('Custom'), operate: false, formatter: Controller.api.formatter.custom},
                         //browser是一个不存在的字段
                         //通过formatter来渲染数据,同时为它添加上事件
                         {field: 'browser', title: __('Browser'), operate: false, events: Controller.api.events.browser, formatter: Controller.api.formatter.browser},
@@ -49,6 +51,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 //可以控制是否默认显示搜索单表,false则隐藏,默认为false
                 searchFormVisible: true
             });
+            
+            //在表格内容渲染完成后回调的事件
+            table.on('post-body.bs.table', function (e, settings, json, xhr) {
+                
+            });
 
             // 为表格绑定事件
             Table.api.bindevent(table);
@@ -62,6 +69,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             $(document).on("click", ".btn-selected", function () {
                 Layer.alert(JSON.stringify(table.bootstrapTable('getSelections')));
             });
+            
+            //启动和暂停按钮
+            $(document).on("click", ".btn-start,.btn-pause", function () {
+                //在table外不可以使用添加.btn-change的方法
+                //只能自己调用Table.api.multi实现
+                Table.api.multi("changestatus", 0, table, this);
+            });
+
         },
         add: function () {
             Controller.api.bindevent();
@@ -84,6 +99,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     //这里我们直接使用row的数据
                     return '<a class="btn btn-xs btn-browser">' + row.useragent.split(" ")[0] + '</a>';
                 },
+                custom: function (value, row, index) {
+                    //添加上btn-change可以自定义请求的URL进行数据处理
+                    return '<a class="btn btn-xs btn-danger btn-change" data-url="example/bootstraptable/change" data-id="' + row.id + '">' + __('Locked') + '</a>';
+                },
                 operate: function (value, row, index) {
                     //返回字符串加上Table.api.formatter.operate的结果
                     //默认需要按需显示排序/编辑/删除按钮,则需要在Table.api.formatter.operate将table传入
@@ -96,6 +115,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 ip: {
                     //格式为:方法名+空格+DOM元素
                     'click .btn-ip': function (e, value, row, index) {
+                        e.stopPropagation();
                         var options = $("#table").bootstrapTable('getOptions');
                         //这里我们手动将数据填充到表单然后提交
                         $("#commonSearchContent_" + options.idTable + " form [name='ip']").val(value);
@@ -105,11 +125,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 },
                 browser: {
                     'click .btn-browser': function (e, value, row, index) {
+                        e.stopPropagation();
                         Layer.alert("该行数据为: <code>" + JSON.stringify(row) + "</code>");
                     }
                 },
                 operate: $.extend({
                     'click .btn-detail': function (e, value, row, index) {
+                        e.stopPropagation();
                         Backend.api.open('example/bootstraptable/detail/ids/' + row['id'], __('Detail'));
                     }
                 }, Table.api.events.operate)

+ 4 - 11
public/assets/js/backend/general/config.js

@@ -78,17 +78,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
 
             //添加向发件人发送测试邮件按钮和方法
-            testMail = '<a class="btn btn-info testmail">向发件人发送测试邮件</a>'
-            $('input[name="row[mail_from]"]').parent().next().append(testMail);
-            $(document).on("click", ".testmail",function(){
-                $.get("/admin/general.config/emailtest", function(result){
-                    if (result.data.data) {
-                        Toastr.success(result.data.text)
-                    }else{
-                        Toastr.warning(result.data.text)
-                    }
-                });
-            })
+            $('input[name="row[mail_from]"]').parent().next().append('<a class="btn btn-info testmail">' + __('Send a test message') + '</a>');
+            $(document).on("click", ".testmail", function () {
+                Backend.api.ajax({url: "general/config/emailtest", data: {receiver: $('input[name="row[mail_from]"]').val()}});
+            });
         },
         add: function () {
             Controller.api.bindevent();

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

@@ -162,7 +162,7 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
             if ($("ul.sidebar-menu li.active a").size() > 0) {
                 $("ul.sidebar-menu li.active a").trigger("click");
             } else {
-                $("ul.sidebar-menu li a[url!='javascript:;']").trigger("click");
+                $("ul.sidebar-menu li a[url!='javascript:;']:first").trigger("click");
             }
             if (Config.referer) {
                 //刷新页面后跳到到刷新前的页面

+ 0 - 1
public/assets/js/bootstrap-table-commonsearch.js

@@ -97,7 +97,6 @@
                                     var resultlist = ret;
                                     isArray = ret.constructor === Array ? true : isArray;
                                 }
-                                console.log(resultlist);
                                 var optionList = [];
                                 $.each(resultlist, function (key, value) {
                                     var isSelect = (isArray ? value : key) == vObjCol.defaultValue ? 'selected' : '';

+ 3 - 2
public/assets/js/require-form.js

@@ -67,8 +67,6 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'], func
                 return false;
             },
             bindevent: function (form, onBeforeSubmit, onAfterSubmit) {
-                //移除提交按钮的disabled类
-                $(".layer-footer .btn.disabled", form).removeClass("disabled");
                 //绑定表单事件
                 form.validator($.extend({
                     validClass: 'has-success',
@@ -105,6 +103,9 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'upload', 'validator'], func
                         return false;
                     }
                 }, form.data("validator-options") || {}));
+                
+                //移除提交按钮的disabled类
+                $(".layer-footer .btn.disabled", form).removeClass("disabled");
 
                 //绑定select元素事件
                 if ($(".selectpicker", form).size() > 0) {

+ 22 - 3
public/assets/js/require-table.js

@@ -219,6 +219,10 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'moment', 'bootstrap-table',
                 $(table).on("click", "input[data-id][name='checkbox']", function (e) {
                     table.trigger('fa.event.check');
                 });
+                $(table).on("click", "[data-id].btn-change", function (e) {
+                    e.preventDefault();
+                    Table.api.multi($(this).data("action") ? $(this).data("action") : '', [$(this).data("id")], table, this);
+                });
                 $(table).on("click", "[data-id].btn-edit", function (e) {
                     e.preventDefault();
                     Backend.api.open(options.extend.edit_url + "/ids/" + $(this).data("id"), __('Edit'));
@@ -245,9 +249,11 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'moment', 'bootstrap-table',
             // 批量操作请求
             multi: function (action, ids, table, element) {
                 var options = table.bootstrapTable('getOptions');
-                var url = action == "del" ? options.extend.del_url : options.extend.multi_url;
+                var data = element ? $(element).data() : {};
+                var url = typeof data.url !== "undefined" ? data.url : (action == "del" ? options.extend.del_url : options.extend.multi_url);
                 url = url + "/ids/" + ($.isArray(ids) ? ids.join(",") : ids);
-                var options = {url: url, data: {action: action, ids: ids, params: element ? $(element).data("params") : ''}};
+                var params = typeof data.params !== "undefined" ? (typeof data.params == 'object' ? $.param(data.params) : data.params) : '';
+                var options = {url: url, data: {action: action, ids: ids, params: params}};
                 Backend.api.ajax(options, function (data) {
                     Toastr.success(__('Operation completed'));
                     table.bootstrapTable('refresh');
@@ -257,10 +263,12 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'moment', 'bootstrap-table',
             events: {
                 operate: {
                     'click .btn-editone': function (e, value, row, index) {
+                        e.stopPropagation();
                         var options = $(this).closest('table').bootstrapTable('getOptions');
                         Backend.api.open(options.extend.edit_url + "/ids/" + row[options.pk], __('Edit'));
                     },
                     'click .btn-delone': function (e, value, row, index) {
+                        e.stopPropagation();
                         var that = this;
                         var top = $(that).offset().top - $(window).scrollTop();
                         var left = $(that).offset().left - $(window).scrollLeft() - 260;
@@ -280,7 +288,6 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'moment', 'bootstrap-table',
                                     Backend.api.layer.close(index);
                                 }
                         );
-
                     }
                 }
             },
@@ -348,6 +355,18 @@ define(['jquery', 'bootstrap', 'backend', 'toastr', 'moment', 'bootstrap-table',
                     });
                     return html.join(' ');
                 },
+                label: function (value, row, index, custom) {
+                    var colorArr = ['success', 'warning', 'danger', 'info'];
+                    //渲染Flag
+                    var html = [];
+                    var arr = value.split(',');
+                    $.each(arr, function (i, value) {
+                        value = value.toString();
+                        var color = colorArr[i % colorArr.length];
+                        html.push('<span class="label label-' + color + '">' + __(value) + '</span>');
+                    });
+                    return html.join(' ');
+                },
                 datetime: function (value, row, index) {
                     return value ? Moment(parseInt(value) * 1000).format("YYYY-MM-DD HH:mm:ss") : __('None');
                 },