Jelajahi Sumber

升级版本 1.3.0-1.3.1 1.3.1-1.3.2 1.3.2-1.3.3

pengchanglu 3 tahun lalu
induk
melakukan
5ce79b42b6
100 mengubah file dengan 13658 tambahan dan 664 penghapusan
  1. 97 0
      application/README.md
  2. 3 0
      application/admin/command/Crud.php
  3. 1 1
      application/admin/command/Install.php
  4. 2 1
      application/admin/command/Install/fastadmin.sql
  5. 2 2
      application/admin/command/Install/zh-cn.php
  6. 1 16
      application/admin/controller/Addon.php
  7. 4 0
      application/admin/controller/general/Attachment.php
  8. 4 0
      application/admin/controller/general/Config.php
  9. 2 2
      application/admin/validate/Admin.php
  10. 2 2
      application/admin/validate/User.php
  11. 1 4
      application/admin/view/addon/index.html
  12. 1 1
      application/admin/view/general/attachment/select.html
  13. 1 1
      application/admin/view/user/user/edit.html
  14. 4 0
      application/api/controller/User.php
  15. 2 1
      application/api/lang/zh-cn/user.php
  16. 44 0
      application/application/admin/command/Addon/stubs/config.stub
  17. 604 0
      application/application/admin/command/Install/fastadmin.sql
  18. 453 0
      application/application/admin/controller/Addon.php
  19. 84 0
      application/application/admin/controller/Dashboard.php
  20. 138 0
      application/application/admin/controller/Index.php
  21. 296 0
      application/application/admin/controller/auth/Admin.php
  22. 12 0
      application/application/admin/lang/zh-cn/auth/admin.php
  23. 50 0
      application/application/admin/lang/zh-cn/dashboard.php
  24. 83 0
      application/application/admin/lang/zh-cn/general/config.php
  25. 14 0
      application/application/admin/lang/zh-cn/general/profile.php
  26. 530 0
      application/application/admin/library/Auth.php
  27. 55 0
      application/application/admin/validate/Admin.php
  28. 134 0
      application/application/admin/view/addon/config.html
  29. 21 0
      application/application/admin/view/auth/admin/index.html
  30. 21 0
      application/application/admin/view/auth/adminlog/index.html
  31. 21 0
      application/application/admin/view/auth/group/index.html
  32. 36 0
      application/application/admin/view/category/index.html
  33. 39 0
      application/application/admin/view/common/menu.html
  34. 403 0
      application/application/admin/view/dashboard/index.html
  35. 47 0
      application/application/admin/view/general/attachment/select.html
  36. 356 0
      application/application/admin/view/general/config/index.html
  37. 115 0
      application/application/admin/view/general/profile/index.html
  38. 143 0
      application/application/admin/view/index/login.html
  39. 28 0
      application/application/admin/view/user/group/index.html
  40. 28 0
      application/application/admin/view/user/rule/index.html
  41. 96 0
      application/application/api/controller/Ems.php
  42. 159 0
      application/application/common/library/Ems.php
  43. 437 0
      application/application/common/library/Upload.php
  44. 301 0
      application/application/config.php
  45. 95 0
      application/application/index/view/user/login.html
  46. 61 0
      application/application/index/view/user/register.html
  47. 34 0
      application/bower.json
  48. 44 0
      application/composer.json
  49. 2 2
      application/config.php
  50. 226 0
      application/extend/fast/Date.php
  51. 438 0
      application/extend/fast/Tree.php
  52. 1 1
      application/extra/site.php
  53. 6 3
      application/index/controller/User.php
  54. 1 0
      application/index/lang/zh-cn/user.php
  55. 1 1
      application/index/view/user/attachment.html
  56. 3 3
      application/index/view/user/changepwd.html
  57. 783 0
      application/public/assets/js/backend/addon.js
  58. 64 0
      application/public/assets/js/backend/auth/adminlog.js
  59. 362 0
      application/public/assets/js/fast.js
  60. 1 0
      application/public/assets/js/require-backend.min.js
  61. 686 0
      application/public/assets/js/require-form.js
  62. 1 0
      application/public/assets/js/require-frontend.min.js
  63. 983 0
      application/public/assets/js/require-table.js
  64. 2843 0
      application/vendor/composer/installed.json
  65. 38 0
      application/vendor/karsonzhang/fastadmin-addons/composer.json
  66. 1150 0
      application/vendor/karsonzhang/fastadmin-addons/src/addons/Service.php
  67. 1 1
      composer.json
  68. 0 1
      public/assets/css/backend.css
  69. 0 0
      public/assets/css/backend.min.css
  70. 26 0
      public/assets/css/frontend.css
  71. 0 0
      public/assets/css/frontend.min.css
  72. TEMPAT SAMPAH
      public/assets/img/login.jpg
  73. TEMPAT SAMPAH
      public/assets/img/login2.jpg
  74. TEMPAT SAMPAH
      public/assets/img/login3.jpg
  75. 8 2
      public/assets/js/backend/addon.js
  76. 0 0
      public/assets/js/require-backend.min.js
  77. 4 0
      public/assets/js/require-form.js
  78. 0 0
      public/assets/js/require-frontend.min.js
  79. 32 3
      public/assets/js/require-table.js
  80. 1 1
      public/assets/less/backend.less
  81. 29 0
      public/assets/less/frontend.less
  82. 6 6
      public/assets/libs/bootstrap/.bower.json
  83. 2 2
      public/assets/libs/bootstrap/CHANGELOG.md
  84. 4 2
      public/assets/libs/bootstrap/Gemfile
  85. 56 25
      public/assets/libs/bootstrap/Gemfile.lock
  86. 96 177
      public/assets/libs/bootstrap/Gruntfile.js
  87. 2 2
      public/assets/libs/bootstrap/ISSUE_TEMPLATE.md
  88. 1 1
      public/assets/libs/bootstrap/LICENSE
  89. 30 23
      public/assets/libs/bootstrap/README.md
  90. 1 1
      public/assets/libs/bootstrap/bower.json
  91. 100 100
      public/assets/libs/bootstrap/dist/css/bootstrap-theme.css
  92. 0 0
      public/assets/libs/bootstrap/dist/css/bootstrap-theme.css.map
  93. 2 2
      public/assets/libs/bootstrap/dist/css/bootstrap-theme.min.css
  94. 0 0
      public/assets/libs/bootstrap/dist/css/bootstrap-theme.min.css.map
  95. 255 173
      public/assets/libs/bootstrap/dist/css/bootstrap.css
  96. 0 0
      public/assets/libs/bootstrap/dist/css/bootstrap.css.map
  97. 2 2
      public/assets/libs/bootstrap/dist/css/bootstrap.min.css
  98. 0 0
      public/assets/libs/bootstrap/dist/css/bootstrap.min.css.map
  99. 300 97
      public/assets/libs/bootstrap/dist/js/bootstrap.js
  100. 2 2
      public/assets/libs/bootstrap/dist/js/bootstrap.min.js

+ 97 - 0
application/README.md

@@ -0,0 +1,97 @@
+FastAdmin是一款基于ThinkPHP+Bootstrap的极速后台开发框架。
+
+
+## 主要特性
+
+* 基于`Auth`验证的权限管理系统
+    * 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
+    * 支持单管理员多角色
+    * 支持管理子级数据或个人数据
+* 强大的一键生成功能
+    * 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
+    * 一键压缩打包JS和CSS文件,一键CDN静态资源部署
+    * 一键生成控制器菜单和规则
+    * 一键生成API接口文档
+* 完善的前端功能组件开发
+    * 基于`AdminLTE`二次开发
+    * 基于`Bootstrap`开发,自适应手机、平板、PC
+    * 基于`RequireJS`进行JS模块管理,按需加载
+    * 基于`Less`进行样式开发
+* 强大的插件扩展功能,在线安装卸载升级插件
+* 通用的会员模块和API模块
+* 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
+* 二级域名部署支持,同时域名支持绑定到应用插件
+* 多语言支持,服务端及客户端支持
+* 支持大文件分片上传、剪切板粘贴上传、拖拽上传,进度条显示,图片上传前压缩
+* 支持表格固定列、固定表头、跨页选择、Excel导出、模板渲染等功能
+* 强大的第三方应用模块支持([CMS](https://www.fastadmin.net/store/cms.html)、[博客](https://www.fastadmin.net/store/blog.html)、[知识付费问答](https://www.fastadmin.net/store/ask.html)、[在线投票系统](https://www.fastadmin.net/store/vote.html)、[B2C商城](https://www.fastadmin.net/store/shopro.html)、[B2B2C商城](https://www.fastadmin.net/store/wanlshop.html))
+* 支持CMS、博客、知识付费问答无缝整合[Xunsearch全文搜索](https://www.fastadmin.net/store/xunsearch.html)
+* 第三方小程序支持([CMS小程序](https://www.fastadmin.net/store/cms.html)、[预订小程序](https://www.fastadmin.net/store/ball.html)、[问答小程序](https://www.fastadmin.net/store/ask.html)、[点餐小程序](https://www.fastadmin.net/store/unidrink.html)、[B2C小程序](https://www.fastadmin.net/store/shopro.html)、[B2B2C小程序](https://www.fastadmin.net/store/wanlshop.html)、[博客小程序](https://www.fastadmin.net/store/blog.html))
+* 整合第三方短信接口(阿里云、腾讯云短信)
+* 无缝整合第三方云存储(七牛云、阿里云OSS、又拍云)功能,支持云储存分片上传
+* 第三方富文本编辑器支持(Summernote、Kindeditor、百度编辑器)
+* 第三方登录(QQ、微信、微博)整合
+* 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
+* 丰富的插件应用市场
+
+## 安装使用
+
+https://doc.fastadmin.net
+
+## 在线演示
+
+https://demo.fastadmin.net
+
+用户名:admin
+
+密 码:123456
+
+提 示:演示站数据无法进行修改,请下载源码安装体验全部功能
+
+## 界面截图
+![控制台](https://images.gitee.com/uploads/images/2020/0929/202947_8db2d281_10933.gif "控制台")
+
+## 问题反馈
+
+在使用中有任何问题,请使用以下联系方式联系我们
+
+交流社区: https://ask.fastadmin.net
+
+QQ群: [636393962](https://jq.qq.com/?_wv=1027&k=487PNBb)(满) [708784003](https://jq.qq.com/?_wv=1027&k=5ObjtwM)(满) [964776039](https://jq.qq.com/?_wv=1027&k=59qjU2P)(3群) [749803490](https://jq.qq.com/?_wv=1027&k=5tczi88)(满) [767103006](https://jq.qq.com/?_wv=1027&k=5Z1U751)(满) [675115483](https://jq.qq.com/?_wv=1027&k=54I6mts)(6群)
+
+Github: https://github.com/karsonzhang/fastadmin
+
+Gitee: https://gitee.com/karson/fastadmin
+
+## 特别鸣谢
+
+感谢以下的项目,排名不分先后
+
+ThinkPHP:http://www.thinkphp.cn
+
+AdminLTE:https://adminlte.io
+
+Bootstrap:http://getbootstrap.com
+
+jQuery:http://jquery.com
+
+Bootstrap-table:https://github.com/wenzhixin/bootstrap-table
+
+Nice-validator: https://validator.niceue.com
+
+SelectPage: https://github.com/TerryZ/SelectPage
+
+Layer: https://layer.layui.com
+
+DropzoneJS: https://www.dropzonejs.com
+
+
+## 版权信息
+
+FastAdmin遵循Apache2开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2017-2022 by FastAdmin (https://www.fastadmin.net)
+
+All rights reserved。

+ 3 - 0
application/admin/command/Crud.php

@@ -179,6 +179,8 @@ class Crud extends Command
         'url'    => 'url',
         'image'  => 'image',
         'images' => 'images',
+        'file'   => 'file',
+        'files'  => 'files',
         'avatar' => 'image',
         'switch' => 'toggle',
         'tag'    => 'flag',
@@ -910,6 +912,7 @@ class Crud extends Command
                                 $attrArr['data-source'] = 'auth/admin/selectpage';
                             } elseif ($selectpageController == 'user') {
                                 $attrArr['data-source'] = 'user/user/index';
+                                $attrArr['data-field'] = 'nickname';
                             }
                             if ($this->isMatchSuffix($field, $this->selectpagesSuffix)) {
                                 $attrArr['data-multiple'] = 'true';

+ 1 - 1
application/admin/command/Install.php

@@ -263,7 +263,7 @@ class Install extends Command
             $configList = $instance->name("config")->select();
             foreach ($configList as $k => $value) {
                 if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) {
-                    $value['value'] = explode(',', $value['value']);
+                    $value['value'] = is_array($value['value']) ? $value['value'] : explode(',', $value['value']);
                 }
                 if ($value['type'] == 'array') {
                     $value['value'] = (array)json_decode($value['value'], true);

+ 2 - 1
application/admin/command/Install/fastadmin.sql

@@ -392,6 +392,7 @@ CREATE TABLE `fa_sms` (
 DROP TABLE IF EXISTS `fa_test`;
 CREATE TABLE `fa_test` (
   `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `user_id` int(10) DEFAULT '0' COMMENT '会员ID',
   `admin_id` int(10) DEFAULT '0' COMMENT '管理员ID',
   `category_id` int(10) unsigned DEFAULT '0' COMMENT '分类ID(单选)',
   `category_ids` varchar(100) COMMENT '分类ID(多选)',
@@ -432,7 +433,7 @@ CREATE TABLE `fa_test` (
 -- Records of fa_test
 -- ----------------------------
 BEGIN;
-INSERT INTO `fa_test` VALUES (1, 0, 12, '12,13', '互联网,计算机', 'monday', 'hot,index', 'male', 'music,reading', '我是一篇测试文章', '<p>我是测试内容</p>', '/assets/img/avatar.png', '/assets/img/avatar.png,/assets/img/qrcode.png', '/assets/img/avatar.png', '关键字', '描述', '广西壮族自治区/百色市/平果县', '{\"a\":\"1\",\"b\":\"2\"}', '[{\"title\":\"标题一\",\"intro\":\"介绍一\",\"author\":\"小明\",\"age\":\"21\"}]', 0.00, 0, '2020-10-01 00:00:00 - 2021-10-31 23:59:59', '2017-07-10', '2017-07-10 18:24:45', 2017, '18:24:45', 1491635035, 1491635035, 1491635035, NULL, 0, 1, 'normal', '1');
+INSERT INTO `fa_test` VALUES (1, 1, 1, 12, '12,13', '互联网,计算机', 'monday', 'hot,index', 'male', 'music,reading', '我是一篇测试文章', '<p>我是测试内容</p>', '/assets/img/avatar.png', '/assets/img/avatar.png,/assets/img/qrcode.png', '/assets/img/avatar.png', '关键字', '描述', '广西壮族自治区/百色市/平果县', '{\"a\":\"1\",\"b\":\"2\"}', '[{\"title\":\"标题一\",\"intro\":\"介绍一\",\"author\":\"小明\",\"age\":\"21\"}]', 0.00, 0, '2020-10-01 00:00:00 - 2021-10-31 23:59:59', '2017-07-10', '2017-07-10 18:24:45', 2017, '18:24:45', 1491635035, 1491635035, 1491635035, NULL, 0, 1, 'normal', '1');
 COMMIT;
 
 -- ----------------------------

+ 2 - 2
application/admin/command/Install/zh-cn.php

@@ -22,8 +22,8 @@ return [
     'Install Successed'                                                                                     => '安装成功!',
     'Security tips'                                                                                         => '温馨提示:请将以下后台登录入口添加到你的收藏夹,为了你的安全,不要泄漏或发送给他人!如有泄漏请及时修改!',
     'Please input correct database'                                                                         => '请输入正确的数据库名',
-    'Please input correct username'                                                                         => '用户名只能由3-12位数字、字母、下划线组合',
-    'Please input correct password'                                                                         => '密码长度必须在6-16位之间,不能包含空格',
+    'Please input correct username'                                                                         => '用户名只能由3-30位数字、字母、下划线组合',
+    'Please input correct password'                                                                         => '密码长度必须在6-30位之间,不能包含空格',
     'Password is too weak'                                                                                  => '密码太简单,请重新输入',
     'The two passwords you entered did not match'                                                           => '两次输入的密码不一致',
     'Please input correct website'                                                                          => '网站名称输入不正确',

+ 1 - 16
application/admin/controller/Addon.php

@@ -41,19 +41,7 @@ class Addon extends Backend
             $v['config'] = $config ? 1 : 0;
             $v['url'] = str_replace($this->request->server('SCRIPT_NAME'), '', $v['url']);
         }
-        if ($this->request->isAjax()) {
-            $result = [];
-            debug('begin');
-            try {
-                $result = Service::addons($this->request->get());
-            } catch (\Exception $e) {
-                $this->error($e->getMessage());
-            }
-            debug('end');
-            \think\Log::record("tx:" . debug('begin', 'end', 6) . 's');
-            return json($result);
-        }
-        $this->assignconfig(['addons' => $addons, 'api_url' => config('fastadmin.api_url'), 'faversion' => config('fastadmin.version')]);
+        $this->assignconfig(['addons' => $addons, 'api_url' => config('fastadmin.api_url'), 'faversion' => config('fastadmin.version'), 'domain' => request()->host(true)]);
         return $this->view->fetch();
     }
 
@@ -227,9 +215,6 @@ class Addon extends Backend
     {
         Config::set('default_return_type', 'json');
 
-        if (!config('app_debug')) {
-            $this->error(__('Only work at debug mode'));
-        }
         $info = [];
         $file = $this->request->file('file');
         try {

+ 4 - 0
application/admin/controller/general/Attachment.php

@@ -86,6 +86,9 @@ class Attachment extends Backend
         if ($this->request->isAjax()) {
             return $this->index();
         }
+        $mimetype = $this->request->get('mimetype', '');
+        $mimetype = substr($mimetype, -1) === '/' ? $mimetype . '*' : $mimetype;
+        $this->view->assign('mimetype', $mimetype);
         return $this->view->fetch();
     }
 
@@ -154,4 +157,5 @@ class Attachment extends Backend
         \app\common\model\Attachment::where('id', 'in', $ids)->update(['category' => $category]);
         $this->success();
     }
+
 }

+ 4 - 0
application/admin/controller/general/Config.php

@@ -69,6 +69,10 @@ class Config extends Backend
                 $value['value'] = json_encode($dictValue, JSON_UNESCAPED_UNICODE);
             }
             $value['tip'] = htmlspecialchars($value['tip']);
+            if ($value['name'] == 'cdnurl') {
+                //cdnurl不支持在线修改
+                continue;
+            }
             $siteList[$v['group']]['list'][] = $value;
         }
         $index = 0;

+ 2 - 2
application/admin/validate/Admin.php

@@ -11,9 +11,9 @@ class Admin extends Validate
      * 验证规则
      */
     protected $rule = [
-        'username' => 'require|regex:\w{3,12}|unique:admin',
+        'username' => 'require|regex:\w{3,30}|unique:admin',
         'nickname' => 'require',
-        'password' => 'require|regex:\S{32}',
+        'password' => 'require|regex:\S{6,30}',
         'email'    => 'require|email|unique:admin,email',
     ];
 

+ 2 - 2
application/admin/validate/User.php

@@ -10,9 +10,9 @@ class User extends Validate
      * 验证规则
      */
     protected $rule = [
-        'username' => 'require|regex:\w{3,32}|unique:user',
+        'username' => 'require|regex:\w{3,30}|unique:user',
         'nickname' => 'require|unique:user',
-        'password' => 'regex:\S{6,32}',
+        'password' => 'regex:\S{6,30}',
         'email'    => 'require|email|unique:user',
         'mobile'   => 'unique:user'
     ];

+ 1 - 4
application/admin/view/addon/index.html

@@ -81,11 +81,9 @@
                     <div id="toolbar" class="toolbar">
                         <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" data-force-refresh="false"><i class="fa fa-refresh"></i> </a>
                         {if $Think.config.fastadmin.api_url}
-                        {if $Think.config.app_debug}
                         <button type="button" id="faupload-addon" class="btn btn-danger faupload btn-mini-xs" data-url="addon/local" data-chunking="false" data-mimetype="zip,fastaddon" data-multiple="false"><i class="fa fa-upload"></i>
                             {:__('Local install')}
                         </button>
-                        {/if}
                         <div class="btn-group">
                             <a href="#" class="btn btn-info btn-switch active btn-mini-xs" data-type="all"><i class="fa fa-list"></i> {:__('All')}</a>
                             <a href="#" class="btn btn-info btn-switch btn-mini-xs" data-type="free"><i class="fa fa-gift"></i> {:__('Free')}</a>
@@ -93,7 +91,6 @@
                             <a href="#" class="btn btn-info btn-switch btn-mini-xs" data-type="local" data-url="addon/downloaded"><i class="fa fa-laptop"></i> {:__('Local addon')}</a>
                         </div>
                         <a class="btn btn-primary btn-userinfo btn-mini-xs" href="javascript:;"><i class="fa fa-user"></i> {:__('Userinfo')}</a>
-                        <a class="btn btn-warning btn-authorization btn-mini-xs" href="javascript:;"><i class="fa fa-cloud"></i> {:__('Reload authorization')}</a>
                         {/if}
                     </div>
                     <table id="table" class="table table-striped table-bordered table-hover" width="100%">
@@ -189,7 +186,7 @@
     <div>
         <form class="form-horizontal form-userinfo">
             <fieldset>
-                <div class="alert alert-dismissable alert-success">
+                <div class="alert alert-dismissable alert-info-light">
                     <button type="button" class="close" data-dismiss="alert">×</button>
                     <strong>{:__('Warning')}</strong><br/>{:__('Logined tips', '<%=username%>')}
                 </div>

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

@@ -31,7 +31,7 @@
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
                         {:build_toolbar('refresh')}
-                        <span><button type="button" id="faupload-image" class="btn btn-success faupload" data-mimetype="{$Think.get.mimetype|default=''|htmlentities}" data-multiple="true"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                        <span><button type="button" id="faupload-image" class="btn btn-success faupload" data-mimetype="{$mimetype|default=''|htmlentities}" data-multiple="true"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                         {if request()->get('multiple') == 'true'}
                         <a class="btn btn-danger btn-choose-multi"><i class="fa fa-check"></i> {:__('Choose')}</a>
                         {/if}

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

@@ -22,7 +22,7 @@
     <div class="form-group">
         <label for="c-password" class="control-label col-xs-12 col-sm-2">{:__('Password')}:</label>
         <div class="col-xs-12 col-sm-4">
-            <input id="c-password" data-rule="password" class="form-control" name="row[password]" type="text" value="" placeholder="{:__('Leave password blank if dont want to change')}" autocomplete="new-password" />
+            <input id="c-password" data-rule="password" class="form-control" name="row[password]" type="password" value="" placeholder="{:__('Leave password blank if dont want to change')}" autocomplete="new-password" />
         </div>
     </div>
     <div class="form-group">

+ 4 - 0
application/api/controller/User.php

@@ -305,6 +305,10 @@ class User extends Api
         if (!$newpassword || !$captcha) {
             $this->error(__('Invalid parameters'));
         }
+        //验证Token
+        if (!Validate::make()->check(['newpassword' => $newpassword], ['newpassword' => 'require|regex:\S{6,30}'])) {
+            $this->error(__('Password must be 6 to 30 characters'));
+        }
         if ($type == 'mobile') {
             if (!Validate::regex($mobile, "^1\d{10}$")) {
                 $this->error(__('Mobile is incorrect'));

+ 2 - 1
application/api/lang/zh-cn/user.php

@@ -6,7 +6,8 @@ return [
     'Login'                                 => '登录',
     'Sign up successful'                    => '注册成功',
     'Username can not be empty'             => '用户名不能为空',
-    'Username must be 6 to 30 characters'   => '用户名必须6-30个字符',
+    'Username must be 3 to 30 characters'   => '用户名必须3-30个字符',
+    'Username must be 6 to 30 characters'   => '用户名必须3-30个字符',
     'Password can not be empty'             => '密码不能为空',
     'Password must be 6 to 30 characters'   => '密码必须6-30个字符',
     'Mobile is incorrect'                   => '手机格式不正确',

+ 44 - 0
application/application/admin/command/Addon/stubs/config.stub

@@ -0,0 +1,44 @@
+<?php
+
+return [
+    [
+        //配置唯一标识
+        'name'    => 'usernmae',
+        //显示的标题
+        'title'   => '用户名',
+        //类型
+        'type'    => 'string',
+        //分组
+        'group'    => '',
+        //动态显示
+        'visible'    => '',
+        //数据字典
+        'content' => [
+        ],
+        //值
+        'value'   => '',
+        //验证规则
+        'rule'    => 'required',
+        //错误消息
+        'msg'     => '',
+        //提示消息
+        'tip'     => '',
+        //成功消息
+        'ok'      => '',
+        //扩展信息
+        'extend'  => ''
+    ],
+    [
+        'name'    => 'password',
+        'title'   => '密码',
+        'type'    => 'string',
+        'content' => [
+        ],
+        'value'   => '',
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '',
+        'ok'      => '',
+        'extend'  => ''
+    ],
+];

+ 604 - 0
application/application/admin/command/Install/fastadmin.sql

@@ -0,0 +1,604 @@
+/*
+ FastAdmin Install SQL
+ Date: 2020-06-11 22:11:09
+*/
+
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for fa_admin
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_admin`;
+CREATE TABLE `fa_admin` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `username` varchar(20) DEFAULT '' COMMENT '用户名',
+  `nickname` varchar(50) DEFAULT '' COMMENT '昵称',
+  `password` varchar(32) DEFAULT '' COMMENT '密码',
+  `salt` varchar(30) DEFAULT '' COMMENT '密码盐',
+  `avatar` varchar(255) DEFAULT '' COMMENT '头像',
+  `email` varchar(100) DEFAULT '' COMMENT '电子邮箱',
+  `loginfailure` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '失败次数',
+  `logintime` int(10) DEFAULT NULL COMMENT '登录时间',
+  `loginip` varchar(50) DEFAULT NULL COMMENT '登录IP',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `token` varchar(59) DEFAULT '' COMMENT 'Session标识',
+  `status` varchar(30) NOT NULL DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `username` (`username`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='管理员表';
+
+-- ----------------------------
+-- Records of fa_admin
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_admin` VALUES (1, 'admin', 'Admin', '', '', '/assets/img/avatar.png', 'admin@admin.com', 0, 1491635035, '127.0.0.1',1491635035, 1491635035, '', 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_admin_log
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_admin_log`;
+CREATE TABLE `fa_admin_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
+  `username` varchar(30) DEFAULT '' COMMENT '管理员名字',
+  `url` varchar(1500) DEFAULT '' COMMENT '操作页面',
+  `title` varchar(100) DEFAULT '' COMMENT '日志标题',
+  `content` longtext NOT NULL COMMENT '内容',
+  `ip` varchar(50) DEFAULT '' COMMENT 'IP',
+  `useragent` varchar(255) DEFAULT '' COMMENT 'User-Agent',
+  `createtime` int(10) DEFAULT NULL COMMENT '操作时间',
+  PRIMARY KEY (`id`),
+  KEY `name` (`username`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='管理员日志表';
+
+-- ----------------------------
+-- Table structure for fa_area
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_area`;
+CREATE TABLE `fa_area` (
+  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `pid` int(10) DEFAULT NULL COMMENT '父id',
+  `shortname` varchar(100) DEFAULT NULL COMMENT '简称',
+  `name` varchar(100) DEFAULT NULL COMMENT '名称',
+  `mergename` varchar(255) DEFAULT NULL COMMENT '全称',
+  `level` tinyint(4) DEFAULT NULL COMMENT '层级:1=省,2=市,3=区/县',
+  `pinyin` varchar(100) DEFAULT NULL COMMENT '拼音',
+  `code` varchar(100) DEFAULT NULL COMMENT '长途区号',
+  `zip` varchar(100) DEFAULT NULL COMMENT '邮编',
+  `first` varchar(50) DEFAULT NULL COMMENT '首字母',
+  `lng` varchar(100) DEFAULT NULL COMMENT '经度',
+  `lat` varchar(100) DEFAULT NULL COMMENT '纬度',
+  PRIMARY KEY (`id`),
+  KEY `pid` (`pid`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='地区表';
+
+-- ----------------------------
+-- Table structure for fa_attachment
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_attachment`;
+CREATE TABLE `fa_attachment` (
+  `id` int(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `category` varchar(50) DEFAULT '' COMMENT '类别',
+  `admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `url` varchar(255) DEFAULT '' COMMENT '物理路径',
+  `imagewidth` varchar(30) DEFAULT '' COMMENT '宽度',
+  `imageheight` varchar(30) DEFAULT '' COMMENT '高度',
+  `imagetype` varchar(30) DEFAULT '' COMMENT '图片类型',
+  `imageframes` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '图片帧数',
+  `filename` varchar(100) DEFAULT '' COMMENT '文件名称',
+  `filesize` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小',
+  `mimetype` varchar(100) DEFAULT '' COMMENT 'mime类型',
+  `extparam` varchar(255) DEFAULT '' COMMENT '透传数据',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建日期',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `uploadtime` int(10) DEFAULT NULL COMMENT '上传时间',
+  `storage` varchar(100) NOT NULL DEFAULT 'local' COMMENT '存储位置',
+  `sha1` varchar(40) DEFAULT '' COMMENT '文件 sha1编码',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='附件表';
+
+-- ----------------------------
+-- Records of fa_attachment
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_attachment` VALUES (1, '', 1, 0, '/assets/img/qrcode.png', '150', '150', 'png', 0, 'qrcode.png', 21859, 'image/png', '', 1491635035, 1491635035, 1491635035, 'local', '17163603d0263e4838b9387ff2cd4877e8b018f6');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_auth_group
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_auth_group`;
+CREATE TABLE `fa_auth_group` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父组别',
+  `name` varchar(100) DEFAULT '' COMMENT '组名',
+  `rules` text NOT NULL COMMENT '规则ID',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `status` varchar(30) DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='分组表';
+
+-- ----------------------------
+-- Records of fa_auth_group
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_auth_group` VALUES (1, 0, 'Admin group', '*', 1491635035, 1491635035, 'normal');
+INSERT INTO `fa_auth_group` VALUES (2, 1, 'Second group', '13,14,16,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,40,41,42,43,44,45,46,47,48,49,50,55,56,57,58,59,60,61,62,63,64,65,1,9,10,11,7,6,8,2,4,5', 1491635035, 1491635035, 'normal');
+INSERT INTO `fa_auth_group` VALUES (3, 2, 'Third group', '1,4,9,10,11,13,14,15,16,17,40,41,42,43,44,45,46,47,48,49,50,55,56,57,58,59,60,61,62,63,64,65,5', 1491635035, 1491635035, 'normal');
+INSERT INTO `fa_auth_group` VALUES (4, 1, 'Second group 2', '1,4,13,14,15,16,17,55,56,57,58,59,60,61,62,63,64,65', 1491635035, 1491635035, 'normal');
+INSERT INTO `fa_auth_group` VALUES (5, 2, 'Third group 2', '1,2,6,7,8,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34', 1491635035, 1491635035, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_auth_group_access
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_auth_group_access`;
+CREATE TABLE `fa_auth_group_access` (
+  `uid` int(10) unsigned NOT NULL COMMENT '会员ID',
+  `group_id` int(10) unsigned NOT NULL COMMENT '级别ID',
+  UNIQUE KEY `uid_group_id` (`uid`,`group_id`),
+  KEY `uid` (`uid`),
+  KEY `group_id` (`group_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='权限分组表';
+
+-- ----------------------------
+-- Records of fa_auth_group_access
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_auth_group_access` VALUES (1, 1);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_auth_rule
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_auth_rule`;
+CREATE TABLE `fa_auth_rule` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `type` enum('menu','file') NOT NULL DEFAULT 'file' COMMENT 'menu为菜单,file为权限节点',
+  `pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父ID',
+  `name` varchar(100) DEFAULT '' COMMENT '规则名称',
+  `title` varchar(50) DEFAULT '' COMMENT '规则名称',
+  `icon` varchar(50) DEFAULT '' COMMENT '图标',
+  `url` varchar(255) DEFAULT '' COMMENT '规则URL',
+  `condition` varchar(255) DEFAULT '' COMMENT '条件',
+  `remark` varchar(255) DEFAULT '' COMMENT '备注',
+  `ismenu` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否为菜单',
+  `menutype` enum('addtabs','blank','dialog','ajax') DEFAULT NULL COMMENT '菜单类型',
+  `extend` varchar(255) DEFAULT '' COMMENT '扩展属性',
+  `py` varchar(30) DEFAULT '' COMMENT '拼音首字母',
+  `pinyin` varchar(100) DEFAULT '' COMMENT '拼音',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `status` varchar(30) DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`) USING BTREE,
+  KEY `pid` (`pid`),
+  KEY `weigh` (`weigh`)
+) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='节点表';
+
+-- ----------------------------
+-- Records of fa_auth_rule
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_auth_rule` VALUES (1, 'file', 0, 'dashboard', 'Dashboard', 'fa fa-dashboard', '', '', 'Dashboard tips', 1, NULL, '', 'kzt', 'kongzhitai', 1491635035, 1491635035, 143, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (2, 'file', 0, 'general', 'General', 'fa fa-cogs', '', '', '', 1, NULL, '', 'cggl', 'changguiguanli', 1491635035, 1491635035, 137, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (3, 'file', 0, 'category', 'Category', 'fa fa-leaf', '', '', 'Category tips', 0, NULL, '', 'flgl', 'fenleiguanli', 1491635035, 1491635035, 119, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (4, 'file', 0, 'addon', 'Addon', 'fa fa-rocket', '', '', 'Addon tips', 1, NULL, '', 'cjgl', 'chajianguanli', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (5, 'file', 0, 'auth', 'Auth', 'fa fa-group', '', '', '', 1, NULL, '', 'qxgl', 'quanxianguanli', 1491635035, 1491635035, 99, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (6, 'file', 2, 'general/config', 'Config', 'fa fa-cog', '', '', 'Config tips', 1, NULL, '', 'xtpz', 'xitongpeizhi', 1491635035, 1491635035, 60, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (7, 'file', 2, 'general/attachment', 'Attachment', 'fa fa-file-image-o', '', '', 'Attachment tips', 1, NULL, '', 'fjgl', 'fujianguanli', 1491635035, 1491635035, 53, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (8, 'file', 2, 'general/profile', 'Profile', 'fa fa-user', '', '', '', 1, NULL, '', 'grzl', 'gerenziliao', 1491635035, 1491635035, 34, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (9, 'file', 5, 'auth/admin', 'Admin', 'fa fa-user', '', '', 'Admin tips', 1, NULL, '', 'glygl', 'guanliyuanguanli', 1491635035, 1491635035, 118, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (10, 'file', 5, 'auth/adminlog', 'Admin log', 'fa fa-list-alt', '', '', 'Admin log tips', 1, NULL, '', 'glyrz', 'guanliyuanrizhi', 1491635035, 1491635035, 113, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (11, 'file', 5, 'auth/group', 'Group', 'fa fa-group', '', '', 'Group tips', 1, NULL, '', 'jsz', 'juesezu', 1491635035, 1491635035, 109, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (12, 'file', 5, 'auth/rule', 'Rule', 'fa fa-bars', '', '', 'Rule tips', 1, NULL, '', 'cdgz', 'caidanguize', 1491635035, 1491635035, 104, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (13, 'file', 1, 'dashboard/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 136, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (14, 'file', 1, 'dashboard/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 135, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (15, 'file', 1, 'dashboard/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 133, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (16, 'file', 1, 'dashboard/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 134, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (17, 'file', 1, 'dashboard/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 132, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (18, 'file', 6, 'general/config/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 52, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (19, 'file', 6, 'general/config/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 51, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (20, 'file', 6, 'general/config/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 50, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (21, 'file', 6, 'general/config/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 49, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (22, 'file', 6, 'general/config/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 48, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (23, 'file', 7, 'general/attachment/index', 'View', 'fa fa-circle-o', '', '', 'Attachment tips', 0, NULL, '', '', '', 1491635035, 1491635035, 59, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (24, 'file', 7, 'general/attachment/select', 'Select attachment', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 58, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (25, 'file', 7, 'general/attachment/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 57, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (26, 'file', 7, 'general/attachment/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 56, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (27, 'file', 7, 'general/attachment/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 55, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (28, 'file', 7, 'general/attachment/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 54, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (29, 'file', 8, 'general/profile/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 33, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (30, 'file', 8, 'general/profile/update', 'Update profile', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 32, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (31, 'file', 8, 'general/profile/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 31, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (32, 'file', 8, 'general/profile/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 30, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (33, 'file', 8, 'general/profile/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 29, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (34, 'file', 8, 'general/profile/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 28, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (35, 'file', 3, 'category/index', 'View', 'fa fa-circle-o', '', '', 'Category tips', 0, NULL, '', '', '', 1491635035, 1491635035, 142, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (36, 'file', 3, 'category/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 141, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (37, 'file', 3, 'category/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 140, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (38, 'file', 3, 'category/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 139, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (39, 'file', 3, 'category/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 138, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (40, 'file', 9, 'auth/admin/index', 'View', 'fa fa-circle-o', '', '', 'Admin tips', 0, NULL, '', '', '', 1491635035, 1491635035, 117, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (41, 'file', 9, 'auth/admin/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 116, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (42, 'file', 9, 'auth/admin/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 115, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (43, 'file', 9, 'auth/admin/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 114, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (44, 'file', 10, 'auth/adminlog/index', 'View', 'fa fa-circle-o', '', '', 'Admin log tips', 0, NULL, '', '', '', 1491635035, 1491635035, 112, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (45, 'file', 10, 'auth/adminlog/detail', 'Detail', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 111, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (46, 'file', 10, 'auth/adminlog/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 110, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (47, 'file', 11, 'auth/group/index', 'View', 'fa fa-circle-o', '', '', 'Group tips', 0, NULL, '', '', '', 1491635035, 1491635035, 108, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (48, 'file', 11, 'auth/group/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 107, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (49, 'file', 11, 'auth/group/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 106, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (50, 'file', 11, 'auth/group/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 105, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (51, 'file', 12, 'auth/rule/index', 'View', 'fa fa-circle-o', '', '', 'Rule tips', 0, NULL, '', '', '', 1491635035, 1491635035, 103, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (52, 'file', 12, 'auth/rule/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 102, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (53, 'file', 12, 'auth/rule/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 101, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (54, 'file', 12, 'auth/rule/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 100, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (55, 'file', 4, 'addon/index', 'View', 'fa fa-circle-o', '', '', 'Addon tips', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (56, 'file', 4, 'addon/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (57, 'file', 4, 'addon/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (58, 'file', 4, 'addon/del', 'Delete', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (59, 'file', 4, 'addon/downloaded', 'Local addon', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (60, 'file', 4, 'addon/state', 'Update state', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (63, 'file', 4, 'addon/config', 'Setting', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (64, 'file', 4, 'addon/refresh', 'Refresh', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (65, 'file', 4, 'addon/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (66, 'file', 0, 'user', 'User', 'fa fa-user-circle', '', '', '', 1, NULL, '', 'hygl', 'huiyuanguanli', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (67, 'file', 66, 'user/user', 'User', 'fa fa-user', '', '', '', 1, NULL, '', 'hygl', 'huiyuanguanli', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (68, 'file', 67, 'user/user/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (69, 'file', 67, 'user/user/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (70, 'file', 67, 'user/user/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (71, 'file', 67, 'user/user/del', 'Del', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (72, 'file', 67, 'user/user/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (73, 'file', 66, 'user/group', 'User group', 'fa fa-users', '', '', '', 1, NULL, '', 'hyfz', 'huiyuanfenzu', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (74, 'file', 73, 'user/group/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (75, 'file', 73, 'user/group/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (76, 'file', 73, 'user/group/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (77, 'file', 73, 'user/group/del', 'Del', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (78, 'file', 73, 'user/group/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (79, 'file', 66, 'user/rule', 'User rule', 'fa fa-circle-o', '', '', '', 1, NULL, '', 'hygz', 'huiyuanguize', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (80, 'file', 79, 'user/rule/index', 'View', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (81, 'file', 79, 'user/rule/del', 'Del', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (82, 'file', 79, 'user/rule/add', 'Add', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (83, 'file', 79, 'user/rule/edit', 'Edit', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (84, 'file', 79, 'user/rule/multi', 'Multi', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', 1491635035, 1491635035, 0, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_category
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_category`;
+CREATE TABLE `fa_category` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父ID',
+  `type` varchar(30) DEFAULT '' COMMENT '栏目类型',
+  `name` varchar(30) DEFAULT '',
+  `nickname` varchar(50) DEFAULT '',
+  `flag` set('hot','index','recommend') DEFAULT '',
+  `image` varchar(100) DEFAULT '' COMMENT '图片',
+  `keywords` varchar(255) DEFAULT '' COMMENT '关键字',
+  `description` varchar(255) DEFAULT '' COMMENT '描述',
+  `diyname` varchar(30) DEFAULT '' COMMENT '自定义名称',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `status` varchar(30) DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `weigh` (`weigh`,`id`),
+  KEY `pid` (`pid`)
+) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='分类表';
+
+-- ----------------------------
+-- Records of fa_category
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_category` VALUES (1, 0, 'page', '官方新闻', 'news', 'recommend', '/assets/img/qrcode.png', '', '', 'news', 1491635035, 1491635035, 1, 'normal');
+INSERT INTO `fa_category` VALUES (2, 0, 'page', '移动应用', 'mobileapp', 'hot', '/assets/img/qrcode.png', '', '', 'mobileapp', 1491635035, 1491635035, 2, 'normal');
+INSERT INTO `fa_category` VALUES (3, 2, 'page', '微信公众号', 'wechatpublic', 'index', '/assets/img/qrcode.png', '', '', 'wechatpublic', 1491635035, 1491635035, 3, 'normal');
+INSERT INTO `fa_category` VALUES (4, 2, 'page', 'Android开发', 'android', 'recommend', '/assets/img/qrcode.png', '', '', 'android', 1491635035, 1491635035, 4, 'normal');
+INSERT INTO `fa_category` VALUES (5, 0, 'page', '软件产品', 'software', 'recommend', '/assets/img/qrcode.png', '', '', 'software', 1491635035, 1491635035, 5, 'normal');
+INSERT INTO `fa_category` VALUES (6, 5, 'page', '网站建站', 'website', 'recommend', '/assets/img/qrcode.png', '', '', 'website', 1491635035, 1491635035, 6, 'normal');
+INSERT INTO `fa_category` VALUES (7, 5, 'page', '企业管理软件', 'company', 'index', '/assets/img/qrcode.png', '', '', 'company', 1491635035, 1491635035, 7, 'normal');
+INSERT INTO `fa_category` VALUES (8, 6, 'page', 'PC端', 'website-pc', 'recommend', '/assets/img/qrcode.png', '', '', 'website-pc', 1491635035, 1491635035, 8, 'normal');
+INSERT INTO `fa_category` VALUES (9, 6, 'page', '移动端', 'website-mobile', 'recommend', '/assets/img/qrcode.png', '', '', 'website-mobile', 1491635035, 1491635035, 9, 'normal');
+INSERT INTO `fa_category` VALUES (10, 7, 'page', 'CRM系统 ', 'company-crm', 'recommend', '/assets/img/qrcode.png', '', '', 'company-crm', 1491635035, 1491635035, 10, 'normal');
+INSERT INTO `fa_category` VALUES (11, 7, 'page', 'SASS平台软件', 'company-sass', 'recommend', '/assets/img/qrcode.png', '', '', 'company-sass', 1491635035, 1491635035, 11, 'normal');
+INSERT INTO `fa_category` VALUES (12, 0, 'test', '测试1', 'test1', 'recommend', '/assets/img/qrcode.png', '', '', 'test1', 1491635035, 1491635035, 12, 'normal');
+INSERT INTO `fa_category` VALUES (13, 0, 'test', '测试2', 'test2', 'recommend', '/assets/img/qrcode.png', '', '', 'test2', 1491635035, 1491635035, 13, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_config
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_config`;
+CREATE TABLE `fa_config` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(30) DEFAULT '' COMMENT '变量名',
+  `group` varchar(30) DEFAULT '' COMMENT '分组',
+  `title` varchar(100) DEFAULT '' COMMENT '变量标题',
+  `tip` varchar(100) DEFAULT '' COMMENT '变量描述',
+  `type` varchar(30) DEFAULT '' COMMENT '类型:string,text,int,bool,array,datetime,date,file',
+  `visible` varchar(255) DEFAULT '' COMMENT '可见条件',
+  `value` text COMMENT '变量值',
+  `content` text COMMENT '变量字典数据',
+  `rule` varchar(100) DEFAULT '' COMMENT '验证规则',
+  `extend` varchar(255) DEFAULT '' COMMENT '扩展属性',
+  `setting` varchar(255) DEFAULT '' COMMENT '配置',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='系统配置';
+
+-- ----------------------------
+-- Records of fa_config
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_config` VALUES (1, 'name', 'basic', 'Site name', '请填写站点名称', 'string', '', '我的网站', '', 'required', '', '');
+INSERT INTO `fa_config` VALUES (2, 'beian', 'basic', 'Beian', '粤ICP备15000000号-1', 'string', '', '', '', '', '', '');
+INSERT INTO `fa_config` VALUES (3, 'cdnurl', 'basic', 'Cdn url', '如果全站静态资源使用第三方云储存请配置该值', 'string', '', '', '', '', '', '');
+INSERT INTO `fa_config` VALUES (4, 'version', 'basic', 'Version', '如果静态资源有变动请重新配置该值', 'string', '', '1.0.1', '', 'required', '', '');
+INSERT INTO `fa_config` VALUES (5, 'timezone', 'basic', 'Timezone', '', 'string', '', 'Asia/Shanghai', '', 'required', '', '');
+INSERT INTO `fa_config` VALUES (6, 'forbiddenip', 'basic', 'Forbidden ip', '一行一条记录', 'text', '', '', '', '', '', '');
+INSERT INTO `fa_config` VALUES (7, 'languages', 'basic', 'Languages', '', 'array', '', '{\"backend\":\"zh-cn\",\"frontend\":\"zh-cn\"}', '', 'required', '', '');
+INSERT INTO `fa_config` VALUES (8, 'fixedpage', 'basic', 'Fixed page', '请尽量输入左侧菜单栏存在的链接', 'string', '', 'dashboard', '', 'required', '', '');
+INSERT INTO `fa_config` VALUES (9, 'categorytype', 'dictionary', 'Category type', '', 'array', '', '{\"default\":\"Default\",\"page\":\"Page\",\"article\":\"Article\",\"test\":\"Test\"}', '', '', '', '');
+INSERT INTO `fa_config` VALUES (10, 'configgroup', 'dictionary', 'Config group', '', 'array', '', '{\"basic\":\"Basic\",\"email\":\"Email\",\"dictionary\":\"Dictionary\",\"user\":\"User\",\"example\":\"Example\"}', '', '', '', '');
+INSERT INTO `fa_config` VALUES (11, 'mail_type', 'email', 'Mail type', '选择邮件发送方式', 'select', '', '1', '[\"请选择\",\"SMTP\"]', '', '', '');
+INSERT INTO `fa_config` VALUES (12, 'mail_smtp_host', 'email', 'Mail smtp host', '错误的配置发送邮件会导致服务器超时', 'string', '', 'smtp.qq.com', '', '', '', '');
+INSERT INTO `fa_config` VALUES (13, 'mail_smtp_port', 'email', 'Mail smtp port', '(不加密默认25,SSL默认465,TLS默认587)', 'string', '', '465', '', '', '', '');
+INSERT INTO `fa_config` VALUES (14, 'mail_smtp_user', 'email', 'Mail smtp user', '(填写完整用户名)', 'string', '', '10000', '', '', '', '');
+INSERT INTO `fa_config` VALUES (15, 'mail_smtp_pass', 'email', 'Mail smtp password', '(填写您的密码或授权码)', 'string', '', 'password', '', '', '', '');
+INSERT INTO `fa_config` VALUES (16, 'mail_verify_type', 'email', 'Mail vertify type', '(SMTP验证方式[推荐SSL])', 'select', '', '2', '[\"无\",\"TLS\",\"SSL\"]', '', '', '');
+INSERT INTO `fa_config` VALUES (17, 'mail_from', 'email', 'Mail from', '', 'string', '', '10000@qq.com', '', '', '', '');
+INSERT INTO `fa_config` VALUES (18, 'attachmentcategory', 'dictionary', 'Attachment category', '', 'array', '', '{\"category1\":\"Category1\",\"category2\":\"Category2\",\"custom\":\"Custom\"}', '', '', '', '');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_ems
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_ems`;
+CREATE TABLE `fa_ems`  (
+  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `event` varchar(30) DEFAULT '' COMMENT '事件',
+  `email` varchar(100) DEFAULT '' COMMENT '邮箱',
+  `code` varchar(10) DEFAULT '' COMMENT '验证码',
+  `times` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '验证次数',
+  `ip` varchar(30) DEFAULT '' COMMENT 'IP',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='邮箱验证码表';
+
+-- ----------------------------
+-- Table structure for fa_sms
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_sms`;
+CREATE TABLE `fa_sms` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `event` varchar(30) DEFAULT '' COMMENT '事件',
+  `mobile` varchar(20) DEFAULT '' COMMENT '手机号',
+  `code` varchar(10) DEFAULT '' COMMENT '验证码',
+  `times` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '验证次数',
+  `ip` varchar(30) DEFAULT '' COMMENT 'IP',
+  `createtime` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='短信验证码表';
+
+-- ----------------------------
+-- Table structure for fa_test
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_test`;
+CREATE TABLE `fa_test` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `user_id` int(10) DEFAULT '0' COMMENT '会员ID',
+  `admin_id` int(10) DEFAULT '0' COMMENT '管理员ID',
+  `category_id` int(10) unsigned DEFAULT '0' COMMENT '分类ID(单选)',
+  `category_ids` varchar(100) COMMENT '分类ID(多选)',
+  `tags` varchar(255) DEFAULT '' COMMENT '标签',
+  `week` enum('monday','tuesday','wednesday') COMMENT '星期(单选):monday=星期一,tuesday=星期二,wednesday=星期三',
+  `flag` set('hot','index','recommend') DEFAULT '' COMMENT '标志(多选):hot=热门,index=首页,recommend=推荐',
+  `genderdata` enum('male','female') DEFAULT 'male' COMMENT '性别(单选):male=男,female=女',
+  `hobbydata` set('music','reading','swimming') COMMENT '爱好(多选):music=音乐,reading=读书,swimming=游泳',
+  `title` varchar(100) DEFAULT '' COMMENT '标题',
+  `content` text COMMENT '内容',
+  `image` varchar(100) DEFAULT '' COMMENT '图片',
+  `images` varchar(1500) DEFAULT '' COMMENT '图片组',
+  `attachfile` varchar(100) DEFAULT '' COMMENT '附件',
+  `keywords` varchar(255) DEFAULT '' COMMENT '关键字',
+  `description` varchar(255) DEFAULT '' COMMENT '描述',
+  `city` varchar(100) DEFAULT '' COMMENT '省市',
+  `json` varchar(255) DEFAULT NULL COMMENT '配置:key=名称,value=值',
+  `multiplejson` varchar(1500) DEFAULT '' COMMENT '二维数组:title=标题,intro=介绍,author=作者,age=年龄',
+  `price` decimal(10,2) unsigned DEFAULT '0.00' COMMENT '价格',
+  `views` int(10) unsigned DEFAULT '0' COMMENT '点击',
+  `workrange` varchar(100) DEFAULT '' COMMENT '时间区间',
+  `startdate` date DEFAULT NULL COMMENT '开始日期',
+  `activitytime` datetime DEFAULT NULL COMMENT '活动时间(datetime)',
+  `year` year(4) DEFAULT NULL COMMENT '年',
+  `times` time DEFAULT NULL COMMENT '时间',
+  `refreshtime` int(10) DEFAULT NULL COMMENT '刷新时间(int)',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `deletetime` int(10) DEFAULT NULL COMMENT '删除时间',
+  `weigh` int(10) DEFAULT '0' COMMENT '权重',
+  `switch` tinyint(1) DEFAULT '0' COMMENT '开关',
+  `status` enum('normal','hidden') DEFAULT 'normal' COMMENT '状态',
+  `state` enum('0','1','2') DEFAULT '1' COMMENT '状态值:0=禁用,1=正常,2=推荐',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='测试表';
+
+-- ----------------------------
+-- Records of fa_test
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_test` VALUES (1, 1, 1, 12, '12,13', '互联网,计算机', 'monday', 'hot,index', 'male', 'music,reading', '我是一篇测试文章', '<p>我是测试内容</p>', '/assets/img/avatar.png', '/assets/img/avatar.png,/assets/img/qrcode.png', '/assets/img/avatar.png', '关键字', '描述', '广西壮族自治区/百色市/平果县', '{\"a\":\"1\",\"b\":\"2\"}', '[{\"title\":\"标题一\",\"intro\":\"介绍一\",\"author\":\"小明\",\"age\":\"21\"}]', 0.00, 0, '2020-10-01 00:00:00 - 2021-10-31 23:59:59', '2017-07-10', '2017-07-10 18:24:45', 2017, '18:24:45', 1491635035, 1491635035, 1491635035, NULL, 0, 1, 'normal', '1');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_user
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user`;
+CREATE TABLE `fa_user` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '组别ID',
+  `username` varchar(32) DEFAULT '' COMMENT '用户名',
+  `nickname` varchar(50) DEFAULT '' COMMENT '昵称',
+  `password` varchar(32) DEFAULT '' COMMENT '密码',
+  `salt` varchar(30) DEFAULT '' COMMENT '密码盐',
+  `email` varchar(100) DEFAULT '' COMMENT '电子邮箱',
+  `mobile` varchar(11) DEFAULT '' COMMENT '手机号',
+  `avatar` varchar(255) DEFAULT '' COMMENT '头像',
+  `level` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '等级',
+  `gender` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '性别',
+  `birthday` date DEFAULT NULL COMMENT '生日',
+  `bio` varchar(100) DEFAULT '' COMMENT '格言',
+  `money` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '余额',
+  `score` int(10) NOT NULL DEFAULT '0' COMMENT '积分',
+  `successions` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '连续登录天数',
+  `maxsuccessions` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '最大连续登录天数',
+  `prevtime` int(10) DEFAULT NULL COMMENT '上次登录时间',
+  `logintime` int(10) DEFAULT NULL COMMENT '登录时间',
+  `loginip` varchar(50) DEFAULT '' COMMENT '登录IP',
+  `loginfailure` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '失败次数',
+  `joinip` varchar(50) DEFAULT '' COMMENT '加入IP',
+  `jointime` int(10) DEFAULT NULL COMMENT '加入时间',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `token` varchar(50) DEFAULT '' COMMENT 'Token',
+  `status` varchar(30) DEFAULT '' COMMENT '状态',
+  `verification` varchar(255) DEFAULT '' COMMENT '验证',
+  PRIMARY KEY (`id`),
+  KEY `username` (`username`),
+  KEY `email` (`email`),
+  KEY `mobile` (`mobile`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='会员表';
+
+-- ----------------------------
+-- Records of fa_user
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_user` VALUES (1, 1, 'admin', 'admin', '', '', 'admin@163.com', '13888888888', '', 0, 0, '2017-04-08', '', 0, 0, 1, 1, 1491635035, 1491635035, '127.0.0.1', 0, '127.0.0.1', 1491635035, 0, 1491635035, '', 'normal','');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_user_group
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_group`;
+CREATE TABLE `fa_user_group` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(50) DEFAULT '' COMMENT '组名',
+  `rules` text COMMENT '权限节点',
+  `createtime` int(10) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `status` enum('normal','hidden') DEFAULT NULL COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='会员组表';
+
+-- ----------------------------
+-- Records of fa_user_group
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_user_group` VALUES (1, '默认组', '1,2,3,4,5,6,7,8,9,10,11,12', 1491635035, 1491635035, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_user_money_log
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_money_log`;
+CREATE TABLE `fa_user_money_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `money` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更余额',
+  `before` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更前余额',
+  `after` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更后余额',
+  `memo` varchar(255) DEFAULT '' COMMENT '备注',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='会员余额变动表';
+
+-- ----------------------------
+-- Table structure for fa_user_rule
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_rule`;
+CREATE TABLE `fa_user_rule` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int(10) DEFAULT NULL COMMENT '父ID',
+  `name` varchar(50) DEFAULT NULL COMMENT '名称',
+  `title` varchar(50) DEFAULT '' COMMENT '标题',
+  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
+  `ismenu` tinyint(1) DEFAULT NULL COMMENT '是否菜单',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `weigh` int(10) DEFAULT '0' COMMENT '权重',
+  `status` enum('normal','hidden') DEFAULT NULL COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='会员规则表';
+
+-- ----------------------------
+-- Records of fa_user_rule
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_user_rule` VALUES (1, 0, 'index', 'Frontend', '', 1, 1491635035, 1491635035, 1, 'normal');
+INSERT INTO `fa_user_rule` VALUES (2, 0, 'api', 'API Interface', '', 1, 1491635035, 1491635035, 2, 'normal');
+INSERT INTO `fa_user_rule` VALUES (3, 1, 'user', 'User Module', '', 1, 1491635035, 1491635035, 12, 'normal');
+INSERT INTO `fa_user_rule` VALUES (4, 2, 'user', 'User Module', '', 1, 1491635035, 1491635035, 11, 'normal');
+INSERT INTO `fa_user_rule` VALUES (5, 3, 'index/user/login', 'Login', '', 0, 1491635035, 1491635035, 5, 'normal');
+INSERT INTO `fa_user_rule` VALUES (6, 3, 'index/user/register', 'Register', '', 0, 1491635035, 1491635035, 7, 'normal');
+INSERT INTO `fa_user_rule` VALUES (7, 3, 'index/user/index', 'User Center', '', 0, 1491635035, 1491635035, 9, 'normal');
+INSERT INTO `fa_user_rule` VALUES (8, 3, 'index/user/profile', 'Profile', '', 0, 1491635035, 1491635035, 4, 'normal');
+INSERT INTO `fa_user_rule` VALUES (9, 4, 'api/user/login', 'Login', '', 0, 1491635035, 1491635035, 6, 'normal');
+INSERT INTO `fa_user_rule` VALUES (10, 4, 'api/user/register', 'Register', '', 0, 1491635035, 1491635035, 8, 'normal');
+INSERT INTO `fa_user_rule` VALUES (11, 4, 'api/user/index', 'User Center', '', 0, 1491635035, 1491635035, 10, 'normal');
+INSERT INTO `fa_user_rule` VALUES (12, 4, 'api/user/profile', 'Profile', '', 0, 1491635035, 1491635035, 3, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_user_score_log
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_score_log`;
+CREATE TABLE `fa_user_score_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `score` int(10) NOT NULL DEFAULT '0' COMMENT '变更积分',
+  `before` int(10) NOT NULL DEFAULT '0' COMMENT '变更前积分',
+  `after` int(10) NOT NULL DEFAULT '0' COMMENT '变更后积分',
+  `memo` varchar(255) DEFAULT '' COMMENT '备注',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='会员积分变动表';
+
+-- ----------------------------
+-- Table structure for fa_user_token
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_token`;
+CREATE TABLE `fa_user_token` (
+  `token` varchar(50) NOT NULL COMMENT 'Token',
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `expiretime` int(10) DEFAULT NULL COMMENT '过期时间',
+  PRIMARY KEY (`token`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='会员Token表';
+
+-- ----------------------------
+-- Table structure for fa_version
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_version`;
+CREATE TABLE `fa_version`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `oldversion` varchar(30) DEFAULT '' COMMENT '旧版本号',
+  `newversion` varchar(30) DEFAULT '' COMMENT '新版本号',
+  `packagesize` varchar(30) DEFAULT '' COMMENT '包大小',
+  `content` varchar(500) DEFAULT '' COMMENT '升级内容',
+  `downloadurl` varchar(255) DEFAULT '' COMMENT '下载地址',
+  `enforce` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '强制更新',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `weigh` int(10) NOT NULL DEFAULT 0 COMMENT '权重',
+  `status` varchar(30) DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='版本表';
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 453 - 0
application/application/admin/controller/Addon.php

@@ -0,0 +1,453 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use fast\Http;
+use think\addons\AddonException;
+use think\addons\Service;
+use think\Cache;
+use think\Config;
+use think\Db;
+use think\Exception;
+
+/**
+ * 插件管理
+ *
+ * @icon   fa fa-cube
+ * @remark 可在线安装、卸载、禁用、启用、配置、升级插件,插件升级前请做好备份。
+ */
+class Addon extends Backend
+{
+    protected $model = null;
+    protected $noNeedRight = ['get_table_list'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        if (!$this->auth->isSuperAdmin() && in_array($this->request->action(), ['install', 'uninstall', 'local', 'upgrade', 'authorization', 'testdata'])) {
+            $this->error(__('Access is allowed only to the super management group'));
+        }
+    }
+
+    /**
+     * 插件列表
+     */
+    public function index()
+    {
+        $addons = get_addon_list();
+        foreach ($addons as $k => &$v) {
+            $config = get_addon_config($v['name']);
+            $v['config'] = $config ? 1 : 0;
+            $v['url'] = str_replace($this->request->server('SCRIPT_NAME'), '', $v['url']);
+        }
+        $this->assignconfig(['addons' => $addons, 'api_url' => config('fastadmin.api_url'), 'faversion' => config('fastadmin.version'), 'domain' => request()->host(true)]);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 配置
+     */
+    public function config($name = null)
+    {
+        $name = $name ? $name : $this->request->get("name");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        $info = get_addon_info($name);
+        $config = get_addon_fullconfig($name);
+        if (!$info) {
+            $this->error(__('Addon not exists'));
+        }
+        if ($this->request->isPost()) {
+            $params = $this->request->post("row/a", [], 'trim');
+            if ($params) {
+                foreach ($config as $k => &$v) {
+                    if (isset($params[$v['name']])) {
+                        if ($v['type'] == 'array') {
+                            $params[$v['name']] = is_array($params[$v['name']]) ? $params[$v['name']] : (array)json_decode($params[$v['name']], true);
+                            $value = $params[$v['name']];
+                        } else {
+                            $value = is_array($params[$v['name']]) ? implode(',', $params[$v['name']]) : $params[$v['name']];
+                        }
+                        $v['value'] = $value;
+                    }
+                }
+                try {
+                    $addon = get_addon_instance($name);
+                    //插件自定义配置实现逻辑
+                    if (method_exists($addon, 'config')) {
+                        $addon->config($name, $config);
+                    } else {
+                        //更新配置文件
+                        set_addon_fullconfig($name, $config);
+                        Service::refresh();
+                    }
+                } catch (Exception $e) {
+                    $this->error(__($e->getMessage()));
+                }
+                $this->success();
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $tips = [];
+        $groupList = [];
+        foreach ($config as $index => &$item) {
+            //如果有设置分组
+            if (isset($item['group']) && $item['group']) {
+                if (!in_array($item['group'], $groupList)) {
+                    $groupList["custom" . (count($groupList) + 1)] = $item['group'];
+                }
+            }
+            if ($item['name'] == '__tips__') {
+                $tips = $item;
+                unset($config[$index]);
+            }
+        }
+        $groupList['other'] = '其它';
+        $this->view->assign("groupList", $groupList);
+        $this->view->assign("addon", ['info' => $info, 'config' => $config, 'tips' => $tips]);
+        $configFile = ADDON_PATH . $name . DS . 'config.html';
+        $viewFile = is_file($configFile) ? $configFile : '';
+        return $this->view->fetch($viewFile);
+    }
+
+    /**
+     * 安装
+     */
+    public function install()
+    {
+        $name = $this->request->post("name");
+        $force = (int)$this->request->post("force");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+
+        $info = [];
+        try {
+            $uid = $this->request->post("uid");
+            $token = $this->request->post("token");
+            $version = $this->request->post("version");
+            $faversion = $this->request->post("faversion");
+            $extend = [
+                'uid'       => $uid,
+                'token'     => $token,
+                'version'   => $version,
+                'faversion' => $faversion
+            ];
+            $info = Service::install($name, $force, $extend);
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()), $e->getCode());
+        }
+        $this->success(__('Install successful'), '', ['addon' => $info]);
+    }
+
+    /**
+     * 卸载
+     */
+    public function uninstall()
+    {
+        $name = $this->request->post("name");
+        $force = (int)$this->request->post("force");
+        $droptables = (int)$this->request->post("droptables");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        //只有开启调试且为超级管理员才允许删除相关数据库
+        $tables = [];
+        if ($droptables && Config::get("app_debug") && $this->auth->isSuperAdmin()) {
+            $tables = get_addon_tables($name);
+        }
+        try {
+            Service::uninstall($name, $force);
+            if ($tables) {
+                $prefix = Config::get('database.prefix');
+                //删除插件关联表
+                foreach ($tables as $index => $table) {
+                    //忽略非插件标识的表名
+                    if (!preg_match("/^{$prefix}{$name}/", $table)) {
+                        continue;
+                    }
+                    Db::execute("DROP TABLE IF EXISTS `{$table}`");
+                }
+            }
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success(__('Uninstall successful'));
+    }
+
+    /**
+     * 禁用启用
+     */
+    public function state()
+    {
+        $name = $this->request->post("name");
+        $action = $this->request->post("action");
+        $force = (int)$this->request->post("force");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        try {
+            $action = $action == 'enable' ? $action : 'disable';
+            //调用启用、禁用的方法
+            Service::$action($name, $force);
+            Cache::rm('__menu__');
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success(__('Operate successful'));
+    }
+
+    /**
+     * 本地上传
+     */
+    public function local()
+    {
+        Config::set('default_return_type', 'json');
+
+        $info = [];
+        $file = $this->request->file('file');
+        try {
+            $uid = $this->request->post("uid");
+            $token = $this->request->post("token");
+            $faversion = $this->request->post("faversion");
+            if (!$uid || !$token) {
+                throw new Exception(__('Please login and try to install'));
+            }
+            $extend = [
+                'uid'       => $uid,
+                'token'     => $token,
+                'faversion' => $faversion
+            ];
+            $info = Service::local($file, $extend);
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success(__('Offline installed tips'), '', ['addon' => $info]);
+    }
+
+    /**
+     * 更新插件
+     */
+    public function upgrade()
+    {
+        $name = $this->request->post("name");
+        $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        if (!is_dir($addonTmpDir)) {
+            @mkdir($addonTmpDir, 0755, true);
+        }
+
+        $info = [];
+        try {
+            $uid = $this->request->post("uid");
+            $token = $this->request->post("token");
+            $version = $this->request->post("version");
+            $faversion = $this->request->post("faversion");
+            $extend = [
+                'uid'       => $uid,
+                'token'     => $token,
+                'version'   => $version,
+                'faversion' => $faversion
+            ];
+            //调用更新的方法
+            $info = Service::upgrade($name, $extend);
+            Cache::rm('__menu__');
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success(__('Operate successful'), '', ['addon' => $info]);
+    }
+
+    /**
+     * 测试数据
+     */
+    public function testdata()
+    {
+        $name = $this->request->post("name");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+
+        try {
+            Service::importsql($name, 'testdata.sql');
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()), $e->getCode());
+        }
+        $this->success(__('Import successful'), '');
+    }
+
+    /**
+     * 已装插件
+     */
+    public function downloaded()
+    {
+        $offset = (int)$this->request->get("offset");
+        $limit = (int)$this->request->get("limit");
+        $filter = $this->request->get("filter");
+        $search = $this->request->get("search");
+        $search = htmlspecialchars(strip_tags($search));
+        $onlineaddons = $this->getAddonList();
+        $filter = (array)json_decode($filter, true);
+        $addons = get_addon_list();
+        $list = [];
+        foreach ($addons as $k => $v) {
+            if ($search && stripos($v['name'], $search) === false && stripos($v['title'], $search) === false && stripos($v['intro'], $search) === false) {
+                continue;
+            }
+
+            if (isset($onlineaddons[$v['name']])) {
+                $v = array_merge($v, $onlineaddons[$v['name']]);
+                $v['price'] = '-';
+            } else {
+                $v['category_id'] = 0;
+                $v['flag'] = '';
+                $v['banner'] = '';
+                $v['image'] = '';
+                $v['donateimage'] = '';
+                $v['demourl'] = '';
+                $v['price'] = __('None');
+                $v['screenshots'] = [];
+                $v['releaselist'] = [];
+                $v['url'] = addon_url($v['name']);
+                $v['url'] = str_replace($this->request->server('SCRIPT_NAME'), '', $v['url']);
+            }
+            $v['createtime'] = filemtime(ADDON_PATH . $v['name']);
+            if ($filter && isset($filter['category_id']) && is_numeric($filter['category_id']) && $filter['category_id'] != $v['category_id']) {
+                continue;
+            }
+            $list[] = $v;
+        }
+        $total = count($list);
+        if ($limit) {
+            $list = array_slice($list, $offset, $limit);
+        }
+        $result = array("total" => $total, "rows" => $list);
+
+        $callback = $this->request->get('callback') ? "jsonp" : "json";
+        return $callback($result);
+    }
+
+    /**
+     * 检测
+     */
+    public function isbuy()
+    {
+        $name = $this->request->post("name");
+        $uid = $this->request->post("uid");
+        $token = $this->request->post("token");
+        $version = $this->request->post("version");
+        $faversion = $this->request->post("faversion");
+        $extend = [
+            'uid'       => $uid,
+            'token'     => $token,
+            'version'   => $version,
+            'faversion' => $faversion
+        ];
+        try {
+            $result = Service::isBuy($name, $extend);
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        return json($result);
+    }
+
+    /**
+     * 刷新授权
+     */
+    public function authorization()
+    {
+        $params = [
+            'uid'       => $this->request->post('uid'),
+            'token'     => $this->request->post('token'),
+            'faversion' => $this->request->post('faversion'),
+        ];
+        try {
+            Service::authorization($params);
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success(__('Operate successful'));
+    }
+
+    /**
+     * 获取插件相关表
+     */
+    public function get_table_list()
+    {
+        $name = $this->request->post("name");
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        $tables = get_addon_tables($name);
+        $prefix = Config::get('database.prefix');
+        foreach ($tables as $index => $table) {
+            //忽略非插件标识的表名
+            if (!preg_match("/^{$prefix}{$name}/", $table)) {
+                unset($tables[$index]);
+            }
+        }
+        $tables = array_values($tables);
+        $this->success('', null, ['tables' => $tables]);
+    }
+
+    protected function getAddonList()
+    {
+        $onlineaddons = Cache::get("onlineaddons");
+        if (!is_array($onlineaddons) && config('fastadmin.api_url')) {
+            $onlineaddons = [];
+            $params = [
+                'uid'       => $this->request->post('uid'),
+                'token'     => $this->request->post('token'),
+                'version'   => config('fastadmin.version'),
+                'faversion' => config('fastadmin.version'),
+            ];
+            $json = [];
+            try {
+                $json = Service::addons($params);
+            } catch (\Exception $e) {
+
+            }
+            $rows = isset($json['rows']) ? $json['rows'] : [];
+            foreach ($rows as $index => $row) {
+                $onlineaddons[$row['name']] = $row;
+            }
+            Cache::set("onlineaddons", $onlineaddons, 600);
+        }
+        return $onlineaddons;
+    }
+
+}

+ 84 - 0
application/application/admin/controller/Dashboard.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\model\Admin;
+use app\admin\model\User;
+use app\common\controller\Backend;
+use app\common\model\Attachment;
+use fast\Date;
+use think\Db;
+
+/**
+ * 控制台
+ *
+ * @icon   fa fa-dashboard
+ * @remark 用于展示当前系统中的统计数据、统计报表及重要实时数据
+ */
+class Dashboard extends Backend
+{
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        try {
+            \think\Db::execute("SET @@sql_mode='';");
+        } catch (\Exception $e) {
+
+        }
+        $column = [];
+        $starttime = Date::unixtime('day', -6);
+        $endtime = Date::unixtime('day', 0, 'end');
+        $joinlist = Db("user")->where('jointime', 'between time', [$starttime, $endtime])
+            ->field('jointime, status, COUNT(*) AS nums, DATE_FORMAT(FROM_UNIXTIME(jointime), "%Y-%m-%d") AS join_date')
+            ->group('join_date')
+            ->select();
+        for ($time = $starttime; $time <= $endtime;) {
+            $column[] = date("Y-m-d", $time);
+            $time += 86400;
+        }
+        $userlist = array_fill_keys($column, 0);
+        foreach ($joinlist as $k => $v) {
+            $userlist[$v['join_date']] = $v['nums'];
+        }
+
+        $dbTableList = Db::query("SHOW TABLE STATUS");
+        $addonList = get_addon_list();
+        $totalworkingaddon = 0;
+        $totaladdon = count($addonList);
+        foreach ($addonList as $index => $item) {
+            if ($item['state']) {
+                $totalworkingaddon += 1;
+            }
+        }
+        $this->view->assign([
+            'totaluser'         => User::count(),
+            'totaladdon'        => $totaladdon,
+            'totaladmin'        => Admin::count(),
+            'totalcategory'     => \app\common\model\Category::count(),
+            'todayusersignup'   => User::whereTime('jointime', 'today')->count(),
+            'todayuserlogin'    => User::whereTime('logintime', 'today')->count(),
+            'sevendau'          => User::whereTime('jointime|logintime|prevtime', '-7 days')->count(),
+            'thirtydau'         => User::whereTime('jointime|logintime|prevtime', '-30 days')->count(),
+            'threednu'          => User::whereTime('jointime', '-3 days')->count(),
+            'sevendnu'          => User::whereTime('jointime', '-7 days')->count(),
+            'dbtablenums'       => count($dbTableList),
+            'dbsize'            => array_sum(array_map(function ($item) {
+                return $item['Data_length'] + $item['Index_length'];
+            }, $dbTableList)),
+            'totalworkingaddon' => $totalworkingaddon,
+            'attachmentnums'    => Attachment::count(),
+            'attachmentsize'    => Attachment::sum('filesize'),
+            'picturenums'       => Attachment::where('mimetype', 'like', 'image/%')->count(),
+            'picturesize'       => Attachment::where('mimetype', 'like', 'image/%')->sum('filesize'),
+        ]);
+
+        $this->assignconfig('column', array_keys($userlist));
+        $this->assignconfig('userdata', array_values($userlist));
+
+        return $this->view->fetch();
+    }
+
+}

+ 138 - 0
application/application/admin/controller/Index.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\model\AdminLog;
+use app\common\controller\Backend;
+use think\Config;
+use think\Hook;
+use think\Session;
+use think\Validate;
+
+/**
+ * 后台首页
+ * @internal
+ */
+class Index extends Backend
+{
+
+    protected $noNeedLogin = ['login'];
+    protected $noNeedRight = ['index', 'logout'];
+    protected $layout = '';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        //移除HTML标签
+        $this->request->filter('trim,strip_tags,htmlspecialchars');
+    }
+
+    /**
+     * 后台首页
+     */
+    public function index()
+    {
+        $cookieArr = ['adminskin' => "/^skin\-([a-z\-]+)\$/i", 'multiplenav' => "/^(0|1)\$/", 'multipletab' => "/^(0|1)\$/", 'show_submenu' => "/^(0|1)\$/"];
+        foreach ($cookieArr as $key => $regex) {
+            $cookieValue = $this->request->cookie($key);
+            if (!is_null($cookieValue) && preg_match($regex, $cookieValue)) {
+                config('fastadmin.' . $key, $cookieValue);
+            }
+        }
+        //左侧菜单
+        list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar([
+            'dashboard' => 'hot',
+            'addon'     => ['new', 'red', 'badge'],
+            'auth/rule' => __('Menu'),
+            'general'   => ['new', 'purple'],
+        ], $this->view->site['fixedpage']);
+        $action = $this->request->request('action');
+        if ($this->request->isPost()) {
+            if ($action == 'refreshmenu') {
+                $this->success('', null, ['menulist' => $menulist, 'navlist' => $navlist]);
+            }
+        }
+        $this->assignconfig('cookie', ['prefix' => config('cookie.prefix')]);
+        $this->view->assign('menulist', $menulist);
+        $this->view->assign('navlist', $navlist);
+        $this->view->assign('fixedmenu', $fixedmenu);
+        $this->view->assign('referermenu', $referermenu);
+        $this->view->assign('title', __('Home'));
+        return $this->view->fetch();
+    }
+
+    /**
+     * 管理员登录
+     */
+    public function login()
+    {
+        $url = $this->request->get('url', 'index/index');
+        if ($this->auth->isLogin()) {
+            $this->success(__("You've logged in, do not login again"), $url);
+        }
+        if ($this->request->isPost()) {
+            $username = $this->request->post('username');
+            $password = $this->request->post('password');
+            $keeplogin = $this->request->post('keeplogin');
+            $token = $this->request->post('__token__');
+            $rule = [
+                'username'  => 'require|length:3,30',
+                'password'  => 'require|length:3,30',
+                '__token__' => 'require|token',
+            ];
+            $data = [
+                'username'  => $username,
+                'password'  => $password,
+                '__token__' => $token,
+            ];
+            if (Config::get('fastadmin.login_captcha')) {
+                $rule['captcha'] = 'require|captcha';
+                $data['captcha'] = $this->request->post('captcha');
+            }
+            $validate = new Validate($rule, [], ['username' => __('Username'), 'password' => __('Password'), 'captcha' => __('Captcha')]);
+            $result = $validate->check($data);
+            if (!$result) {
+                $this->error($validate->getError(), $url, ['token' => $this->request->token()]);
+            }
+            AdminLog::setTitle(__('Login'));
+            $result = $this->auth->login($username, $password, $keeplogin ? 86400 : 0);
+            if ($result === true) {
+                Hook::listen("admin_login_after", $this->request);
+                $this->success(__('Login successful'), $url, ['url' => $url, 'id' => $this->auth->id, 'username' => $username, 'avatar' => $this->auth->avatar]);
+            } else {
+                $msg = $this->auth->getError();
+                $msg = $msg ? $msg : __('Username or password is incorrect');
+                $this->error($msg, $url, ['token' => $this->request->token()]);
+            }
+        }
+
+        // 根据客户端的cookie,判断是否可以自动登录
+        if ($this->auth->autologin()) {
+            Session::delete("referer");
+            $this->redirect($url);
+        }
+        $background = Config::get('fastadmin.login_background');
+        $background = $background ? (stripos($background, 'http') === 0 ? $background : config('site.cdnurl') . $background) : '';
+        $this->view->assign('background', $background);
+        $this->view->assign('title', __('Login'));
+        Hook::listen("admin_login_init", $this->request);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 退出登录
+     */
+    public function logout()
+    {
+        if ($this->request->isPost()) {
+            $this->auth->logout();
+            Hook::listen("admin_logout_after", $this->request);
+            $this->success(__('Logout successful'), 'index/login');
+        }
+        $html = "<form id='logout_submit' name='logout_submit' action='' method='post'>" . token() . "<input type='submit' value='ok' style='display:none;'></form>";
+        $html .= "<script>document.forms['logout_submit'].submit();</script>";
+
+        return $html;
+    }
+
+}

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

@@ -0,0 +1,296 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use app\admin\model\AuthGroup;
+use app\admin\model\AuthGroupAccess;
+use app\common\controller\Backend;
+use fast\Random;
+use fast\Tree;
+use think\Db;
+use think\Validate;
+
+/**
+ * 管理员管理
+ *
+ * @icon   fa fa-users
+ * @remark 一个管理员可以有多个角色组,左侧的菜单根据管理员所拥有的权限进行生成
+ */
+class Admin extends Backend
+{
+
+    /**
+     * @var \app\admin\model\Admin
+     */
+    protected $model = null;
+    protected $selectpageFields = 'id,username,nickname,avatar';
+    protected $searchFields = 'id,username,nickname';
+    protected $childrenGroupIds = [];
+    protected $childrenAdminIds = [];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('Admin');
+
+        $this->childrenAdminIds = $this->auth->getChildrenAdminIds($this->auth->isSuperAdmin());
+        $this->childrenGroupIds = $this->auth->getChildrenGroupIds($this->auth->isSuperAdmin());
+
+        $groupList = collection(AuthGroup::where('id', 'in', $this->childrenGroupIds)->select())->toArray();
+
+        Tree::instance()->init($groupList);
+        $groupdata = [];
+        if ($this->auth->isSuperAdmin()) {
+            $result = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0));
+            foreach ($result as $k => $v) {
+                $groupdata[$v['id']] = $v['name'];
+            }
+        } else {
+            $result = [];
+            $groups = $this->auth->getGroups();
+            foreach ($groups as $m => $n) {
+                $childlist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray($n['id']));
+                $temp = [];
+                foreach ($childlist as $k => $v) {
+                    $temp[$v['id']] = $v['name'];
+                }
+                $result[__($n['name'])] = $temp;
+            }
+            $groupdata = $result;
+        }
+
+        $this->view->assign('groupdata', $groupdata);
+        $this->assignconfig("admin", ['id' => $this->auth->id]);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            $childrenGroupIds = $this->childrenGroupIds;
+            $groupName = AuthGroup::where('id', 'in', $childrenGroupIds)
+                ->column('id,name');
+            $authGroupList = AuthGroupAccess::where('group_id', 'in', $childrenGroupIds)
+                ->field('uid,group_id')
+                ->select();
+
+            $adminGroupName = [];
+            foreach ($authGroupList as $k => $v) {
+                if (isset($groupName[$v['group_id']])) {
+                    $adminGroupName[$v['uid']][$v['group_id']] = $groupName[$v['group_id']];
+                }
+            }
+            $groups = $this->auth->getGroups();
+            foreach ($groups as $m => $n) {
+                $adminGroupName[$this->auth->id][$n['id']] = $n['name'];
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+
+            $list = $this->model
+                ->where($where)
+                ->where('id', 'in', $this->childrenAdminIds)
+                ->field(['password', 'salt', 'token'], true)
+                ->order($sort, $order)
+                ->paginate($limit);
+
+            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));
+            }
+            unset($v);
+            $result = array("total" => $list->total(), "rows" => $list->items());
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a");
+            if ($params) {
+                Db::startTrans();
+                try {
+                    if (!Validate::is($params['password'], '\S{6,30}')) {
+                        exception(__("Please input correct password"));
+                    }
+                    $params['salt'] = Random::alnum();
+                    $params['password'] = md5(md5($params['password']) . $params['salt']);
+                    $params['avatar'] = '/assets/img/avatar.png'; //设置新管理员默认头像。
+                    $result = $this->model->validate('Admin.add')->save($params);
+                    if ($result === false) {
+                        exception($this->model->getError());
+                    }
+                    $group = $this->request->post("group/a");
+
+                    //过滤不允许的组别,避免越权
+                    $group = array_intersect($this->childrenGroupIds, $group);
+                    if (!$group) {
+                        exception(__('The parent group exceeds permission limit'));
+                    }
+
+                    $dataset = [];
+                    foreach ($group as $value) {
+                        $dataset[] = ['uid' => $this->model->id, 'group_id' => $value];
+                    }
+                    model('AuthGroupAccess')->saveAll($dataset);
+                    Db::commit();
+                } catch (\Exception $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                }
+                $this->success();
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get(['id' => $ids]);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        if (!in_array($row->id, $this->childrenAdminIds)) {
+            $this->error(__('You have no permission'));
+        }
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a");
+            if ($params) {
+                Db::startTrans();
+                try {
+                    if ($params['password']) {
+                        if (!Validate::is($params['password'], '\S{6,30}')) {
+                            exception(__("Please input correct password"));
+                        }
+                        $params['salt'] = Random::alnum();
+                        $params['password'] = md5(md5($params['password']) . $params['salt']);
+                    } else {
+                        unset($params['password'], $params['salt']);
+                    }
+                    //这里需要针对username和email做唯一验证
+                    $adminValidate = \think\Loader::validate('Admin');
+                    $adminValidate->rule([
+                        'username' => 'require|regex:\w{3,30}|unique:admin,username,' . $row->id,
+                        'email'    => 'require|email|unique:admin,email,' . $row->id,
+                        'password' => 'regex:\S{32}',
+                    ]);
+                    $result = $row->validate('Admin.edit')->save($params);
+                    if ($result === false) {
+                        exception($row->getError());
+                    }
+
+                    // 先移除所有权限
+                    model('AuthGroupAccess')->where('uid', $row->id)->delete();
+
+                    $group = $this->request->post("group/a");
+
+                    // 过滤不允许的组别,避免越权
+                    $group = array_intersect($this->childrenGroupIds, $group);
+                    if (!$group) {
+                        exception(__('The parent group exceeds permission limit'));
+                    }
+
+                    $dataset = [];
+                    foreach ($group as $value) {
+                        $dataset[] = ['uid' => $row->id, 'group_id' => $value];
+                    }
+                    model('AuthGroupAccess')->saveAll($dataset);
+                    Db::commit();
+                } catch (\Exception $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                }
+                $this->success();
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $grouplist = $this->auth->getGroups($row['id']);
+        $groupids = [];
+        foreach ($grouplist as $k => $v) {
+            $groupids[] = $v['id'];
+        }
+        $this->view->assign("row", $row);
+        $this->view->assign("groupids", $groupids);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        if (!$this->request->isPost()) {
+            $this->error(__("Invalid parameters"));
+        }
+        $ids = $ids ? $ids : $this->request->post("ids");
+        if ($ids) {
+            $ids = array_intersect($this->childrenAdminIds, array_filter(explode(',', $ids)));
+            // 避免越权删除管理员
+            $childrenGroupIds = $this->childrenGroupIds;
+            $adminList = $this->model->where('id', 'in', $ids)->where('id', 'in', function ($query) use ($childrenGroupIds) {
+                $query->name('auth_group_access')->where('group_id', 'in', $childrenGroupIds)->field('uid');
+            })->select();
+            if ($adminList) {
+                $deleteIds = [];
+                foreach ($adminList as $k => $v) {
+                    $deleteIds[] = $v->id;
+                }
+                $deleteIds = array_values(array_diff($deleteIds, [$this->auth->id]));
+                if ($deleteIds) {
+                    Db::startTrans();
+                    try {
+                        $this->model->destroy($deleteIds);
+                        model('AuthGroupAccess')->where('uid', 'in', $deleteIds)->delete();
+                        Db::commit();
+                    } catch (\Exception $e) {
+                        Db::rollback();
+                        $this->error($e->getMessage());
+                    }
+                    $this->success();
+                }
+                $this->error(__('No rows were deleted'));
+            }
+        }
+        $this->error(__('You have no permission'));
+    }
+
+    /**
+     * 批量更新
+     * @internal
+     */
+    public function multi($ids = "")
+    {
+        // 管理员禁止批量操作
+        $this->error();
+    }
+
+    /**
+     * 下拉搜索
+     */
+    public function selectpage()
+    {
+        $this->dataLimit = 'auth';
+        $this->dataLimitField = 'id';
+        return parent::selectpage();
+    }
+}

+ 12 - 0
application/application/admin/lang/zh-cn/auth/admin.php

@@ -0,0 +1,12 @@
+<?php
+
+return [
+    'Group'                                     => '所属组别',
+    'Loginfailure'                              => '登录失败次数',
+    'Login time'                                => '最后登录',
+    'The parent group exceeds permission limit' => '父组别超出权限范围',
+    'Please input correct username'             => '用户名只能由3-30位数字、字母、下划线组合',
+    'Username must be 3 to 30 characters'       => '用户名只能由3-30位数字、字母、下划线组合',
+    'Please input correct password'             => '密码长度必须在6-30位之间,不能包含空格',
+    'Password must be 6 to 30 characters'       => '密码长度必须在6-30位之间,不能包含空格',
+];

+ 50 - 0
application/application/admin/lang/zh-cn/dashboard.php

@@ -0,0 +1,50 @@
+<?php
+
+return [
+    'Custom'                   => '自定义',
+    'Pid'                      => '父ID',
+    'Type'                     => '栏目类型',
+    'Image'                    => '图片',
+    'Total user'               => '总会员数',
+    'Total addon'              => '总插件数',
+    'Total category'           => '总分类数',
+    'Total attachment'         => '总附件数',
+    'Total admin'              => '总管理员数',
+    'Today user signup'        => '今日注册',
+    'Today user login'         => '今日登录',
+    'Today order'              => '今日订单',
+    'Unsettle order'           => '未处理订单',
+    'Three dnu'                => '三日新增',
+    'Seven dnu'                => '七日新增',
+    'Seven dau'                => '七日活跃',
+    'Thirty dau'               => '月活跃',
+    'Custom zone'              => '这里是你的自定义数据',
+    'Register user'            => '注册用户数',
+    'Real time'                => '实时',
+    'Category count'           => '分类统计',
+    'Working addon count'      => '运行中的插件',
+    'Category count tips'      => '当前分类总记录数',
+    'Working addon count tips' => '当前运行中的插件数',
+    'Database count'           => '数据库统计',
+    'Database table nums'      => '数据表数量',
+    'Database size'            => '占用空间',
+    'Attachment count'         => '附件统计',
+    'Attachment nums'          => '附件数量',
+    'Attachment size'          => '附件大小',
+    'Attachment count tips'    => '当前上传的附件数量',
+    'Picture count'            => '图片统计',
+    'Picture nums'             => '图片数量',
+    'Picture size'             => '图片大小',
+    'Server info'              => '服务器信息',
+    'PHP version'              => 'PHP版本',
+    'Sapi name'                => '运行方式',
+    'Debug mode'               => '调试模式',
+    'Software'                 => '环境信息',
+    'Upload mode'              => '上传模式',
+    'Upload url'               => '上传URL',
+    'Upload cdn url'           => '上传CDN',
+    'Cdn url'                  => '静态资源CDN',
+    'Timezone'                 => '时区',
+    'Language'                 => '语言',
+    'View more'                => '查看更多',
+];

+ 83 - 0
application/application/admin/lang/zh-cn/general/config.php

@@ -0,0 +1,83 @@
+<?php
+
+return [
+    'Name'                                 => '变量名',
+    'Tip'                                  => '提示信息',
+    'Group'                                => '分组',
+    'Type'                                 => '类型',
+    'Title'                                => '变量标题',
+    'Value'                                => '变量值',
+    'Basic'                                => '基础配置',
+    'Email'                                => '邮件配置',
+    'Attachment'                           => '附件配置',
+    'Dictionary'                           => '字典配置',
+    'User'                                 => '会员配置',
+    'Example'                              => '示例分组',
+    'Extend'                               => '扩展属性',
+    'String'                               => '字符',
+    'Password'                             => '密码',
+    'Text'                                 => '文本',
+    'Editor'                               => '编辑器',
+    'Number'                               => '数字',
+    'Date'                                 => '日期',
+    'Time'                                 => '时间',
+    'Datetime'                             => '日期时间',
+    'Datetimerange'                        => '日期时间区间',
+    'Image'                                => '图片',
+    'Images'                               => '图片(多)',
+    'File'                                 => '文件',
+    'Files'                                => '文件(多)',
+    'Select'                               => '列表',
+    'Selects'                              => '列表(多选)',
+    'Switch'                               => '开关',
+    'Checkbox'                             => '复选',
+    'Radio'                                => '单选',
+    'Array'                                => '数组',
+    'Array key'                            => '键名',
+    'Array value'                          => '键值',
+    'City'                                 => '城市地区',
+    'Selectpage'                           => '关联表',
+    'Selectpages'                          => '关联表(多选)',
+    'Custom'                               => '自定义',
+    'Please select table'                  => '关联表',
+    'Selectpage table'                     => '关联表',
+    'Selectpage primarykey'                => '存储字段',
+    'Selectpage field'                     => '显示字段',
+    'Selectpage conditions'                => '筛选条件',
+    'Field title'                          => '字段名',
+    'Field value'                          => '字段值',
+    'Content'                              => '数据列表',
+    'Rule'                                 => '校验规则',
+    'Visible condition'                    => '可见条件',
+    'Site name'                            => '站点名称',
+    'Beian'                                => '备案号',
+    'Cdn url'                              => 'CDN地址',
+    'Version'                              => '版本号',
+    'Timezone'                             => '时区',
+    'Forbidden ip'                         => '禁止IP',
+    'Languages'                            => '语言',
+    'Fixed page'                           => '后台固定页',
+    'Category type'                        => '分类类型',
+    'Config group'                         => '配置分组',
+    'Attachment category'                  => '附件类别',
+    'Category1'                            => '分类一',
+    'Category2'                            => '分类二',
+    'Rule tips'                            => '校验规则使用请参考Nice-validator文档',
+    'Extend tips'                          => '扩展属性支持{id}、{name}、{group}、{title}、{value}、{content}、{rule}替换',
+    'Mail type'                            => '邮件发送方式',
+    'Mail smtp host'                       => 'SMTP服务器',
+    'Mail smtp port'                       => 'SMTP端口',
+    'Mail smtp user'                       => 'SMTP用户名',
+    'Mail smtp password'                   => 'SMTP密码',
+    'Mail vertify type'                    => 'SMTP验证方式',
+    'Mail from'                            => '发件人邮箱',
+    'Site name incorrect'                  => '网站名称错误',
+    'Name already exist'                   => '变量名称已经存在',
+    'Add new config'                       => '点击添加新的配置',
+    'Send a test message'                  => '发送测试邮件',
+    'Only work at development environment' => '只允许在开发环境开操作',
+    'This is a test mail content'          => '这是一封来自%s的校验邮件,用于校验邮件配置是否正常!',
+    'This is a test mail'                  => '这是一封来自%s的邮件',
+    'Please input your email'              => '请输入测试接收者邮箱',
+    'Please input correct email'           => '请输入正确的邮箱地址',
+];

+ 14 - 0
application/application/admin/lang/zh-cn/general/profile.php

@@ -0,0 +1,14 @@
+<?php
+
+return [
+    'Url'                                         => '链接',
+    'Userame'                                     => '用户名',
+    'Createtime'                                  => '操作时间',
+    'Click to edit'                               => '点击编辑',
+    'Admin log'                                   => '操作日志',
+    'Leave password blank if dont want to change' => '不修改密码请留空',
+    'Please input correct email'                  => '请输入正确的Email地址',
+    'Please input correct password'               => '密码长度必须在6-30位之间,不能包含空格',
+    'Password must be 6 to 30 characters'         => '密码长度必须在6-30位之间,不能包含空格',
+    'Email already exists'                        => '邮箱已经存在',
+];

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

@@ -0,0 +1,530 @@
+<?php
+
+namespace app\admin\library;
+
+use app\admin\model\Admin;
+use fast\Random;
+use fast\Tree;
+use think\Config;
+use think\Cookie;
+use think\Hook;
+use think\Request;
+use think\Session;
+
+class Auth extends \fast\Auth
+{
+    protected $_error = '';
+    protected $requestUri = '';
+    protected $breadcrumb = [];
+    protected $logined = false; //登录状态
+
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function __get($name)
+    {
+        return Session::get('admin.' . $name);
+    }
+
+    /**
+     * 管理员登录
+     *
+     * @param string $username 用户名
+     * @param string $password 密码
+     * @param int    $keeptime 有效时长
+     * @return  boolean
+     */
+    public function login($username, $password, $keeptime = 0)
+    {
+        $admin = Admin::get(['username' => $username]);
+        if (!$admin) {
+            $this->setError('Username is incorrect');
+            return false;
+        }
+        if ($admin['status'] == 'hidden') {
+            $this->setError('Admin is forbidden');
+            return false;
+        }
+        if (Config::get('fastadmin.login_failure_retry') && $admin->loginfailure >= 10 && time() - $admin->updatetime < 86400) {
+            $this->setError('Please try again after 1 day');
+            return false;
+        }
+        if ($admin->password != md5(md5($password) . $admin->salt)) {
+            $admin->loginfailure++;
+            $admin->save();
+            $this->setError('Password is incorrect');
+            return false;
+        }
+        $admin->loginfailure = 0;
+        $admin->logintime = time();
+        $admin->loginip = request()->ip();
+        $admin->token = Random::uuid();
+        $admin->save();
+        Session::set("admin", $admin->toArray());
+        $this->keeplogin($keeptime);
+        return true;
+    }
+
+    /**
+     * 退出登录
+     */
+    public function logout()
+    {
+        $admin = Admin::get(intval($this->id));
+        if ($admin) {
+            $admin->token = '';
+            $admin->save();
+        }
+        $this->logined = false; //重置登录状态
+        Session::delete("admin");
+        Cookie::delete("keeplogin");
+        return true;
+    }
+
+    /**
+     * 自动登录
+     * @return boolean
+     */
+    public function autologin()
+    {
+        $keeplogin = Cookie::get('keeplogin');
+        if (!$keeplogin) {
+            return false;
+        }
+        list($id, $keeptime, $expiretime, $key) = explode('|', $keeplogin);
+        if ($id && $keeptime && $expiretime && $key && $expiretime > time()) {
+            $admin = Admin::get($id);
+            if (!$admin || !$admin->token) {
+                return false;
+            }
+            //token有变更
+            if ($key != md5(md5($id) . md5($keeptime) . md5($expiretime) . $admin->token . config('token.key'))) {
+                return false;
+            }
+            $ip = request()->ip();
+            //IP有变动
+            if ($admin->loginip != $ip) {
+                return false;
+            }
+            Session::set("admin", $admin->toArray());
+            //刷新自动登录的时效
+            $this->keeplogin($keeptime);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 刷新保持登录的Cookie
+     *
+     * @param int $keeptime
+     * @return  boolean
+     */
+    protected function keeplogin($keeptime = 0)
+    {
+        if ($keeptime) {
+            $expiretime = time() + $keeptime;
+            $key = md5(md5($this->id) . md5($keeptime) . md5($expiretime) . $this->token . config('token.key'));
+            $data = [$this->id, $keeptime, $expiretime, $key];
+            Cookie::set('keeplogin', implode('|', $data), 86400 * 7);
+            return true;
+        }
+        return false;
+    }
+
+    public function check($name, $uid = '', $relation = 'or', $mode = 'url')
+    {
+        $uid = $uid ? $uid : $this->id;
+        return parent::check($name, $uid, $relation, $mode);
+    }
+
+    /**
+     * 检测当前控制器和方法是否匹配传递的数组
+     *
+     * @param array $arr 需要验证权限的数组
+     * @return bool
+     */
+    public function match($arr = [])
+    {
+        $request = Request::instance();
+        $arr = is_array($arr) ? $arr : explode(',', $arr);
+        if (!$arr) {
+            return false;
+        }
+
+        $arr = array_map('strtolower', $arr);
+        // 是否存在
+        if (in_array(strtolower($request->action()), $arr) || in_array('*', $arr)) {
+            return true;
+        }
+
+        // 没找到匹配
+        return false;
+    }
+
+    /**
+     * 检测是否登录
+     *
+     * @return boolean
+     */
+    public function isLogin()
+    {
+        if ($this->logined) {
+            return true;
+        }
+        $admin = Session::get('admin');
+        if (!$admin) {
+            return false;
+        }
+        //判断是否同一时间同一账号只能在一个地方登录
+        if (Config::get('fastadmin.login_unique')) {
+            $my = Admin::get($admin['id']);
+            if (!$my || $my['token'] != $admin['token']) {
+                $this->logined = false; //重置登录状态
+                Session::delete("admin");
+                Cookie::delete("keeplogin");
+                return false;
+            }
+        }
+        //判断管理员IP是否变动
+        if (Config::get('fastadmin.loginip_check')) {
+            if (!isset($admin['loginip']) || $admin['loginip'] != request()->ip()) {
+                $this->logout();
+                return false;
+            }
+        }
+        $this->logined = true;
+        return true;
+    }
+
+    /**
+     * 获取当前请求的URI
+     * @return string
+     */
+    public function getRequestUri()
+    {
+        return $this->requestUri;
+    }
+
+    /**
+     * 设置当前请求的URI
+     * @param string $uri
+     */
+    public function setRequestUri($uri)
+    {
+        $this->requestUri = $uri;
+    }
+
+    public function getGroups($uid = null)
+    {
+        $uid = is_null($uid) ? $this->id : $uid;
+        return parent::getGroups($uid);
+    }
+
+    public function getRuleList($uid = null)
+    {
+        $uid = is_null($uid) ? $this->id : $uid;
+        return parent::getRuleList($uid);
+    }
+
+    public function getUserInfo($uid = null)
+    {
+        $uid = is_null($uid) ? $this->id : $uid;
+
+        return $uid != $this->id ? Admin::get(intval($uid)) : Session::get('admin');
+    }
+
+    public function getRuleIds($uid = null)
+    {
+        $uid = is_null($uid) ? $this->id : $uid;
+        return parent::getRuleIds($uid);
+    }
+
+    public function isSuperAdmin()
+    {
+        return in_array('*', $this->getRuleIds()) ? true : false;
+    }
+
+    /**
+     * 获取管理员所属于的分组ID
+     * @param int $uid
+     * @return array
+     */
+    public function getGroupIds($uid = null)
+    {
+        $groups = $this->getGroups($uid);
+        $groupIds = [];
+        foreach ($groups as $K => $v) {
+            $groupIds[] = (int)$v['group_id'];
+        }
+        return $groupIds;
+    }
+
+    /**
+     * 取出当前管理员所拥有权限的分组
+     * @param boolean $withself 是否包含当前所在的分组
+     * @return array
+     */
+    public function getChildrenGroupIds($withself = false)
+    {
+        //取出当前管理员所有的分组
+        $groups = $this->getGroups();
+        $groupIds = [];
+        foreach ($groups as $k => $v) {
+            $groupIds[] = $v['id'];
+        }
+        $originGroupIds = $groupIds;
+        foreach ($groups as $k => $v) {
+            if (in_array($v['pid'], $originGroupIds)) {
+                $groupIds = array_diff($groupIds, [$v['id']]);
+                unset($groups[$k]);
+            }
+        }
+        // 取出所有分组
+        $groupList = \app\admin\model\AuthGroup::where(['status' => 'normal'])->select();
+        $objList = [];
+        foreach ($groups as $k => $v) {
+            if ($v['rules'] === '*') {
+                $objList = $groupList;
+                break;
+            }
+            // 取出包含自己的所有子节点
+            $childrenList = Tree::instance()->init($groupList, 'pid')->getChildren($v['id'], true);
+            $obj = Tree::instance()->init($childrenList, 'pid')->getTreeArray($v['pid']);
+            $objList = array_merge($objList, Tree::instance()->getTreeList($obj));
+        }
+        $childrenGroupIds = [];
+        foreach ($objList as $k => $v) {
+            $childrenGroupIds[] = $v['id'];
+        }
+        if (!$withself) {
+            $childrenGroupIds = array_diff($childrenGroupIds, $groupIds);
+        }
+        return $childrenGroupIds;
+    }
+
+    /**
+     * 取出当前管理员所拥有权限的管理员
+     * @param boolean $withself 是否包含自身
+     * @return array
+     */
+    public function getChildrenAdminIds($withself = false)
+    {
+        $childrenAdminIds = [];
+        if (!$this->isSuperAdmin()) {
+            $groupIds = $this->getChildrenGroupIds(false);
+            $authGroupList = \app\admin\model\AuthGroupAccess::
+            field('uid,group_id')
+                ->where('group_id', 'in', $groupIds)
+                ->select();
+            foreach ($authGroupList as $k => $v) {
+                $childrenAdminIds[] = $v['uid'];
+            }
+        } else {
+            //超级管理员拥有所有人的权限
+            $childrenAdminIds = Admin::column('id');
+        }
+        if ($withself) {
+            if (!in_array($this->id, $childrenAdminIds)) {
+                $childrenAdminIds[] = $this->id;
+            }
+        } else {
+            $childrenAdminIds = array_diff($childrenAdminIds, [$this->id]);
+        }
+        return $childrenAdminIds;
+    }
+
+    /**
+     * 获得面包屑导航
+     * @param string $path
+     * @return array
+     */
+    public function getBreadCrumb($path = '')
+    {
+        if ($this->breadcrumb || !$path) {
+            return $this->breadcrumb;
+        }
+        $titleArr = [];
+        $menuArr = [];
+        $urlArr = explode('/', $path);
+        foreach ($urlArr as $index => $item) {
+            $pathArr[implode('/', array_slice($urlArr, 0, $index + 1))] = $index;
+        }
+        if (!$this->rules && $this->id) {
+            $this->getRuleList();
+        }
+        foreach ($this->rules as $rule) {
+            if (isset($pathArr[$rule['name']])) {
+                $rule['title'] = __($rule['title']);
+                $rule['url'] = url($rule['name']);
+                $titleArr[$pathArr[$rule['name']]] = $rule['title'];
+                $menuArr[$pathArr[$rule['name']]] = $rule;
+            }
+
+        }
+        ksort($menuArr);
+        $this->breadcrumb = $menuArr;
+        return $this->breadcrumb;
+    }
+
+    /**
+     * 获取左侧和顶部菜单栏
+     *
+     * @param array  $params    URL对应的badge数据
+     * @param string $fixedPage 默认页
+     * @return array
+     */
+    public function getSidebar($params = [], $fixedPage = 'dashboard')
+    {
+        // 边栏开始
+        Hook::listen("admin_sidebar_begin", $params);
+        $colorArr = ['red', 'green', 'yellow', 'blue', 'teal', 'orange', 'purple'];
+        $colorNums = count($colorArr);
+        $badgeList = [];
+        $module = request()->module();
+        // 生成菜单的badge
+        foreach ($params as $k => $v) {
+            $url = $k;
+            if (is_array($v)) {
+                $nums = isset($v[0]) ? $v[0] : 0;
+                $color = isset($v[1]) ? $v[1] : $colorArr[(is_numeric($nums) ? $nums : strlen($nums)) % $colorNums];
+                $class = isset($v[2]) ? $v[2] : 'label';
+            } else {
+                $nums = $v;
+                $color = $colorArr[(is_numeric($nums) ? $nums : strlen($nums)) % $colorNums];
+                $class = 'label';
+            }
+            //必须nums大于0才显示
+            if ($nums) {
+                $badgeList[$url] = '<small class="' . $class . ' pull-right bg-' . $color . '">' . $nums . '</small>';
+            }
+        }
+
+        // 读取管理员当前拥有的权限节点
+        $userRule = $this->getRuleList();
+        $selected = $referer = [];
+        $refererUrl = Session::get('referer');
+        // 必须将结果集转换为数组
+        $ruleList = collection(\app\admin\model\AuthRule::where('status', 'normal')
+            ->where('ismenu', 1)
+            ->order('weigh', 'desc')
+            ->cache("__menu__")
+            ->select())->toArray();
+        $indexRuleList = \app\admin\model\AuthRule::where('status', 'normal')
+            ->where('ismenu', 0)
+            ->where('name', 'like', '%/index')
+            ->column('name,pid');
+        $pidArr = array_unique(array_filter(array_column($ruleList, 'pid')));
+        foreach ($ruleList as $k => &$v) {
+            if (!in_array($v['name'], $userRule)) {
+                unset($ruleList[$k]);
+                continue;
+            }
+            $indexRuleName = $v['name'] . '/index';
+            if (isset($indexRuleList[$indexRuleName]) && !in_array($indexRuleName, $userRule)) {
+                unset($ruleList[$k]);
+                continue;
+            }
+            $v['icon'] = $v['icon'] . ' fa-fw';
+            $v['url'] = isset($v['url']) && $v['url'] ? $v['url'] : '/' . $module . '/' . $v['name'];
+            $v['badge'] = isset($badgeList[$v['name']]) ? $badgeList[$v['name']] : '';
+            $v['title'] = __($v['title']);
+            $v['url'] = preg_match("/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i", $v['url']) ? $v['url'] : url($v['url']);
+            $v['menuclass'] = in_array($v['menutype'], ['dialog', 'ajax']) ? 'btn-' . $v['menutype'] : '';
+            $v['menutabs'] = !$v['menutype'] || in_array($v['menutype'], ['default', 'addtabs']) ? 'addtabs="' . $v['id'] . '"' : '';
+            $selected = $v['name'] == $fixedPage ? $v : $selected;
+            $referer = $v['url'] == $refererUrl ? $v : $referer;
+        }
+        $lastArr = array_unique(array_filter(array_column($ruleList, 'pid')));
+        $pidDiffArr = array_diff($pidArr, $lastArr);
+        foreach ($ruleList as $index => $item) {
+            if (in_array($item['id'], $pidDiffArr)) {
+                unset($ruleList[$index]);
+            }
+        }
+        if ($selected == $referer) {
+            $referer = [];
+        }
+
+        $select_id = $referer ? $referer['id'] : ($selected ? $selected['id'] : 0);
+        $menu = $nav = '';
+        $showSubmenu = config('fastadmin.show_submenu');
+        if (Config::get('fastadmin.multiplenav')) {
+            $topList = [];
+            foreach ($ruleList as $index => $item) {
+                if (!$item['pid']) {
+                    $topList[] = $item;
+                }
+            }
+            $selectParentIds = [];
+            $tree = Tree::instance();
+            $tree->init($ruleList);
+            if ($select_id) {
+                $selectParentIds = $tree->getParentsIds($select_id, true);
+            }
+            foreach ($topList as $index => $item) {
+                $childList = Tree::instance()->getTreeMenu(
+                    $item['id'],
+                    '<li class="@class" pid="@pid"><a @extend href="@url@addtabs" addtabs="@id" class="@menuclass" url="@url" py="@py" pinyin="@pinyin"><i class="@icon"></i> <span>@title</span> <span class="pull-right-container">@caret @badge</span></a> @childlist</li>',
+                    $select_id,
+                    '',
+                    'ul',
+                    'class="treeview-menu' . ($showSubmenu ? ' menu-open' : '') . '"'
+                );
+                $current = in_array($item['id'], $selectParentIds);
+                $url = $childList ? 'javascript:;' : $item['url'];
+                $addtabs = $childList || !$url ? "" : (stripos($url, "?") !== false ? "&" : "?") . "ref=addtabs";
+                $childList = str_replace(
+                    '" pid="' . $item['id'] . '"',
+                    ' ' . ($current ? '' : 'hidden') . '" pid="' . $item['id'] . '"',
+                    $childList
+                );
+                $nav .= '<li class="' . ($current ? 'active' : '') . '"><a ' . $item['extend'] . ' href="' . $url . $addtabs . '" ' . $item['menutabs'] . ' class="' . $item['menuclass'] . '" url="' . $url . '" title="' . $item['title'] . '"><i class="' . $item['icon'] . '"></i> <span>' . $item['title'] . '</span> <span class="pull-right-container"> </span></a> </li>';
+                $menu .= $childList;
+            }
+        } else {
+            // 构造菜单数据
+            Tree::instance()->init($ruleList);
+            $menu = Tree::instance()->getTreeMenu(
+                0,
+                '<li class="@class"><a @extend href="@url@addtabs" @menutabs class="@menuclass" url="@url" py="@py" pinyin="@pinyin"><i class="@icon"></i> <span>@title</span> <span class="pull-right-container">@caret @badge</span></a> @childlist</li>',
+                $select_id,
+                '',
+                'ul',
+                'class="treeview-menu' . ($showSubmenu ? ' menu-open' : '') . '"'
+            );
+            if ($selected) {
+                $nav .= '<li role="presentation" id="tab_' . $selected['id'] . '" class="' . ($referer ? '' : 'active') . '"><a href="#con_' . $selected['id'] . '" node-id="' . $selected['id'] . '" aria-controls="' . $selected['id'] . '" role="tab" data-toggle="tab"><i class="' . $selected['icon'] . ' fa-fw"></i> <span>' . $selected['title'] . '</span> </a></li>';
+            }
+            if ($referer) {
+                $nav .= '<li role="presentation" id="tab_' . $referer['id'] . '" class="active"><a href="#con_' . $referer['id'] . '" node-id="' . $referer['id'] . '" aria-controls="' . $referer['id'] . '" role="tab" data-toggle="tab"><i class="' . $referer['icon'] . ' fa-fw"></i> <span>' . $referer['title'] . '</span> </a> <i class="close-tab fa fa-remove"></i></li>';
+            }
+        }
+
+        return [$menu, $nav, $selected, $referer];
+    }
+
+    /**
+     * 设置错误信息
+     *
+     * @param string $error 错误信息
+     * @return Auth
+     */
+    public function setError($error)
+    {
+        $this->_error = $error;
+        return $this;
+    }
+
+    /**
+     * 获取错误信息
+     * @return string
+     */
+    public function getError()
+    {
+        return $this->_error ? __($this->_error) : '';
+    }
+}

+ 55 - 0
application/application/admin/validate/Admin.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace app\admin\validate;
+
+use think\Validate;
+
+class Admin extends Validate
+{
+
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+        'username' => 'require|regex:\w{3,30}|unique:admin',
+        'nickname' => 'require',
+        'password' => 'require|regex:\S{32}',
+        'email'    => 'require|email|unique:admin,email',
+    ];
+
+    /**
+     * 提示消息
+     */
+    protected $message = [
+    ];
+
+    /**
+     * 字段描述
+     */
+    protected $field = [
+    ];
+
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'  => ['username', 'email', 'nickname', 'password'],
+        'edit' => ['username', 'email', 'nickname', 'password'],
+    ];
+
+    public function __construct(array $rules = [], $message = [], $field = [])
+    {
+        $this->field = [
+            'username' => __('Username'),
+            'nickname' => __('Nickname'),
+            'password' => __('Password'),
+            'email'    => __('Email'),
+        ];
+        $this->message = array_merge($this->message, [
+            'username.regex' => __('Please input correct username'),
+            'password.regex' => __('Please input correct password')
+        ]);
+        parent::__construct($rules, $message, $field);
+    }
+
+}

+ 134 - 0
application/application/admin/view/addon/config.html

@@ -0,0 +1,134 @@
+<form id="config-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+    {if $addon.tips && $addon.tips.value}
+    <div class="alert {$addon.tips.extend|default='alert-info-light'}" style="margin-bottom:10px;">
+        {if $addon.tips.title}
+        <b>{$addon.tips.title}</b><br>
+        {/if}
+        {$addon.tips.value}
+    </div>
+    {/if}
+
+    <div class="panel panel-default panel-intro">
+        {if count($groupList)>1}
+        <div class="panel-heading mb-3">
+            <ul class="nav nav-tabs nav-group">
+                <li class="active"><a href="#all" data-toggle="tab">全部</a></li>
+                {foreach name="groupList" id="tab"}
+                    <li><a href="#tab-{$key}" title="{$tab}" data-toggle="tab">{$tab}</a></li>
+                {/foreach}
+            </ul>
+        </div>
+        {/if}
+
+        <div class="panel-body no-padding">
+            <div id="myTabContent" class="tab-content">
+                {foreach name="groupList" id="group" key="groupName"}
+                <div class="tab-pane fade active in" id="tab-{$groupName}">
+
+                    <table class="table table-striped table-config mb-0">
+                        <tbody>
+                        {foreach name="$addon.config" id="item"}
+                        {if ((!isset($item['group']) || $item['group']=='') && $groupName=='other') || (isset($item['group']) && $item['group']==$group)}
+                        <tr data-favisible="{$item.visible|default=''|htmlentities}" data-name="{$item.name}" class="{if $item.visible??''}hidden{/if}">
+                            <td width="15%">{$item.title}</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        {switch $item.type}
+                                        {case string}
+                                        <input {$item.extend} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-rule="{$item.rule}" data-tip="{$item.tip}"/>
+                                        {/case}
+                                        {case password}
+                                        <input {$item.extend} type="password" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-rule="{$item.rule}" data-tip="{$item.tip}"/>
+                                        {/case}
+                                        {case text}
+                                        <textarea {$item.extend} name="row[{$item.name}]" class="form-control" data-rule="{$item.rule}" rows="5" data-tip="{$item.tip}">{$item.value|htmlentities}</textarea>
+                                        {/case}
+                                        {case array}
+                                        <dl class="fieldlist" data-name="row[{$item.name}]">
+                                            <dd>
+                                                <ins>{:__('Array key')}</ins>
+                                                <ins>{:__('Array value')}</ins>
+                                            </dd>
+                                            <dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                                            <textarea name="row[{$item.name}]" cols="30" rows="5" class="hide">{$item.value|json_encode|htmlentities}</textarea>
+                                        </dl>
+                                        {/case}
+                                        {case date}
+                                        <input {$item.extend} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="YYYY-MM-DD" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                        {/case}
+                                        {case time}
+                                        <input {$item.extend} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="HH:mm:ss" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                        {/case}
+                                        {case datetime}
+                                        <input {$item.extend} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                        {/case}
+                                        {case number}
+                                        <input {$item.extend} type="number" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                        {/case}
+                                        {case checkbox}
+                                        {foreach name="item.content" item="vo"}
+                                        <label for="row[{$item.name}][]-{$key}"><input id="row[{$item.name}][]-{$key}" name="row[{$item.name}][]" type="checkbox" value="{$key}" data-tip="{$item.tip}" {in name="key" value="$item.value" }checked{/in} /> {$vo}</label>
+                                        {/foreach}
+                                        <span class="msg-box n-right" for="c-{$item.name}"></span>
+                                        {/case}
+                                        {case radio}
+                                        {foreach name="item.content" item="vo"}
+                                        <label for="row[{$item.name}]-{$key}"><input id="row[{$item.name}]-{$key}" name="row[{$item.name}]" type="radio" value="{$key}" data-tip="{$item.tip}" {in name="key" value="$item.value" }checked{/in} /> {$vo}</label>
+                                        {/foreach}
+                                        <span class="msg-box n-right" for="c-{$item.name}"></span>
+                                        {/case}
+                                        {case value="select" break="0"}{/case}
+                                        {case value="selects"}
+                                        <select {$item.extend} name="row[{$item.name}]{$item.type=='selects'?'[]':''}" class="form-control selectpicker" data-tip="{$item.tip}" {$item.type=='selects'?'multiple':''}>
+                                            {foreach name="item.content" item="vo"}
+                                            <option value="{$key}" {in name="key" value="$item.value" }selected{/in}>{$vo}</option>
+                                            {/foreach}
+                                        </select>
+                                        {/case}
+                                        {case value="image" break="0"}{/case}
+                                        {case value="images"}
+                                        <div class="form-inline">
+                                            <input id="c-{$item.name}" class="form-control" size="35" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
+                                            <span><button type="button" id="plupload-{$item.name}" class="btn btn-danger plupload" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}" data-preview-id="p-{$item.name}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                            <ul class="row list-inline plupload-preview" id="p-{$item.name}"></ul>
+                                        </div>
+                                        {/case}
+                                        {case value="file" break="0"}{/case}
+                                        {case value="files"}
+                                        <div class="form-inline">
+                                            <input id="c-{$item.name}" class="form-control" size="35" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
+                                            <span><button type="button" id="plupload-{$item.name}" class="btn btn-danger plupload" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                        </div>
+                                        {/case}
+                                        {case bool}
+                                        <label for="row[{$item.name}]-yes"><input id="row[{$item.name}]-yes" name="row[{$item.name}]" type="radio" value="1" {$item.value?'checked':''} data-tip="{$item.tip}" /> {:__('Yes')}</label>
+                                        <label for="row[{$item.name}]-no"><input id="row[{$item.name}]-no" name="row[{$item.name}]" type="radio" value="0" {$item.value?'':'checked'} data-tip="{$item.tip}" /> {:__('No')}</label>
+                                        {/case}
+                                        {default /}{$item.value}
+                                        {/switch}
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+
+                            </td>
+                        </tr>
+                        {/if}
+                        {/foreach}
+                        </tbody>
+                    </table>
+                </div>
+                {/foreach}
+                <div class="form-group layer-footer">
+                    <label class="control-label col-xs-12 col-sm-2" style="width:15%;"></label>
+                    <div class="col-xs-12 col-sm-8">
+                        <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+                        <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</form>

+ 21 - 0
application/application/admin/view/auth/admin/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,add,delete')}
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit="{:$auth->check('auth/admin/edit')}"
+                           data-operate-del="{:$auth->check('auth/admin/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 21 - 0
application/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 table-nowrap"
+                           data-operate-detail="{:$auth->check('auth/adminlog/index')}"
+                           data-operate-del="{:$auth->check('auth/adminlog/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 21 - 0
application/application/admin/view/auth/group/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,add,delete')}
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit="{:$auth->check('auth/group/edit')}"
+                           data-operate-del="{:$auth->check('auth/group/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 36 - 0
application/application/admin/view/category/index.html

@@ -0,0 +1,36 @@
+<div class="panel panel-default panel-intro">
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs">
+            <li class="active"><a href="#all" data-toggle="tab">{:__('All')}</a></li>
+            {foreach name="typeList" item="vo"}
+                <li><a href="#{$key}" data-toggle="tab">{$vo}</a></li>
+            {/foreach}
+        </ul>
+
+    </div>
+    <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,add,edit,del')}
+                        <div class="dropdown btn-group {:$auth->check('category/multi')?'':'hide'}">
+                            <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">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <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>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit="{:$auth->check('category/edit')}"
+                           data-operate-del="{:$auth->check('category/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 39 - 0
application/application/admin/view/common/menu.html

@@ -0,0 +1,39 @@
+<!-- 左侧菜单栏 -->
+<section class="sidebar">
+    <!-- 管理员信息 -->
+    <div class="user-panel hidden-xs">
+        <div class="pull-left image">
+            <a href="general/profile" class="addtabsit"><img src="{$admin.avatar|cdnurl|htmlentities}" class="img-circle" /></a>
+        </div>
+        <div class="pull-left info">
+            <p>{$admin.nickname|htmlentities}</p>
+            <i class="fa fa-circle text-success"></i> {:__('Online')}
+        </div>
+    </div>
+
+    <!-- 菜单搜索 -->
+    <form action="" method="get" class="sidebar-form" onsubmit="return false;">
+        <div class="input-group">
+            <input type="text" name="q" class="form-control" placeholder="{:__('Search menu')}">
+            <span class="input-group-btn">
+                <button type="submit" name="search" id="search-btn" class="btn btn-flat"><i class="fa fa-search"></i>
+                </button>
+            </span>
+            <div class="menuresult list-group sidebar-form hide">
+            </div>
+        </div>
+    </form>
+
+    <!-- 移动端一级菜单 -->
+    <div class="mobilenav visible-xs">
+
+    </div>
+
+    <!-- 左侧菜单栏 -->
+    <ul class="sidebar-menu {if $Think.config.fastadmin.show_submenu}show-submenu{/if}">
+
+        <!-- 菜单可以在 后台管理->权限管理->菜单规则 中进行增删改排序 -->
+        {$menulist}
+
+    </ul>
+</section>

+ 403 - 0
application/application/admin/view/dashboard/index.html

@@ -0,0 +1,403 @@
+<style type="text/css">
+    .sm-st {
+        background: #fff;
+        padding: 20px;
+        -webkit-border-radius: 3px;
+        -moz-border-radius: 3px;
+        border-radius: 3px;
+        margin-bottom: 20px;
+    }
+
+    .sm-st-icon {
+        width: 60px;
+        height: 60px;
+        display: inline-block;
+        line-height: 60px;
+        text-align: center;
+        font-size: 30px;
+        background: #eee;
+        -webkit-border-radius: 5px;
+        -moz-border-radius: 5px;
+        border-radius: 5px;
+        float: left;
+        margin-right: 10px;
+        color: #fff;
+    }
+
+    .sm-st-info {
+        padding-top: 2px;
+    }
+
+    .sm-st-info span {
+        display: block;
+        font-size: 24px;
+        font-weight: 600;
+    }
+
+    .orange {
+        background: #fa8564 !important;
+    }
+
+    .tar {
+        background: #45cf95 !important;
+    }
+
+    .sm-st .green {
+        background: #86ba41 !important;
+    }
+
+    .pink {
+        background: #AC75F0 !important;
+    }
+
+    .yellow-b {
+        background: #fdd752 !important;
+    }
+
+    .stat-elem {
+
+        background-color: #fff;
+        padding: 18px;
+        border-radius: 40px;
+
+    }
+
+    .stat-info {
+        text-align: center;
+        background-color: #fff;
+        border-radius: 5px;
+        margin-top: -5px;
+        padding: 8px;
+        -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.05);
+        box-shadow: 0 1px 0px rgba(0, 0, 0, 0.05);
+        font-style: italic;
+    }
+
+    .stat-icon {
+        text-align: center;
+        margin-bottom: 5px;
+    }
+
+    .st-red {
+        background-color: #F05050;
+    }
+
+    .st-green {
+        background-color: #27C24C;
+    }
+
+    .st-violet {
+        background-color: #7266ba;
+    }
+
+    .st-blue {
+        background-color: #23b7e5;
+    }
+
+    .stats .stat-icon {
+        color: #28bb9c;
+        display: inline-block;
+        font-size: 26px;
+        text-align: center;
+        vertical-align: middle;
+        width: 50px;
+        float: left;
+    }
+
+    .stat {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: inline-block;
+    }
+
+    .stat .value {
+        font-size: 20px;
+        line-height: 24px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        font-weight: 500;
+    }
+
+    .stat .name {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        margin: 5px 0;
+    }
+
+    .stat.lg .value {
+        font-size: 26px;
+        line-height: 28px;
+    }
+
+    .stat-col {
+        margin:0 0 10px 0;
+    }
+    .stat.lg .name {
+        font-size: 16px;
+    }
+
+    .stat-col .progress {
+        height: 2px;
+    }
+
+    .stat-col .progress-bar {
+        line-height: 2px;
+        height: 2px;
+    }
+
+    .item {
+        padding: 30px 0;
+    }
+
+
+    #statistics .panel {
+        min-height: 150px;
+    }
+
+    #statistics .panel h5 {
+        font-size: 14px;
+    }
+</style>
+<div class="panel panel-default panel-intro">
+    <div class="panel-heading">
+        {:build_heading(null, false)}
+        <ul class="nav nav-tabs">
+            <li class="active"><a href="#one" data-toggle="tab">{:__('Dashboard')}</a></li>
+            <li><a href="#two" data-toggle="tab">{:__('Custom')}</a></li>
+        </ul>
+    </div>
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+
+                <div class="row">
+                    <div class="col-sm-3 col-xs-6">
+                        <div class="sm-st clearfix">
+                            <span class="sm-st-icon st-red"><i class="fa fa-users"></i></span>
+                            <div class="sm-st-info">
+                                <span>{$totaluser}</span>
+                                {:__('Total user')}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-sm-3 col-xs-6">
+                        <div class="sm-st clearfix">
+                            <span class="sm-st-icon st-violet"><i class="fa fa-magic"></i></span>
+                            <div class="sm-st-info">
+                                <span>{$totaladdon}</span>
+                                {:__('Total addon')}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-sm-3 col-xs-6">
+                        <div class="sm-st clearfix">
+                            <span class="sm-st-icon st-blue"><i class="fa fa-leaf"></i></span>
+                            <div class="sm-st-info">
+                                <span>{$attachmentnums}</span>
+                                {:__('Total attachment')}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-sm-3 col-xs-6">
+                        <div class="sm-st clearfix">
+                            <span class="sm-st-icon st-green"><i class="fa fa-user"></i></span>
+                            <div class="sm-st-info">
+                                <span>{$totaladmin}</span>
+                                {:__('Total admin')}
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="row">
+                    <div class="col-lg-8">
+                        <div id="echart" class="btn-refresh" style="height:300px;width:100%;"></div>
+                    </div>
+                    <div class="col-lg-4">
+                        <div class="card sameheight-item stats">
+                            <div class="card-block">
+                                <div class="row row-sm stats-container">
+                                    <div class="col-xs-6 stat-col">
+                                        <div class="stat-icon"><i class="fa fa-rocket"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$todayusersignup}</div>
+                                            <div class="name"> {:__('Today user signup')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6 stat-col">
+                                        <div class="stat-icon"><i class="fa fa-vcard"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$todayuserlogin}</div>
+                                            <div class="name"> {:__('Today user login')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6  stat-col">
+                                        <div class="stat-icon"><i class="fa fa-calendar"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$threednu}</div>
+                                            <div class="name"> {:__('Three dnu')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6 stat-col">
+                                        <div class="stat-icon"><i class="fa fa-calendar-plus-o"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$sevendnu}</div>
+                                            <div class="name"> {:__('Seven dnu')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6  stat-col">
+                                        <div class="stat-icon"><i class="fa fa-user-circle"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$sevendau}</div>
+                                            <div class="name"> {:__('Seven dau')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6  stat-col">
+                                        <div class="stat-icon"><i class="fa fa-user-circle-o"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$thirtydau}</div>
+                                            <div class="name"> {:__('Thirty dau')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="row" style="margin-top:15px;" id="statistics">
+
+                    <div class="col-lg-12">
+                    </div>
+                    <div class="col-xs-6 col-md-3">
+                        <div class="panel bg-blue-gradient no-border">
+                            <div class="panel-body">
+                                <div class="panel-title">
+                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
+                                    <h5>{:__('Working addon count')}</h5>
+                                </div>
+                                <div class="panel-content">
+                                    <div class="row">
+                                        <div class="col-md-12">
+                                            <h1 class="no-margins">{$totalworkingaddon}</h1>
+                                            <div class="font-bold"><i class="fa fa-magic"></i>
+                                                <small>{:__('Working addon count tips')}</small>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-xs-6 col-md-3">
+                        <div class="panel bg-teal-gradient no-border">
+                            <div class="panel-body">
+                                <div class="ibox-title">
+                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
+                                    <h5>{:__('Database count')}</h5>
+                                </div>
+                                <div class="ibox-content">
+                                    <div class="row">
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$dbtablenums}</h1>
+                                            <div class="font-bold"><i class="fa fa-database"></i>
+                                                <small>{:__('Database table nums')}</small>
+                                            </div>
+                                        </div>
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$dbsize|format_bytes=###,'',0}</h1>
+                                            <div class="font-bold"><i class="fa fa-filter"></i>
+                                                <small>{:__('Database size')}</small>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="col-xs-6 col-md-3">
+                        <div class="panel bg-purple-gradient no-border">
+                            <div class="panel-body">
+                                <div class="ibox-title">
+                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
+                                    <h5>{:__('Attachment count')}</h5>
+                                </div>
+                                <div class="ibox-content">
+
+                                    <div class="row">
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$attachmentnums}</h1>
+                                            <div class="font-bold"><i class="fa fa-files-o"></i>
+                                                <small>{:__('Attachment nums')}</small>
+                                            </div>
+                                        </div>
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$attachmentsize|format_bytes=###,'',0}</h1>
+                                            <div class="font-bold"><i class="fa fa-filter"></i>
+                                                <small>{:__('Attachment size')}</small>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-xs-6 col-md-3">
+                        <div class="panel bg-green-gradient no-border">
+                            <div class="panel-body">
+                                <div class="ibox-title">
+                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
+                                    <h5>{:__('Picture count')}</h5>
+                                </div>
+                                <div class="ibox-content">
+
+                                    <div class="row">
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$picturenums}</h1>
+                                            <div class="font-bold"><i class="fa fa-picture-o"></i>
+                                                <small>{:__('Picture nums')}</small>
+                                            </div>
+                                        </div>
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$picturesize|format_bytes=###,'',0}</h1>
+                                            <div class="font-bold"><i class="fa fa-filter"></i>
+                                                <small>{:__('Picture size')}</small>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="tab-pane fade" id="two">
+                <div class="row">
+                    <div class="col-xs-12">
+                        {:__('Custom zone')}
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 47 - 0
application/application/admin/view/general/attachment/select.html

@@ -0,0 +1,47 @@
+<style>
+    #one .commonsearch-table{
+        padding-top:15px!important;
+    }
+</style>
+<div class="panel panel-default panel-intro">
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs" data-field="category">
+            <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
+            {foreach name="categoryList" item="vo"}
+            <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
+            {/foreach}
+            {if stripos(request()->get('mimetype'),'image/')===false}
+            <li class="pull-right dropdown filter-type">
+                <a href="javascript:" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-filter"></i> {:__('Filter Type')}</a>
+                <ul class="dropdown-menu text-left" role="menu">
+                    <li class="active"><a href="javascript:" data-value="">{:__('All')}</a></li>
+                    {foreach name="mimetypeList" id="item"}
+                    <li><a href="javascript:" data-value="{$key}">{$item}</a></li>
+                    {/foreach}
+                </ul>
+            </li>
+            {/if}
+        </ul>
+    </div>
+
+    <div class="panel-body no-padding">
+        <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')}
+                        <span><button type="button" id="faupload-image" class="btn btn-success faupload" data-mimetype="{$mimetype|default=''|htmlentities}" data-multiple="true"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                        {if request()->get('multiple') == 'true'}
+                        <a class="btn btn-danger btn-choose-multi"><i class="fa fa-check"></i> {:__('Choose')}</a>
+                        {/if}
+                    </div>
+                    <table id="table" class="table table-bordered table-hover table-nowrap" width="100%">
+
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 356 - 0
application/application/admin/view/general/config/index.html

@@ -0,0 +1,356 @@
+<style type="text/css">
+    @media (max-width: 375px) {
+        .edit-form tr td input {
+            width: 100%;
+        }
+
+        .edit-form tr th:first-child, .edit-form tr td:first-child {
+            width: 20%;
+        }
+
+        .edit-form tr th:nth-last-of-type(-n+2), .edit-form tr td:nth-last-of-type(-n+2) {
+            display: none;
+        }
+    }
+
+    .edit-form table > tbody > tr td a.btn-delcfg {
+        visibility: hidden;
+    }
+
+    .edit-form table > tbody > tr:hover td a.btn-delcfg {
+        visibility: visible;
+    }
+
+    @media (max-width: 767px) {
+        .edit-form table tr th:nth-last-child(-n + 2), .edit-form table tr td:nth-last-child(-n + 2) {
+            display: none;
+        }
+
+        .edit-form table tr td .msg-box {
+            display: none;
+        }
+    }
+</style>
+<div class="panel panel-default panel-intro">
+    <div class="panel-heading">
+        {:build_heading(null, false)}
+        <ul class="nav nav-tabs">
+            {foreach $siteList as $index=>$vo}
+            <li class="{$vo.active?'active':''}"><a href="#tab-{$vo.name}" data-toggle="tab">{:__($vo.title)}</a></li>
+            {/foreach}
+            {if $Think.config.app_debug}
+            <li data-toggle="tooltip" title="{:__('Add new config')}">
+                <a href="#addcfg" data-toggle="tab"><i class="fa fa-plus"></i></a>
+            </li>
+            {/if}
+        </ul>
+    </div>
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <!--@formatter:off-->
+            {foreach $siteList as $index=>$vo}
+            <div class="tab-pane fade {$vo.active ? 'active in' : ''}" id="tab-{$vo.name}">
+                <div class="widget-body no-padding">
+                    <form id="{$vo.name}-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST" action="{:url('general.config/edit')}">
+                        {:token()}
+                        <table class="table table-striped">
+                            <thead>
+                            <tr>
+                                <th width="15%">{:__('Title')}</th>
+                                <th width="68%">{:__('Value')}</th>
+                                {if $Think.config.app_debug}
+                                <th width="15%">{:__('Name')}</th>
+                                <th width="2%"></th>
+                                {/if}
+                            </tr>
+                            </thead>
+                            <tbody>
+                            {foreach $vo.list as $item}
+                            <tr data-favisible="{$item.visible|default=''|htmlentities}" data-name="{$item.name}" class="{if $item.visible??''}hidden{/if}">
+                                <td>{$item.title}</td>
+                                <td>
+                                    <div class="row">
+                                        <div class="col-sm-8 col-xs-12">
+                                            {switch $item.type}
+                                            {case string}
+                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-rule="{$item.rule}" data-tip="{$item.tip}"/>
+                                            {/case}
+                                            {case password}
+                                            <input {$item.extend_html} type="password" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-rule="{$item.rule}" data-tip="{$item.tip}"/>
+                                            {/case}
+                                            {case text}
+                                            <textarea {$item.extend_html} name="row[{$item.name}]" class="form-control" data-rule="{$item.rule}" rows="5" data-tip="{$item.tip}">{$item.value|htmlentities}</textarea>
+                                            {/case}
+                                            {case editor}
+                                            <textarea {$item.extend_html} name="row[{$item.name}]" id="editor-{$item.name}" class="form-control editor" data-rule="{$item.rule}" rows="5" data-tip="{$item.tip}">{$item.value|htmlentities}</textarea>
+                                            {/case}
+                                            {case array}
+                                            <dl {$item.extend_html} class="fieldlist" data-name="row[{$item.name}]">
+                                                <dd>
+                                                    <ins>{:isset($item["setting"]["key"])&&$item["setting"]["key"]?$item["setting"]["key"]:__('Array key')}</ins>
+                                                    <ins>{:isset($item["setting"]["value"])&&$item["setting"]["value"]?$item["setting"]["value"]:__('Array value')}</ins>
+                                                </dd>
+                                                <dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                                                <textarea name="row[{$item.name}]" class="form-control hide" cols="30" rows="5">{$item.value|htmlentities}</textarea>
+                                            </dl>
+                                            {/case}
+                                            {case date}
+                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="YYYY-MM-DD" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                            {/case}
+                                            {case time}
+                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="HH:mm:ss" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                            {/case}
+                                            {case datetime}
+                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                            {/case}
+                                            {case datetimerange}
+                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimerange" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                            {/case}
+                                            {case number}
+                                            <input {$item.extend_html} type="number" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                            {/case}
+                                            {case checkbox}
+                                            <div class="checkbox">
+                                            {foreach name="item.content" item="vo"}
+                                            <label for="row[{$item.name}][]-{$key}"><input id="row[{$item.name}][]-{$key}" name="row[{$item.name}][]" type="checkbox" value="{$key}" data-tip="{$item.tip}" {in name="key" value="$item.value" }checked{/in} /> {$vo}</label>
+                                            {/foreach}
+                                            </div>
+                                            {/case}
+                                            {case radio}
+                                            <div class="radio">
+                                            {foreach name="item.content" item="vo"}
+                                            <label for="row[{$item.name}]-{$key}"><input id="row[{$item.name}]-{$key}" name="row[{$item.name}]" type="radio" value="{$key}" data-tip="{$item.tip}" {in name="key" value="$item.value" }checked{/in} /> {$vo}</label>
+                                            {/foreach}
+                                            </div>
+                                            {/case}
+                                            {case value="select" break="0"}{/case}
+                                            {case value="selects"}
+                                            <select {$item.extend_html} name="row[{$item.name}]{$item.type=='selects'?'[]':''}" class="form-control selectpicker" data-tip="{$item.tip}" {$item.type=='selects'?'multiple':''}>
+                                                {foreach name="item.content" item="vo"}
+                                                <option value="{$key}" {in name="key" value="$item.value" }selected{/in}>{$vo}</option>
+                                                {/foreach}
+                                            </select>
+                                            {/case}
+                                            {case value="image" break="0"}{/case}
+                                            {case value="images"}
+                                            <div class="form-inline">
+                                                <input id="c-{$item.name}" class="form-control" size="50" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
+                                                <span><button type="button" id="faupload-{$item.name}" class="btn btn-danger faupload" data-input-id="c-{$item.name}" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp" data-multiple="{$item.type=='image'?'false':'true'}" data-preview-id="p-{$item.name}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                                <span class="msg-box n-right" for="c-{$item.name}"></span>
+                                                <ul class="row list-inline faupload-preview" id="p-{$item.name}"></ul>
+                                            </div>
+                                            {/case}
+                                            {case value="file" break="0"}{/case}
+                                            {case value="files"}
+                                            <div class="form-inline">
+                                                <input id="c-{$item.name}" class="form-control" size="50" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
+                                                <span><button type="button" id="faupload-{$item.name}" class="btn btn-danger faupload" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                                <span class="msg-box n-right" for="c-{$item.name}"></span>
+                                            </div>
+                                            {/case}
+                                            {case switch}
+                                            <input id="c-{$item.name}" name="row[{$item.name}]" type="hidden" value="{:$item.value?1:0}">
+                                            <a href="javascript:;" data-toggle="switcher" class="btn-switcher" data-input-id="c-{$item.name}" data-yes="1" data-no="0">
+                                                <i class="fa fa-toggle-on text-success {if !$item.value}fa-flip-horizontal text-gray{/if} fa-2x"></i>
+                                            </a>
+                                            {/case}
+                                            {case bool}
+                                            <label for="row[{$item.name}]-yes"><input id="row[{$item.name}]-yes" name="row[{$item.name}]" type="radio" value="1" {$item.value?'checked':''} data-tip="{$item.tip}" /> {:__('Yes')}</label>
+                                            <label for="row[{$item.name}]-no"><input id="row[{$item.name}]-no" name="row[{$item.name}]" type="radio" value="0" {$item.value?'':'checked'} data-tip="{$item.tip}" /> {:__('No')}</label>
+                                            {/case}
+                                            {case city}
+                                            <div style="position:relative">
+                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" id="c-{$item.name}" value="{$item.value|htmlentities}" class="form-control" data-toggle="city-picker" data-tip="{$item.tip}" data-rule="{$item.rule}" />
+                                            </div>
+                                            {/case}
+                                            {case value="selectpage" break="0"}{/case}
+                                            {case value="selectpages"}
+                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" id="c-{$item.name}" value="{$item.value|htmlentities}" class="form-control selectpage" data-source="{:url('general.config/selectpage')}?id={$item.id}" data-primary-key="{$item.setting.primarykey}" data-field="{$item.setting.field}" data-multiple="{$item.type=='selectpage'?'false':'true'}" data-tip="{$item.tip}" data-rule="{$item.rule}" />
+                                            {/case}
+                                            {case custom}
+                                            {$item.extend_html}
+                                            {/case}
+                                            {/switch}
+                                        </div>
+                                        <div class="col-sm-4"></div>
+                                    </div>
+
+                                </td>
+                                {if $Think.config.app_debug}
+                                <td>{php}echo "{\$site.". $item['name'] . "}";{/php}</td>
+                                <td>{if $item['id']>18}<a href="javascript:;" class="btn-delcfg text-muted" data-name="{$item.name}"><i class="fa fa-times"></i></a>{/if}</td>
+                                {/if}
+                            </tr>
+                            {/foreach}
+                            </tbody>
+                            <tfoot>
+                            <tr>
+                                <td></td>
+                                <td>
+                                    <div class="layer-footer">
+                                        <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+                                        <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                                    </div>
+                                </td>
+                                {if $Think.config.app_debug}
+                                <td></td>
+                                <td></td>
+                                {/if}
+                            </tr>
+                            </tfoot>
+                        </table>
+                    </form>
+                </div>
+            </div>
+            {/foreach}
+            <div class="tab-pane fade" id="addcfg">
+                <form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="{:url('general.config/add')}">
+                    {:token()}
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Group')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <select name="row[group]" class="form-control selectpicker">
+                                {foreach name="groupList" item="vo"}
+                                <option value="{$key}" {in name="key" value="basic" }selected{/in}>{$vo}</option>
+                                {/foreach}
+                            </select>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <select name="row[type]" id="c-type" class="form-control selectpicker">
+                                {foreach name="typeList" item="vo"}
+                                <option value="{$key}" {in name="key" value="string" }selected{/in}>{$vo}</option>
+                                {/foreach}
+                            </select>
+                        </div>
+                    </div>
+                    <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-4">
+                            <input type="text" class="form-control" id="name" name="row[name]" value="" data-rule="required; length(3~30); remote(general/config/check)"/>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="title" name="row[title]" value="" data-rule="required"/>
+                        </div>
+                    </div>
+                    <div class="form-group hidden tf tf-selectpage tf-selectpages">
+                        <label for="c-selectpage-table" class="control-label col-xs-12 col-sm-2">{:__('Selectpage table')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <select id="c-selectpage-table" name="row[setting][table]" class="form-control selectpicker" data-live-search="true">
+                                <option value="">{:__('Please select table')}</option>
+                            </select>
+                        </div>
+                    </div>
+                    <div class="form-group hidden tf tf-selectpage tf-selectpages">
+                        <label for="c-selectpage-primarykey" class="control-label col-xs-12 col-sm-2">{:__('Selectpage primarykey')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <select name="row[setting][primarykey]" class="form-control selectpicker" id="c-selectpage-primarykey"></select>
+                        </div>
+                    </div>
+                    <div class="form-group hidden tf tf-selectpage tf-selectpages">
+                        <label for="c-selectpage-field" class="control-label col-xs-12 col-sm-2">{:__('Selectpage field')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <select name="row[setting][field]" class="form-control selectpicker" id="c-selectpage-field"></select>
+                        </div>
+                    </div>
+                    <div class="form-group hidden tf tf-selectpage tf-selectpages">
+                        <label class="control-label col-xs-12 col-sm-2">{:__('Selectpage conditions')}:</label>
+                        <div class="col-xs-12 col-sm-8">
+                            <dl class="fieldlist" data-name="row[setting][conditions]">
+                                <dd>
+                                    <ins>{:__('Field title')}</ins>
+                                    <ins>{:__('Field value')}</ins>
+                                </dd>
+
+                                <dd><a href="javascript:;" class="append btn btn-sm btn-success"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                                <textarea name="row[setting][conditions]" class="form-control hide" cols="30" rows="5"></textarea>
+                            </dl>
+                        </div>
+                    </div>
+                    <div class="form-group hidden tf tf-array">
+                        <label for="c-array-key" class="control-label col-xs-12 col-sm-2">{:__('Array key')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" name="row[setting][key]" class="form-control" id="c-array-key">
+                        </div>
+                    </div>
+                    <div class="form-group hidden tf tf-array">
+                        <label for="c-array-value" class="control-label col-xs-12 col-sm-2">{:__('Array value')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" name="row[setting][value]" class="form-control" id="c-array-value">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="value" class="control-label col-xs-12 col-sm-2">{:__('Value')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="value" name="row[value]" value="" data-rule=""/>
+                        </div>
+                    </div>
+                    <div class="form-group hide" id="add-content-container">
+                        <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <textarea name="row[content]" id="content" cols="30" rows="5" class="form-control" data-rule="required(content)">value1|title1
+value2|title2</textarea>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="tip" class="control-label col-xs-12 col-sm-2">{:__('Tip')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="tip" name="row[tip]" value="" data-rule=""/>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="rule" class="control-label col-xs-12 col-sm-2">{:__('Rule')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <div class="input-group pull-left">
+                                <input type="text" class="form-control" id="rule" name="row[rule]" value="" data-tip="{:__('Rule tips')}"/>
+                                <span class="input-group-btn">
+                                    <button class="btn btn-primary dropdown-toggle" data-toggle="dropdown" type="button">{:__('Choose')}</button>
+                                    <ul class="dropdown-menu pull-right rulelist">
+                                        {volist name="ruleList" id="item"}
+                                        <li><a href="javascript:;" data-value="{$key}">{$item}<span class="text-muted">({$key})</span></a></li>
+                                        {/volist}
+                                    </ul>
+                                </span>
+                            </div>
+                            <span class="msg-box n-right" for="rule"></span>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="visible" class="control-label col-xs-12 col-sm-2">{:__('Visible condition')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <input type="text" class="form-control" id="visible" name="row[visible]" value="" data-rule=""/>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="extend" class="control-label col-xs-12 col-sm-2">{:__('Extend')}:</label>
+                        <div class="col-xs-12 col-sm-4">
+                            <textarea name="row[extend]" id="extend" cols="30" rows="5" class="form-control" data-tip="{:__('Extend tips')}" data-rule="required(extend)" data-msg-extend="当类型为自定义时,扩展属性不能为空"></textarea>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label col-xs-12 col-sm-2"></label>
+                        <div class="col-xs-12 col-sm-4">
+                            {if !$Think.config.app_debug}
+                            <button type="button" class="btn btn-primary disabled">{:__('Only work at development environment')}</button>
+                            {else/}
+                            <button type="submit" class="btn btn-primary btn-embossed">{:__('OK')}</button>
+                            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                            {/if}
+                        </div>
+                    </div>
+
+                </form>
+
+            </div>
+            <!--@formatter:on-->
+        </div>
+    </div>
+</div>

+ 115 - 0
application/application/admin/view/general/profile/index.html

@@ -0,0 +1,115 @@
+<style>
+    .profile-avatar-container {
+        position: relative;
+        width: 100px;
+        margin: 0 auto;
+    }
+
+    .profile-avatar-container .profile-user-img {
+        width: 100px;
+        height: 100px;
+    }
+
+    .profile-avatar-container .profile-avatar-text {
+        display: none;
+    }
+
+    .profile-avatar-container:hover .profile-avatar-text {
+        display: block;
+        position: absolute;
+        height: 100px;
+        width: 100px;
+        background: #444;
+        opacity: .6;
+        color: #fff;
+        top: 0;
+        left: 0;
+        line-height: 100px;
+        text-align: center;
+    }
+
+    .profile-avatar-container button {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100px;
+        height: 100px;
+        opacity: 0;
+    }
+</style>
+<div class="row animated fadeInRight">
+    <div class="col-md-4">
+        <div class="box box-primary">
+            <div class="panel-heading">
+                {:__('Profile')}
+            </div>
+            <div class="panel-body">
+
+                <form id="update-form" role="form" data-toggle="validator" method="POST" action="{:url('general.profile/update')}">
+                    {:token()}
+                    <input type="hidden" id="c-avatar" name="row[avatar]" value="{$admin.avatar|htmlentities}"/>
+                    <div class="box-body box-profile">
+
+                        <div class="profile-avatar-container">
+                            <img class="profile-user-img img-responsive img-circle" src="{$admin.avatar|cdnurl|htmlentities}" alt="">
+                            <div class="profile-avatar-text img-circle">{:__('Click to edit')}</div>
+                            <button type="button" id="faupload-avatar" class="faupload" data-input-id="c-avatar"><i class="fa fa-upload"></i> {:__('Upload')}</button>
+                        </div>
+
+                        <h3 class="profile-username text-center">{$admin.username|htmlentities}</h3>
+
+                        <p class="text-muted text-center">{$admin.email|htmlentities}</p>
+                        <div class="form-group">
+                            <label for="username" class="control-label">{:__('Username')}:</label>
+                            <input type="text" class="form-control" id="username" name="row[username]" value="{$admin.username|htmlentities}" disabled/>
+                        </div>
+                        <div class="form-group">
+                            <label for="email" class="control-label">{:__('Email')}:</label>
+                            <input type="text" class="form-control" id="email" name="row[email]" value="{$admin.email|htmlentities}" data-rule="required;email"/>
+                        </div>
+                        <div class="form-group">
+                            <label for="nickname" class="control-label">{:__('Nickname')}:</label>
+                            <input type="text" class="form-control" id="nickname" name="row[nickname]" value="{$admin.nickname|htmlentities}" data-rule="required"/>
+                        </div>
+                        <div class="form-group">
+                            <label for="password" class="control-label">{:__('Password')}:</label>
+                            <input type="password" class="form-control" id="password" placeholder="{:__('Leave password blank if dont want to change')}" autocomplete="new-password" name="row[password]" value="" data-rule="password"/>
+                        </div>
+                        <div class="form-group">
+                            <button type="submit" class="btn btn-primary">{:__('Submit')}</button>
+                            <button type="reset" class="btn btn-default">{:__('Reset')}</button>
+                        </div>
+
+                    </div>
+                </form>
+            </div>
+        </div>
+
+    </div>
+    <div class="col-md-8">
+        <div class="panel panel-default panel-intro panel-nav">
+            <div class="panel-heading">
+                <ul class="nav nav-tabs">
+                    <li class="active"><a href="#one" data-toggle="tab"><i class="fa fa-list"></i> {:__('Admin log')}</a></li>
+                </ul>
+            </div>
+            <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')}
+                            </div>
+                            <table id="table" class="table table-striped table-bordered table-hover table-nowrap" width="100%">
+
+                            </table>
+
+                        </div>
+                    </div>
+
+                </div>
+            </div>
+        </div>
+
+    </div>
+</div>

+ 143 - 0
application/application/admin/view/index/login.html

@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html lang="{$config.language}">
+<head>
+    {include file="common/meta" /}
+
+    <style type="text/css">
+        body {
+            color: #999;
+            background-color: #f1f4fd;
+            background-size: cover;
+        }
+
+        a {
+            color: #444;
+        }
+
+
+        .login-screen {
+            max-width: 430px;
+            padding: 0;
+            margin: 100px auto 0 auto;
+
+        }
+
+        .login-screen .well {
+            border-radius: 3px;
+            -webkit-box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
+            box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
+            background: rgba(255, 255, 255, 1);
+            border: none;
+            overflow: hidden;
+            padding: 0;
+        }
+
+        @media (max-width: 767px) {
+            .login-screen {
+                padding: 0 20px;
+            }
+        }
+
+        .profile-img-card {
+            width: 100px;
+            height: 100px;
+            display: block;
+            -moz-border-radius: 50%;
+            -webkit-border-radius: 50%;
+            border-radius: 50%;
+            margin: -93px auto 30px;
+            border: 5px solid #fff;
+        }
+
+        .profile-name-card {
+            text-align: center;
+        }
+
+        .login-head {
+            background: #899fe1;
+        }
+
+        .login-form {
+            padding: 40px 30px;
+            position: relative;
+            z-index: 99;
+        }
+
+        #login-form {
+            margin-top: 20px;
+        }
+
+        #login-form .input-group {
+            margin-bottom: 15px;
+        }
+
+        #login-form .form-control {
+            font-size: 13px;
+        }
+
+    </style>
+    <!--@formatter:off-->
+    {if $background}
+        <style type="text/css">
+            body{
+                background-image: url('{$background}');
+            }
+        </style>
+    {/if}
+    <!--@formatter:on-->
+</head>
+<body>
+<div class="container">
+    <div class="login-wrapper">
+        <div class="login-screen">
+            <div class="well">
+                <div class="login-head">
+                    <img src="__CDN__/assets/img/login-head.png" style="width:100%;"/>
+                </div>
+                <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">
+                        <!--@AdminLoginFormBegin-->
+                        <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="{:__('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="{:__('Password')}" name="password" autocomplete="off" value="" data-rule="{:__('Password')}:required;password"/>
+                        </div>
+                        <!--@CaptchaBegin-->
+                        {if $Think.config.fastadmin.login_captcha}
+                        <div class="input-group">
+                            <div class="input-group-addon"><span class="glyphicon glyphicon-option-horizontal" aria-hidden="true"></span></div>
+                            <input type="text" name="captcha" class="form-control" placeholder="{:__('Captcha')}" data-rule="{:__('Captcha')}:required;length({$Think.config.captcha.length})" autocomplete="off"/>
+                            <span class="input-group-addon" style="padding:0;border:none;cursor:pointer;">
+                                    <img src="{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha" width="100" height="30" onclick="this.src = '{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha&r=' + Math.random();"/>
+                            </span>
+                        </div>
+                        {/if}
+                        <!--@CaptchaEnd-->
+                        <div class="form-group checkbox">
+                            <label class="inline" for="keeplogin">
+                                <input type="checkbox" name="keeplogin" id="keeplogin" value="1"/>
+                                {:__('Keep login')}
+                            </label>
+                        </div>
+                        <div class="form-group">
+                            <button type="submit" class="btn btn-success btn-lg btn-block" style="background:#708eea;">{:__('Sign in')}</button>
+                        </div>
+                        <!--@AdminLoginFormEnd-->
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{include file="common/script" /}
+</body>
+</html>

+ 28 - 0
application/application/admin/view/user/group/index.html

@@ -0,0 +1,28 @@
+<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,add,edit,del')}
+                        <div class="dropdown btn-group {:$auth->check('user/group/multi')?'':'hide'}">
+                            <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">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <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>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit="{:$auth->check('user/group/edit')}"
+                           data-operate-del="{:$auth->check('user/group/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 28 - 0
application/application/admin/view/user/rule/index.html

@@ -0,0 +1,28 @@
+<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,add,edit,del')}
+                        <div class="dropdown btn-group {:$auth->check('user/rule/multi')?'':'hide'}">
+                            <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">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <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>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit="{:$auth->check('user/rule/edit')}"
+                           data-operate-del="{:$auth->check('user/rule/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 96 - 0
application/application/api/controller/Ems.php

@@ -0,0 +1,96 @@
+<?php
+
+namespace app\api\controller;
+
+use app\common\controller\Api;
+use app\common\library\Ems as Emslib;
+use app\common\model\User;
+use think\Hook;
+
+/**
+ * 邮箱验证码接口
+ */
+class Ems extends Api
+{
+    protected $noNeedLogin = '*';
+    protected $noNeedRight = '*';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+    }
+
+    /**
+     * 发送验证码
+     *
+     * @ApiMethod (POST)
+     * @param string $email 邮箱
+     * @param string $event 事件名称
+     */
+    public function send()
+    {
+        $email = $this->request->post("email");
+        $event = $this->request->post("event");
+        $event = $event ? $event : 'register';
+
+        $last = Emslib::get($email, $event);
+        if ($last && time() - $last['createtime'] < 60) {
+            $this->error(__('发送频繁'));
+        }
+        if ($event) {
+            $userinfo = User::getByEmail($email);
+            if ($event == 'register' && $userinfo) {
+                //已被注册
+                $this->error(__('已被注册'));
+            } elseif (in_array($event, ['changeemail']) && $userinfo) {
+                //被占用
+                $this->error(__('已被占用'));
+            } elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo) {
+                //未注册
+                $this->error(__('未注册'));
+            }
+        }
+        $ret = Emslib::send($email, null, $event);
+        if ($ret) {
+            $this->success(__('发送成功'));
+        } else {
+            $this->error(__('发送失败'));
+        }
+    }
+
+    /**
+     * 检测验证码
+     *
+     * @ApiMethod (POST)
+     * @param string $email   邮箱
+     * @param string $event   事件名称
+     * @param string $captcha 验证码
+     */
+    public function check()
+    {
+        $email = $this->request->post("email");
+        $event = $this->request->post("event");
+        $event = $event ? $event : 'register';
+        $captcha = $this->request->post("captcha");
+
+        if ($event) {
+            $userinfo = User::getByEmail($email);
+            if ($event == 'register' && $userinfo) {
+                //已被注册
+                $this->error(__('已被注册'));
+            } elseif (in_array($event, ['changeemail']) && $userinfo) {
+                //被占用
+                $this->error(__('已被占用'));
+            } elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo) {
+                //未注册
+                $this->error(__('未注册'));
+            }
+        }
+        $ret = Emslib::check($email, $captcha, $event);
+        if ($ret) {
+            $this->success(__('成功'));
+        } else {
+            $this->error(__('验证码不正确'));
+        }
+    }
+}

+ 159 - 0
application/application/common/library/Ems.php

@@ -0,0 +1,159 @@
+<?php
+
+namespace app\common\library;
+
+use fast\Random;
+use think\Hook;
+
+/**
+ * 邮箱验证码类
+ */
+class Ems
+{
+
+    /**
+     * 验证码有效时长
+     * @var int
+     */
+    protected static $expire = 120;
+
+    /**
+     * 最大允许检测的次数
+     * @var int
+     */
+    protected static $maxCheckNums = 10;
+
+    /**
+     * 获取最后一次邮箱发送的数据
+     *
+     * @param int    $email 邮箱
+     * @param string $event 事件
+     * @return  Ems
+     */
+    public static function get($email, $event = 'default')
+    {
+        $ems = \app\common\model\Ems::
+        where(['email' => $email, 'event' => $event])
+            ->order('id', 'DESC')
+            ->find();
+        Hook::listen('ems_get', $ems, null, true);
+        return $ems ? $ems : null;
+    }
+
+    /**
+     * 发送验证码
+     *
+     * @param int    $email 邮箱
+     * @param int    $code  验证码,为空时将自动生成4位数字
+     * @param string $event 事件
+     * @return  boolean
+     */
+    public static function send($email, $code = null, $event = 'default')
+    {
+        $code = is_null($code) ? Random::numeric(config('captcha.length')) : $code;
+        $time = time();
+        $ip = request()->ip();
+        $ems = \app\common\model\Ems::create(['event' => $event, 'email' => $email, 'code' => $code, 'ip' => $ip, 'createtime' => $time]);
+        if (!Hook::get('ems_send')) {
+            //采用框架默认的邮件推送
+            Hook::add('ems_send', function ($params) {
+                $obj = new Email();
+                $result = $obj
+                    ->to($params->email)
+                    ->subject('请查收你的验证码!')
+                    ->message("你的验证码是:" . $params->code . "," . ceil(self::$expire / 60) . "分钟内有效。")
+                    ->send();
+                return $result;
+            });
+        }
+        $result = Hook::listen('ems_send', $ems, null, true);
+        if (!$result) {
+            $ems->delete();
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 发送通知
+     *
+     * @param mixed  $email    邮箱,多个以,分隔
+     * @param string $msg      消息内容
+     * @param string $template 消息模板
+     * @return  boolean
+     */
+    public static function notice($email, $msg = '', $template = null)
+    {
+        $params = [
+            'email'    => $email,
+            'msg'      => $msg,
+            'template' => $template
+        ];
+        if (!Hook::get('ems_notice')) {
+            //采用框架默认的邮件推送
+            Hook::add('ems_notice', function ($params) {
+                $subject = '你收到一封新的邮件!';
+                $content = $params['msg'];
+                $email = new Email();
+                $result = $email->to($params['email'])
+                    ->subject($subject)
+                    ->message($content)
+                    ->send();
+                return $result;
+            });
+        }
+        $result = Hook::listen('ems_notice', $params, null, true);
+        return $result ? true : false;
+    }
+
+    /**
+     * 校验验证码
+     *
+     * @param int    $email 邮箱
+     * @param int    $code  验证码
+     * @param string $event 事件
+     * @return  boolean
+     */
+    public static function check($email, $code, $event = 'default')
+    {
+        $time = time() - self::$expire;
+        $ems = \app\common\model\Ems::where(['email' => $email, 'event' => $event])
+            ->order('id', 'DESC')
+            ->find();
+        if ($ems) {
+            if ($ems['createtime'] > $time && $ems['times'] <= self::$maxCheckNums) {
+                $correct = $code == $ems['code'];
+                if (!$correct) {
+                    $ems->times = $ems->times + 1;
+                    $ems->save();
+                    return false;
+                } else {
+                    $result = Hook::listen('ems_check', $ems, null, true);
+                    return true;
+                }
+            } else {
+                // 过期则清空该邮箱验证码
+                self::flush($email, $event);
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 清空指定邮箱验证码
+     *
+     * @param int    $email 邮箱
+     * @param string $event 事件
+     * @return  boolean
+     */
+    public static function flush($email, $event = 'default')
+    {
+        \app\common\model\Ems::
+        where(['email' => $email, 'event' => $event])
+            ->delete();
+        Hook::listen('ems_flush');
+        return true;
+    }
+}

+ 437 - 0
application/application/common/library/Upload.php

@@ -0,0 +1,437 @@
+<?php
+
+namespace app\common\library;
+
+use app\common\exception\UploadException;
+use app\common\model\Attachment;
+use fast\Random;
+use FilesystemIterator;
+use think\Config;
+use think\File;
+use think\Hook;
+
+/**
+ * 文件上传类
+ */
+class Upload
+{
+
+    protected $merging = false;
+
+    protected $chunkDir = null;
+
+    protected $config = [];
+
+    protected $error = '';
+
+    /**
+     * @var File
+     */
+    protected $file = null;
+    protected $fileInfo = null;
+
+    public function __construct($file = null)
+    {
+        $this->config = Config::get('upload');
+        $this->chunkDir = RUNTIME_PATH . 'chunks';
+        if ($file) {
+            $this->setFile($file);
+        }
+    }
+
+    /**
+     * 设置分片目录
+     * @param $dir
+     */
+    public function setChunkDir($dir)
+    {
+        $this->chunkDir = $dir;
+    }
+
+    /**
+     * 获取文件
+     * @return File
+     */
+    public function getFile()
+    {
+        return $this->file;
+    }
+
+    /**
+     * 设置文件
+     * @param $file
+     * @throws UploadException
+     */
+    public function setFile($file)
+    {
+        if (empty($file)) {
+            throw new UploadException(__('No file upload or server upload limit exceeded'));
+        }
+
+        $fileInfo = $file->getInfo();
+        $suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
+        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
+        $fileInfo['suffix'] = $suffix;
+        $fileInfo['imagewidth'] = 0;
+        $fileInfo['imageheight'] = 0;
+
+        $this->file = $file;
+        $this->fileInfo = $fileInfo;
+        $this->checkExecutable();
+    }
+
+    /**
+     * 检测是否为可执行脚本
+     * @return bool
+     * @throws UploadException
+     */
+    protected function checkExecutable()
+    {
+        //禁止上传PHP和HTML文件
+        if (in_array($this->fileInfo['type'], ['text/x-php', 'text/html']) || in_array($this->fileInfo['suffix'], ['php', 'html', 'htm', 'phar', 'phtml']) || preg_match("/^php(.*)/i", $this->fileInfo['suffix'])) {
+            throw new UploadException(__('Uploaded file format is limited'));
+        }
+        return true;
+    }
+
+    /**
+     * 检测文件类型
+     * @return bool
+     * @throws UploadException
+     */
+    protected function checkMimetype()
+    {
+        $mimetypeArr = explode(',', strtolower($this->config['mimetype']));
+        $typeArr = explode('/', $this->fileInfo['type']);
+        //Mimetype值不正确
+        if (stripos($this->fileInfo['type'], '/') === false) {
+            throw new UploadException(__('Uploaded file format is limited'));
+        }
+        //验证文件后缀
+        if ($this->config['mimetype'] === '*'
+            || in_array($this->fileInfo['suffix'], $mimetypeArr) || in_array('.' . $this->fileInfo['suffix'], $mimetypeArr)
+            || in_array($typeArr[0] . "/*", $mimetypeArr) || (in_array($this->fileInfo['type'], $mimetypeArr) && stripos($this->fileInfo['type'], '/') !== false)) {
+            return true;
+        }
+        throw new UploadException(__('Uploaded file format is limited'));
+    }
+
+    /**
+     * 检测是否图片
+     * @param bool $force
+     * @return bool
+     * @throws UploadException
+     */
+    protected function checkImage($force = false)
+    {
+        //验证是否为图片文件
+        if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
+            $imgInfo = getimagesize($this->fileInfo['tmp_name']);
+            if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
+                throw new UploadException(__('Uploaded file is not a valid image'));
+            }
+            $this->fileInfo['imagewidth'] = isset($imgInfo[0]) ? $imgInfo[0] : 0;
+            $this->fileInfo['imageheight'] = isset($imgInfo[1]) ? $imgInfo[1] : 0;
+            return true;
+        } else {
+            return !$force;
+        }
+    }
+
+    /**
+     * 检测文件大小
+     * @throws UploadException
+     */
+    protected function checkSize()
+    {
+        preg_match('/([0-9\.]+)(\w+)/', $this->config['maxsize'], $matches);
+        $size = $matches ? $matches[1] : $this->config['maxsize'];
+        $type = $matches ? strtolower($matches[2]) : 'b';
+        $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
+        $size = (int)($size * pow(1024, isset($typeDict[$type]) ? $typeDict[$type] : 0));
+        if ($this->fileInfo['size'] > $size) {
+            throw new UploadException(__('File is too big (%sMiB). Max filesize: %sMiB.',
+                round($this->fileInfo['size'] / pow(1024, 2), 2),
+                round($size / pow(1024, 2), 2)));
+        }
+    }
+
+    /**
+     * 获取后缀
+     * @return string
+     */
+    public function getSuffix()
+    {
+        return $this->fileInfo['suffix'] ?: 'file';
+    }
+
+    /**
+     * 获取存储的文件名
+     * @param string $savekey
+     * @param string $filename
+     * @param string $md5
+     * @return mixed|null
+     */
+    public function getSavekey($savekey = null, $filename = null, $md5 = null)
+    {
+        if ($filename) {
+            $suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+            $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
+        } else {
+            $suffix = $this->fileInfo['suffix'];
+        }
+        $filename = $filename ? $filename : ($suffix ? substr($this->fileInfo['name'], 0, strripos($this->fileInfo['name'], '.')) : $this->fileInfo['name']);
+        $filename = xss_clean(strip_tags(htmlspecialchars($filename)));
+        $md5 = $md5 ? $md5 : md5_file($this->fileInfo['tmp_name']);
+        $replaceArr = [
+            '{year}'     => date("Y"),
+            '{mon}'      => date("m"),
+            '{day}'      => date("d"),
+            '{hour}'     => date("H"),
+            '{min}'      => date("i"),
+            '{sec}'      => date("s"),
+            '{random}'   => Random::alnum(16),
+            '{random32}' => Random::alnum(32),
+            '{filename}' => substr($filename, 0, 100),
+            '{suffix}'   => $suffix,
+            '{.suffix}'  => $suffix ? '.' . $suffix : '',
+            '{filemd5}'  => $md5,
+        ];
+        $savekey = $savekey ? $savekey : $this->config['savekey'];
+        $savekey = str_replace(array_keys($replaceArr), array_values($replaceArr), $savekey);
+
+        return $savekey;
+    }
+
+    /**
+     * 清理分片文件
+     * @param $chunkid
+     */
+    public function clean($chunkid)
+    {
+        if (!preg_match('/^[a-z0-9\-]{36}$/', $chunkid)) {
+            throw new UploadException(__('Invalid parameters'));
+        }
+        $iterator = new \GlobIterator($this->chunkDir . DS . $chunkid . '-*', FilesystemIterator::KEY_AS_FILENAME);
+        $array = iterator_to_array($iterator);
+        foreach ($array as $index => &$item) {
+            $sourceFile = $item->getRealPath() ?: $item->getPathname();
+            $item = null;
+            @unlink($sourceFile);
+        }
+    }
+
+    /**
+     * 合并分片文件
+     * @param string $chunkid
+     * @param int    $chunkcount
+     * @param string $filename
+     * @return attachment|\think\Model
+     * @throws UploadException
+     */
+    public function merge($chunkid, $chunkcount, $filename)
+    {
+        if (!preg_match('/^[a-z0-9\-]{36}$/', $chunkid)) {
+            throw new UploadException(__('Invalid parameters'));
+        }
+
+        $filePath = $this->chunkDir . DS . $chunkid;
+
+        $completed = true;
+        //检查所有分片是否都存在
+        for ($i = 0; $i < $chunkcount; $i++) {
+            if (!file_exists("{$filePath}-{$i}.part")) {
+                $completed = false;
+                break;
+            }
+        }
+        if (!$completed) {
+            $this->clean($chunkid);
+            throw new UploadException(__('Chunk file info error'));
+        }
+
+        //如果所有文件分片都上传完毕,开始合并
+        $uploadPath = $filePath;
+
+        if (!$destFile = @fopen($uploadPath, "wb")) {
+            $this->clean($chunkid);
+            throw new UploadException(__('Chunk file merge error'));
+        }
+        if (flock($destFile, LOCK_EX)) { // 进行排他型锁定
+            for ($i = 0; $i < $chunkcount; $i++) {
+                $partFile = "{$filePath}-{$i}.part";
+                if (!$handle = @fopen($partFile, "rb")) {
+                    break;
+                }
+                while ($buff = fread($handle, filesize($partFile))) {
+                    fwrite($destFile, $buff);
+                }
+                @fclose($handle);
+                @unlink($partFile); //删除分片
+            }
+
+            flock($destFile, LOCK_UN);
+        }
+        @fclose($destFile);
+
+        $attachment = null;
+        try {
+            $file = new File($uploadPath);
+            $info = [
+                'name'     => $filename,
+                'type'     => $file->getMime(),
+                'tmp_name' => $uploadPath,
+                'error'    => 0,
+                'size'     => $file->getSize()
+            ];
+            $file->setSaveName($filename)->setUploadInfo($info);
+            $file->isTest(true);
+
+            //重新设置文件
+            $this->setFile($file);
+
+            unset($file);
+            $this->merging = true;
+
+            //允许大文件
+            $this->config['maxsize'] = "1024G";
+
+            $attachment = $this->upload();
+        } catch (\Exception $e) {
+            @unlink($destFile);
+            throw new UploadException($e->getMessage());
+        }
+        return $attachment;
+    }
+
+    /**
+     * 分片上传
+     * @throws UploadException
+     */
+    public function chunk($chunkid, $chunkindex, $chunkcount, $chunkfilesize = null, $chunkfilename = null, $direct = false)
+    {
+
+        if ($this->fileInfo['type'] != 'application/octet-stream') {
+            throw new UploadException(__('Uploaded file format is limited'));
+        }
+
+        if (!preg_match('/^[a-z0-9\-]{36}$/', $chunkid)) {
+            throw new UploadException(__('Invalid parameters'));
+        }
+
+        $destDir = RUNTIME_PATH . 'chunks';
+        $fileName = $chunkid . "-" . $chunkindex . '.part';
+        $destFile = $destDir . DS . $fileName;
+        if (!is_dir($destDir)) {
+            @mkdir($destDir, 0755, true);
+        }
+        if (!move_uploaded_file($this->file->getPathname(), $destFile)) {
+            throw new UploadException(__('Chunk file write error'));
+        }
+        $file = new File($destFile);
+        $info = [
+            'name'     => $fileName,
+            'type'     => $file->getMime(),
+            'tmp_name' => $destFile,
+            'error'    => 0,
+            'size'     => $file->getSize()
+        ];
+        $file->setSaveName($fileName)->setUploadInfo($info);
+        $this->setFile($file);
+        return $file;
+    }
+
+    /**
+     * 普通上传
+     * @return \app\common\model\attachment|\think\Model
+     * @throws UploadException
+     */
+    public function upload($savekey = null)
+    {
+        if (empty($this->file)) {
+            throw new UploadException(__('No file upload or server upload limit exceeded'));
+        }
+
+        $this->checkSize();
+        $this->checkExecutable();
+        $this->checkMimetype();
+        $this->checkImage();
+
+        $savekey = $savekey ? $savekey : $this->getSavekey();
+        $savekey = '/' . ltrim($savekey, '/');
+        $uploadDir = substr($savekey, 0, strripos($savekey, '/') + 1);
+        $fileName = substr($savekey, strripos($savekey, '/') + 1);
+
+        $destDir = ROOT_PATH . 'public' . str_replace('/', DS, $uploadDir);
+
+        $sha1 = $this->file->hash();
+
+        //如果是合并文件
+        if ($this->merging) {
+            if (!$this->file->check()) {
+                throw new UploadException($this->file->getError());
+            }
+            $destFile = $destDir . $fileName;
+            $sourceFile = $this->file->getRealPath() ?: $this->file->getPathname();
+            $info = $this->file->getInfo();
+            $this->file = null;
+            if (!is_dir($destDir)) {
+                @mkdir($destDir, 0755, true);
+            }
+            rename($sourceFile, $destFile);
+            $file = new File($destFile);
+            $file->setSaveName($fileName)->setUploadInfo($info);
+        } else {
+            $file = $this->file->move($destDir, $fileName);
+            if (!$file) {
+                // 上传失败获取错误信息
+                throw new UploadException($this->file->getError());
+            }
+        }
+        $this->file = $file;
+        $category = request()->post('category');
+        $category = array_key_exists($category, config('site.attachmentcategory') ?? []) ? $category : '';
+        $auth = Auth::instance();
+        $params = array(
+            'admin_id'    => (int)session('admin.id'),
+            'user_id'     => (int)$auth->id,
+            'filename'    => mb_substr(htmlspecialchars(strip_tags($this->fileInfo['name'])), 0, 100),
+            'category'    => $category,
+            'filesize'    => $this->fileInfo['size'],
+            'imagewidth'  => $this->fileInfo['imagewidth'],
+            'imageheight' => $this->fileInfo['imageheight'],
+            'imagetype'   => $this->fileInfo['suffix'],
+            'imageframes' => 0,
+            'mimetype'    => $this->fileInfo['type'],
+            'url'         => $uploadDir . $file->getSaveName(),
+            'uploadtime'  => time(),
+            'storage'     => 'local',
+            'sha1'        => $sha1,
+            'extparam'    => '',
+        );
+        $attachment = new Attachment();
+        $attachment->data(array_filter($params));
+        $attachment->save();
+
+        \think\Hook::listen("upload_after", $attachment);
+        return $attachment;
+    }
+
+    /**
+     * 设置错误信息
+     * @param $msg
+     */
+    public function setError($msg)
+    {
+        $this->error = $msg;
+    }
+
+    /**
+     * 获取错误信息
+     * @return string
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+}

+ 301 - 0
application/application/config.php

@@ -0,0 +1,301 @@
+<?php
+
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+use think\Env;
+
+return [
+    // +----------------------------------------------------------------------
+    // | 应用设置
+    // +----------------------------------------------------------------------
+    // 应用命名空间
+    'app_namespace'          => 'app',
+    // 应用调试模式
+    'app_debug'              => Env::get('app.debug', false),
+    // 应用Trace
+    'app_trace'              => Env::get('app.trace', false),
+    // 应用模式状态
+    'app_status'             => '',
+    // 是否支持多模块
+    'app_multi_module'       => true,
+    // 入口自动绑定模块
+    'auto_bind_module'       => false,
+    // 注册的根命名空间
+    'root_namespace'         => [],
+    // 扩展函数文件
+    'extra_file_list'        => [THINK_PATH . 'helper' . EXT],
+    // 默认输出类型
+    'default_return_type'    => 'html',
+    // 默认AJAX 数据返回格式,可选json xml ...
+    'default_ajax_return'    => 'json',
+    // 默认JSONP格式返回的处理方法
+    'default_jsonp_handler'  => 'jsonpReturn',
+    // 默认JSONP处理方法
+    'var_jsonp_handler'      => 'callback',
+    // 默认时区
+    'default_timezone'       => 'PRC',
+    // 是否开启多语言
+    'lang_switch_on'         => true,
+    // 默认全局过滤方法 用逗号分隔多个
+    'default_filter'         => '',
+    // 默认语言
+    'default_lang'           => 'zh-cn',
+    // 应用类库后缀
+    'class_suffix'           => false,
+    // 控制器类后缀
+    'controller_suffix'      => false,
+    // 获取IP的变量
+    'http_agent_ip'          => 'REMOTE_ADDR',
+    // +----------------------------------------------------------------------
+    // | 模块设置
+    // +----------------------------------------------------------------------
+    // 默认模块名
+    'default_module'         => 'index',
+    // 禁止访问模块
+    'deny_module_list'       => ['common', 'admin'],
+    // 默认控制器名
+    'default_controller'     => 'Index',
+    // 默认操作名
+    'default_action'         => 'index',
+    // 默认验证器
+    'default_validate'       => '',
+    // 默认的空控制器名
+    'empty_controller'       => 'Error',
+    // 操作方法后缀
+    'action_suffix'          => '',
+    // 自动搜索控制器
+    'controller_auto_search' => true,
+    // +----------------------------------------------------------------------
+    // | URL设置
+    // +----------------------------------------------------------------------
+    // PATHINFO变量名 用于兼容模式
+    'var_pathinfo'           => 's',
+    // 兼容PATH_INFO获取
+    'pathinfo_fetch'         => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
+    // pathinfo分隔符
+    'pathinfo_depr'          => '/',
+    // URL伪静态后缀
+    'url_html_suffix'        => 'html',
+    // URL普通方式参数 用于自动生成
+    'url_common_param'       => false,
+    // URL参数方式 0 按名称成对解析 1 按顺序解析
+    'url_param_type'         => 0,
+    // 是否开启路由
+    'url_route_on'           => true,
+    // 路由使用完整匹配
+    'route_complete_match'   => false,
+    // 路由配置文件(支持配置多个)
+    'route_config_file'      => ['route'],
+    // 是否强制使用路由
+    'url_route_must'         => false,
+    // 域名部署
+    'url_domain_deploy'      => false,
+    // 域名根,如thinkphp.cn
+    'url_domain_root'        => '',
+    // 是否自动转换URL中的控制器和操作名
+    'url_convert'            => true,
+    // 默认的访问控制器层
+    'url_controller_layer'   => 'controller',
+    // 表单请求类型伪装变量
+    'var_method'             => '_method',
+    // 表单ajax伪装变量
+    'var_ajax'               => '_ajax',
+    // 表单pjax伪装变量
+    'var_pjax'               => '_pjax',
+    // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
+    'request_cache'          => false,
+    // 请求缓存有效期
+    'request_cache_expire'   => null,
+    // +----------------------------------------------------------------------
+    // | 模板设置
+    // +----------------------------------------------------------------------
+    'template'               => [
+        // 模板引擎类型 支持 php think 支持扩展
+        'type'         => 'Think',
+        // 模板路径
+        'view_path'    => '',
+        // 模板后缀
+        'view_suffix'  => 'html',
+        // 模板文件名分隔符
+        'view_depr'    => DS,
+        // 模板引擎普通标签开始标记
+        'tpl_begin'    => '{',
+        // 模板引擎普通标签结束标记
+        'tpl_end'      => '}',
+        // 标签库标签开始标记
+        'taglib_begin' => '{',
+        // 标签库标签结束标记
+        'taglib_end'   => '}',
+        'tpl_cache'    => true,
+    ],
+    // 视图输出字符串内容替换,留空则会自动进行计算
+    'view_replace_str'       => [
+        '__PUBLIC__' => '',
+        '__ROOT__'   => '',
+        '__CDN__'    => '',
+    ],
+    // 默认跳转页面对应的模板文件
+    'dispatch_success_tmpl'  => APP_PATH . 'common' . DS . 'view' . DS . 'tpl' . DS . 'dispatch_jump.tpl',
+    'dispatch_error_tmpl'    => APP_PATH . 'common' . DS . 'view' . DS . 'tpl' . DS . 'dispatch_jump.tpl',
+    // +----------------------------------------------------------------------
+    // | 异常及错误设置
+    // +----------------------------------------------------------------------
+    // 异常页面的模板文件
+    'exception_tmpl'         => APP_PATH . 'common' . DS . 'view' . DS . 'tpl' . DS . 'think_exception.tpl',
+    // 错误显示信息,非调试模式有效
+    'error_message'          => '你所浏览的页面暂时无法访问',
+    // 显示错误信息
+    'show_error_msg'         => false,
+    // 异常处理handle类 留空使用 \think\exception\Handle
+    'exception_handle'       => '',
+    // +----------------------------------------------------------------------
+    // | 日志设置
+    // +----------------------------------------------------------------------
+    'log'                    => [
+        // 日志记录方式,内置 file socket 支持扩展
+        'type'  => 'File',
+        // 日志保存目录
+        'path'  => LOG_PATH,
+        // 日志记录级别
+        'level' => [],
+    ],
+    // +----------------------------------------------------------------------
+    // | Trace设置 开启 app_trace 后 有效
+    // +----------------------------------------------------------------------
+    'trace'                  => [
+        // 内置Html Console 支持扩展
+        'type' => 'Html',
+    ],
+    // +----------------------------------------------------------------------
+    // | 缓存设置
+    // +----------------------------------------------------------------------
+    'cache'                  => [
+        // 驱动方式
+        'type'   => 'File',
+        // 缓存保存目录
+        'path'   => CACHE_PATH,
+        // 缓存前缀
+        'prefix' => '',
+        // 缓存有效期 0表示永久缓存
+        'expire' => 0,
+    ],
+    // +----------------------------------------------------------------------
+    // | 会话设置
+    // +----------------------------------------------------------------------
+    'session'                => [
+        'id'             => '',
+        // SESSION_ID的提交变量,解决flash上传跨域
+        'var_session_id' => '',
+        // SESSION 前缀
+        'prefix'         => 'think',
+        // 驱动方式 支持redis memcache memcached
+        'type'           => '',
+        // 是否自动开启 SESSION
+        'auto_start'     => true,
+    ],
+    // +----------------------------------------------------------------------
+    // | Cookie设置
+    // +----------------------------------------------------------------------
+    'cookie'                 => [
+        // cookie 名称前缀
+        'prefix'    => '',
+        // cookie 保存时间
+        'expire'    => 0,
+        // cookie 保存路径
+        'path'      => '/',
+        // cookie 有效域名
+        'domain'    => '',
+        //  cookie 启用安全传输
+        'secure'    => false,
+        // httponly设置
+        'httponly'  => '',
+        // 是否使用 setcookie
+        'setcookie' => true,
+    ],
+    //分页配置
+    'paginate'               => [
+        'type'      => 'bootstrap',
+        'var_page'  => 'page',
+        'list_rows' => 15,
+    ],
+    //验证码配置
+    'captcha'                => [
+        // 验证码字符集合
+        'codeSet'  => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY',
+        // 验证码字体大小(px)
+        'fontSize' => 18,
+        // 是否画混淆曲线
+        'useCurve' => false,
+        //使用中文验证码
+        'useZh'    => false,
+        // 验证码图片高度
+        'imageH'   => 40,
+        // 验证码图片宽度
+        'imageW'   => 130,
+        // 验证码位数
+        'length'   => 4,
+        // 验证成功后是否重置
+        'reset'    => true
+    ],
+    // +----------------------------------------------------------------------
+    // | Token设置
+    // +----------------------------------------------------------------------
+    'token'                  => [
+        // 驱动方式
+        'type'     => 'Mysql',
+        // 缓存前缀
+        'key'      => 'i3d6o32wo8fvs1fvdpwens',
+        // 加密方式
+        'hashalgo' => 'ripemd160',
+        // 缓存有效期 0表示永久缓存
+        'expire'   => 0,
+    ],
+    //FastAdmin配置
+    'fastadmin'              => [
+        //是否开启前台会员中心
+        'usercenter'            => true,
+        //会员注册验证码类型email/mobile/wechat/text/false
+        'user_register_captcha' => 'text',
+        //登录验证码
+        'login_captcha'         => true,
+        //登录失败超过10次则1天后重试
+        'login_failure_retry'   => true,
+        //是否同一账号同一时间只能在一个地方登录
+        'login_unique'          => false,
+        //是否开启IP变动检测
+        'loginip_check'         => true,
+        //登录页默认背景图
+        'login_background'      => "",
+        //是否启用多级菜单导航
+        'multiplenav'           => false,
+        //是否开启多选项卡(仅在开启多级菜单时起作用)
+        'multipletab'           => true,
+        //是否默认展示子菜单
+        'show_submenu'          => false,
+        //后台皮肤,为空时表示使用skin-black-blue
+        'adminskin'             => '',
+        //后台是否启用面包屑
+        'breadcrumb'            => false,
+        //是否允许未知来源的插件压缩包
+        'unknownsources'        => false,
+        //插件启用禁用时是否备份对应的全局文件
+        'backup_global_files'   => true,
+        //是否开启后台自动日志记录
+        'auto_record_log'       => true,
+        //插件纯净模式,插件启用后是否删除插件目录的application、public和assets文件夹
+        'addon_pure_mode'       => true,
+        //允许跨域的域名,多个以,分隔
+        'cors_request_domain'   => 'localhost,127.0.0.1',
+        //版本号
+        'version'               => '1.3.3.20220121',
+        //API接口地址
+        'api_url'               => 'https://api.fastadmin.net',
+    ],
+];

+ 95 - 0
application/application/index/view/user/login.html

@@ -0,0 +1,95 @@
+<div id="content-container" class="container">
+    <div class="user-section login-section">
+        <div class="logon-tab clearfix"><a class="active">{:__('Sign in')}</a> <a href="{:url('user/register')}?url={$url|urlencode|htmlentities}">{:__('Sign up')}</a></div>
+        <div class="login-main">
+            <form name="form" id="login-form" class="form-vertical" method="POST" action="">
+                <!--@IndexLoginFormBegin-->
+                <input type="hidden" name="url" value="{$url|htmlentities}"/>
+                {:token()}
+                <div class="form-group">
+                    <label class="control-label" for="account">{:__('Account')}</label>
+                    <div class="controls">
+                        <input class="form-control" id="account" type="text" name="account" value="" data-rule="required" placeholder="{:__('Email/Mobile/Username')}" autocomplete="off">
+                        <div class="help-block"></div>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="control-label" for="password">{:__('Password')}</label>
+                    <div class="controls">
+                        <input class="form-control" id="password" type="password" name="password" data-rule="required;password" placeholder="{:__('Password')}" autocomplete="off">
+                    </div>
+                </div>
+                <div class="form-group">
+                    <div class="controls">
+                        <div class="checkbox inline">
+                            <label>
+                                <input type="checkbox" name="keeplogin" checked="checked" value="1"> {:__('Keep login')}
+                            </label>
+                        </div>
+                        <div class="pull-right"><a href="javascript:;" class="btn-forgot">{:__('Forgot password')}</a></div>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <button type="submit" class="btn btn-primary btn-lg btn-block">{:__('Sign in')}</button>
+                    <a href="{:url('user/register')}?url={$url|urlencode|htmlentities}" class="btn btn-default btn-lg btn-block mt-3 no-border">还没有账号?点击注册</a>
+                </div>
+                <!--@IndexLoginFormEnd-->
+            </form>
+        </div>
+    </div>
+</div>
+<script type="text/html" id="resetpwdtpl">
+    <form id="resetpwd-form" class="form-horizontal form-layer" method="POST" action="{:url('api/user/resetpwd')}">
+        <div class="form-body">
+            <input type="hidden" name="action" value="resetpwd"/>
+            <div class="form-group">
+                <label class="control-label col-xs-12 col-sm-3">{:__('Type')}:</label>
+                <div class="col-xs-12 col-sm-8">
+                    <div class="radio">
+                        <label for="type-email"><input id="type-email" checked="checked" name="type" data-send-url="{:url('api/ems/send')}" data-check-url="{:url('api/validate/check_ems_correct')}" type="radio" value="email"> {:__('Reset password by email')}</label>
+                        <label for="type-mobile"><input id="type-mobile" name="type" type="radio" data-send-url="{:url('api/sms/send')}" data-check-url="{:url('api/validate/check_sms_correct')}" value="mobile"> {:__('Reset password by mobile')}</label>
+                    </div>
+                </div>
+            </div>
+            <div class="form-group" data-type="email">
+                <label for="email" class="control-label col-xs-12 col-sm-3">{:__('Email')}:</label>
+                <div class="col-xs-12 col-sm-8">
+                    <input type="text" class="form-control" id="email" name="email" value="" data-rule="required(#type-email:checked);email;remote({:url('api/validate/check_email_exist')}, event=resetpwd, id=0)" placeholder="">
+                    <span class="msg-box"></span>
+                </div>
+            </div>
+            <div class="form-group hide" data-type="mobile">
+                <label for="mobile" class="control-label col-xs-12 col-sm-3">{:__('Mobile')}:</label>
+                <div class="col-xs-12 col-sm-8">
+                    <input type="text" class="form-control" id="mobile" name="mobile" value="" data-rule="required(#type-mobile:checked);mobile;remote({:url('api/validate/check_mobile_exist')}, event=resetpwd, id=0)" placeholder="">
+                    <span class="msg-box"></span>
+                </div>
+            </div>
+            <div class="form-group">
+                <label for="captcha" class="control-label col-xs-12 col-sm-3">{:__('Captcha')}:</label>
+                <div class="col-xs-12 col-sm-8">
+                    <div class="input-group">
+                        <input type="text" name="captcha" class="form-control" data-rule="required;length({$Think.config.captcha.length});integer[+];remote({:url('api/validate/check_ems_correct')}, event=resetpwd, email:#email)"/>
+                        <span class="input-group-btn" style="padding:0;border:none;">
+                            <a href="javascript:;" class="btn btn-primary btn-captcha" data-url="{:url('api/ems/send')}" data-type="email" data-event="resetpwd">{:__('Send verification code')}</a>
+                        </span>
+                    </div>
+                    <span class="msg-box"></span>
+                </div>
+            </div>
+            <div class="form-group">
+                <label for="newpassword" class="control-label col-xs-12 col-sm-3">{:__('New password')}:</label>
+                <div class="col-xs-12 col-sm-8">
+                    <input type="password" class="form-control" id="newpassword" name="newpassword" value="" data-rule="required;password" placeholder="">
+                    <span class="msg-box"></span>
+                </div>
+            </div>
+        </div>
+        <div class="form-group form-footer">
+            <label class="control-label col-xs-12 col-sm-3"></label>
+            <div class="col-xs-12 col-sm-8">
+                <button type="submit" class="btn btn-md btn-primary">{:__('Ok')}</button>
+            </div>
+        </div>
+    </form>
+</script>

+ 61 - 0
application/application/index/view/user/register.html

@@ -0,0 +1,61 @@
+<div id="content-container" class="container">
+    <div class="user-section login-section">
+        <div class="logon-tab clearfix"><a href="{:url('user/login')}?url={$url|urlencode|htmlentities}">{:__('Sign in')}</a> <a class="active">{:__('Sign up')}</a></div>
+        <div class="login-main">
+            <form name="form1" id="register-form" class="form-vertical" method="POST" action="">
+                <!--@IndexRegisterFormBegin-->
+                <input type="hidden" name="invite_user_id" value="0"/>
+                <input type="hidden" name="url" value="{$url|htmlentities}"/>
+                {:token()}
+                <div class="form-group">
+                    <label class="control-label required">{:__('Email')}<span class="text-success"></span></label>
+                    <div class="controls">
+                        <input type="text" name="email" id="email" data-rule="required;email" class="form-control" placeholder="{:__('Email')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="control-label">{:__('Username')}</label>
+                    <div class="controls">
+                        <input type="text" id="username" name="username" data-rule="required;username" class="form-control" placeholder="{:__('Username must be 3 to 30 characters')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="control-label">{:__('Password')}</label>
+                    <div class="controls">
+                        <input type="password" id="password" name="password" data-rule="required;password" class="form-control" placeholder="{:__('Password must be 6 to 30 characters')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="control-label">{:__('Mobile')}</label>
+                    <div class="controls">
+                        <input type="text" id="mobile" name="mobile" data-rule="required;mobile" class="form-control" placeholder="{:__('Mobile')}">
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+
+                <!--@CaptchaBegin-->
+                {if $captchaType}
+                <div class="form-group">
+                    <label class="control-label">{:__('Captcha')}</label>
+                    <div class="controls">
+                        <div class="input-group">
+                            {include file="common/captcha" event="register" type="$captchaType" /}
+                        </div>
+                        <p class="help-block"></p>
+                    </div>
+                </div>
+                {/if}
+                <!--@CaptchaEnd-->
+
+                <div class="form-group">
+                    <button type="submit" class="btn btn-primary btn-lg btn-block">{:__('Sign up')}</button>
+                    <a href="{:url('user/login')}?url={$url|urlencode|htmlentities}" class="btn btn-default btn-lg btn-block mt-3 no-border">已经有账号?点击登录</a>
+                </div>
+                <!--@IndexRegisterFormEnd-->
+            </form>
+        </div>
+    </div>
+</div>

+ 34 - 0
application/bower.json

@@ -0,0 +1,34 @@
+{
+  "name": "fastadmin",
+  "description": "the fastest admin framework",
+  "main": "",
+  "license": "Apache2.0",
+  "homepage": "https://www.fastadmin.net",
+  "private": true,
+  "dependencies": {
+    "jquery": "^2.1.4",
+    "bootstrap": "^3.3.7",
+    "font-awesome": "^4.6.1",
+    "bootstrap-table": "fastadmin-bootstraptable#~1.11.5",
+    "jstree": "~3.3.2",
+    "moment": "~2.29.0",
+    "toastr": "~2.1.3",
+    "eonasdan-bootstrap-datetimepicker": "~4.17.43",
+    "bootstrap-select": "~1.11.2",
+    "require-css": "~0.1.8",
+    "tableExport.jquery.plugin": "~1.10.3",
+    "jquery-slimscroll": "~1.3.8",
+    "jquery.cookie": "~1.4.1",
+    "Sortable": "~1.10.0",
+    "nice-validator": "~1.1.1",
+    "art-template": "~3.1.3",
+    "bootstrap-daterangepicker": "~2.1.25",
+    "fastadmin-citypicker": "~1.3.1",
+    "fastadmin-cxselect": "~1.4.0",
+    "fastadmin-dragsort": "~1.0.0",
+    "fastadmin-addtabs": "~1.0.5",
+    "fastadmin-selectpage": "~1.0.6",
+    "fastadmin-layer": "~3.5.1",
+    "bootstrap-slider": "*"
+  }
+}

+ 44 - 0
application/composer.json

@@ -0,0 +1,44 @@
+{
+    "name": "karsonzhang/fastadmin",
+    "description": "the fastest admin framework",
+    "type": "project",
+    "keywords": [
+        "fastadmin",
+        "thinkphp"
+    ],
+    "homepage": "https://www.fastadmin.net/",
+    "license": "Apache-2.0",
+    "authors": [
+        {
+            "name": "Karson",
+            "email": "karson@fastadmin.net"
+        }
+    ],
+    "require": {
+        "php": ">=7.1.0",
+        "topthink/framework": "dev-master",
+        "topthink/think-captcha": "^1.0",
+        "topthink/think-installer": "^1.0.14",
+        "topthink/think-queue": "1.1.6",
+        "topthink/think-helper": "^1.0.7",
+        "karsonzhang/fastadmin-addons": "~1.3.2",
+        "overtrue/pinyin": "^3.0",
+        "phpoffice/phpspreadsheet": "1.12",
+        "overtrue/wechat": "4.2.11",
+        "nelexa/zip": "^3.3",
+        "ext-json": "*",
+        "ext-curl": "*",
+        "ext-pdo": "*",
+        "ext-bcmath": "*",
+        "txthinking/mailer": "^2.0"
+    },
+    "config": {
+        "preferred-install": "dist"
+    },
+    "repositories": [
+        {
+            "type": "git",
+            "url": "https://gitee.com/karson/framework"
+        }
+    ]
+}

+ 2 - 2
application/config.php

@@ -251,7 +251,7 @@ return [
         // 驱动方式
         'type'     => 'Mysql',
         // 缓存前缀
-        'key'      => 'uPzj1SsvVpxRyB9TGCLhYoHOgwQiEbqr',
+        'key'      => 'i3d6o32wo8fvs1fvdpwenspcl',
         // 加密方式
         'hashalgo' => 'ripemd160',
         // 缓存有效期 0表示永久缓存
@@ -292,7 +292,7 @@ return [
         //允许跨域的域名,多个以,分隔
         'cors_request_domain'   => 'localhost,127.0.0.1',
         //版本号
-        'version'               => '1.3.0.20220101',
+        'version'               => '1.3.1.20220112',
         //API接口地址
         'api_url'               => 'https://api.fastadmin.net',
     ],

+ 226 - 0
application/extend/fast/Date.php

@@ -0,0 +1,226 @@
+<?php
+
+namespace fast;
+
+use DateTime;
+use DateTimeZone;
+
+/**
+ * 日期时间处理类
+ */
+class Date
+{
+    const YEAR = 31536000;
+    const MONTH = 2592000;
+    const WEEK = 604800;
+    const DAY = 86400;
+    const HOUR = 3600;
+    const MINUTE = 60;
+
+    /**
+     * 计算两个时区间相差的时长,单位为秒
+     *
+     * $seconds = self::offset('America/Chicago', 'GMT');
+     *
+     * [!!] A list of time zones that PHP supports can be found at
+     * <http://php.net/timezones>.
+     *
+     * @param string $remote timezone that to find the offset of
+     * @param string $local  timezone used as the baseline
+     * @param mixed  $now    UNIX timestamp or date string
+     * @return  integer
+     */
+    public static function offset($remote, $local = null, $now = null)
+    {
+        if ($local === null) {
+            // Use the default timezone
+            $local = date_default_timezone_get();
+        }
+        if (is_int($now)) {
+            // Convert the timestamp into a string
+            $now = date(DateTime::RFC2822, $now);
+        }
+        // Create timezone objects
+        $zone_remote = new DateTimeZone($remote);
+        $zone_local = new DateTimeZone($local);
+        // Create date objects from timezones
+        $time_remote = new DateTime($now, $zone_remote);
+        $time_local = new DateTime($now, $zone_local);
+        // Find the offset
+        $offset = $zone_remote->getOffset($time_remote) - $zone_local->getOffset($time_local);
+        return $offset;
+    }
+
+    /**
+     * 计算两个时间戳之间相差的时间
+     *
+     * $span = self::span(60, 182, 'minutes,seconds'); // array('minutes' => 2, 'seconds' => 2)
+     * $span = self::span(60, 182, 'minutes'); // 2
+     *
+     * @param int    $remote timestamp to find the span of
+     * @param int    $local  timestamp to use as the baseline
+     * @param string $output formatting string
+     * @return  string   when only a single output is requested
+     * @return  array    associative list of all outputs requested
+     * @from https://github.com/kohana/ohanzee-helpers/blob/master/src/Date.php
+     */
+    public static function span($remote, $local = null, $output = 'years,months,weeks,days,hours,minutes,seconds')
+    {
+        // Normalize output
+        $output = trim(strtolower((string)$output));
+        if (!$output) {
+            // Invalid output
+            return false;
+        }
+        // Array with the output formats
+        $output = preg_split('/[^a-z]+/', $output);
+        // Convert the list of outputs to an associative array
+        $output = array_combine($output, array_fill(0, count($output), 0));
+        // Make the output values into keys
+        extract(array_flip($output), EXTR_SKIP);
+        if ($local === null) {
+            // Calculate the span from the current time
+            $local = time();
+        }
+        // Calculate timespan (seconds)
+        $timespan = abs($remote - $local);
+        if (isset($output['years'])) {
+            $timespan -= self::YEAR * ($output['years'] = (int)floor($timespan / self::YEAR));
+        }
+        if (isset($output['months'])) {
+            $timespan -= self::MONTH * ($output['months'] = (int)floor($timespan / self::MONTH));
+        }
+        if (isset($output['weeks'])) {
+            $timespan -= self::WEEK * ($output['weeks'] = (int)floor($timespan / self::WEEK));
+        }
+        if (isset($output['days'])) {
+            $timespan -= self::DAY * ($output['days'] = (int)floor($timespan / self::DAY));
+        }
+        if (isset($output['hours'])) {
+            $timespan -= self::HOUR * ($output['hours'] = (int)floor($timespan / self::HOUR));
+        }
+        if (isset($output['minutes'])) {
+            $timespan -= self::MINUTE * ($output['minutes'] = (int)floor($timespan / self::MINUTE));
+        }
+        // Seconds ago, 1
+        if (isset($output['seconds'])) {
+            $output['seconds'] = $timespan;
+        }
+        if (count($output) === 1) {
+            // Only a single output was requested, return it
+            return array_pop($output);
+        }
+        // Return array
+        return $output;
+    }
+
+    /**
+     * 格式化 UNIX 时间戳为人易读的字符串
+     *
+     * @param int    Unix 时间戳
+     * @param mixed $local 本地时间
+     *
+     * @return    string    格式化的日期字符串
+     */
+    public static function human($remote, $local = null)
+    {
+        $time_diff = (is_null($local) || $local ? time() : $local) - $remote;
+        $tense = $time_diff < 0 ? 'after' : 'ago';
+        $time_diff = abs($time_diff);
+        $chunks = [
+            [60 * 60 * 24 * 365, 'year'],
+            [60 * 60 * 24 * 30, 'month'],
+            [60 * 60 * 24 * 7, 'week'],
+            [60 * 60 * 24, 'day'],
+            [60 * 60, 'hour'],
+            [60, 'minute'],
+            [1, 'second']
+        ];
+        $name = 'second';
+        $count = 0;
+
+        for ($i = 0, $j = count($chunks); $i < $j; $i++) {
+            $seconds = $chunks[$i][0];
+            $name = $chunks[$i][1];
+            if (($count = floor($time_diff / $seconds)) != 0) {
+                break;
+            }
+        }
+        return __("%d $name%s $tense", $count, ($count > 1 ? 's' : ''));
+    }
+
+    /**
+     * 获取一个基于时间偏移的Unix时间戳
+     *
+     * @param string $type     时间类型,默认为day,可选minute,hour,day,week,month,quarter,year
+     * @param int    $offset   时间偏移量 默认为0,正数表示当前type之后,负数表示当前type之前
+     * @param string $position 时间的开始或结束,默认为begin,可选前(begin,start,first,front),end
+     * @param int    $year     基准年,默认为null,即以当前年为基准
+     * @param int    $month    基准月,默认为null,即以当前月为基准
+     * @param int    $day      基准天,默认为null,即以当前天为基准
+     * @param int    $hour     基准小时,默认为null,即以当前年小时基准
+     * @param int    $minute   基准分钟,默认为null,即以当前分钟为基准
+     * @return int 处理后的Unix时间戳
+     */
+    public static function unixtime($type = 'day', $offset = 0, $position = 'begin', $year = null, $month = null, $day = null, $hour = null, $minute = null)
+    {
+        $year = is_null($year) ? date('Y') : $year;
+        $month = is_null($month) ? date('m') : $month;
+        $day = is_null($day) ? date('d') : $day;
+        $hour = is_null($hour) ? date('H') : $hour;
+        $minute = is_null($minute) ? date('i') : $minute;
+        $position = in_array($position, array('begin', 'start', 'first', 'front'));
+
+        $baseTime = mktime(0, 0, 0, $month, $day, $year);
+
+        switch ($type) {
+            case 'minute':
+                $time = $position ? mktime($hour, $minute + $offset, 0, $month, $day, $year) : mktime($hour, $minute + $offset, 59, $month, $day, $year);
+                break;
+            case 'hour':
+                $time = $position ? mktime($hour + $offset, 0, 0, $month, $day, $year) : mktime($hour + $offset, 59, 59, $month, $day, $year);
+                break;
+            case 'day':
+                $time = $position ? mktime(0, 0, 0, $month, $day + $offset, $year) : mktime(23, 59, 59, $month, $day + $offset, $year);
+                break;
+            case 'week':
+                $weekIndex = date("w", $baseTime);
+                $time = $position ?
+                    strtotime($offset . " weeks", strtotime(date('Y-m-d', strtotime("-" . ($weekIndex ? $weekIndex - 1 : 6) . " days", $baseTime)))) :
+                    strtotime($offset . " weeks", strtotime(date('Y-m-d 23:59:59', strtotime("+" . (6 - ($weekIndex ? $weekIndex - 1 : 6)) . " days", $baseTime))));
+                break;
+            case 'month':
+                $_timestamp = mktime(0, 0, 0, $month + $offset, 1, $year);
+                $time = $position ? $_timestamp : mktime(23, 59, 59, $month + $offset, self::days_in_month(date("m", $_timestamp), date("Y", $_timestamp)), $year);
+                break;
+            case 'quarter':
+                $_month = date("m", mktime(0, 0, 0, (ceil(date('n', mktime(0, 0, 0, $month, $day, $year)) / 3) + $offset) * 3, $day, $year));
+                $time = $position ?
+                    mktime(0, 0, 0, 1 + ((ceil(date('n', $baseTime) / 3) + $offset) - 1) * 3, 1, $year) :
+                    mktime(23, 59, 59, (ceil(date('n', $baseTime) / 3) + $offset) * 3, self::days_in_month((ceil(date('n', $baseTime) / 3) + $offset) * 3, $year), $year);
+                break;
+            case 'year':
+                $time = $position ? mktime(0, 0, 0, 1, 1, $year + $offset) : mktime(23, 59, 59, 12, 31, $year + $offset);
+                break;
+            default:
+                $time = mktime($hour, $minute, 0, $month, $day, $year);
+                break;
+        }
+        return $time;
+    }
+
+    /**
+     * 获取指定年月拥有的天数
+     * @param int $month
+     * @param int $year
+     * @return false|int|string
+     */
+    public static function days_in_month($month, $year)
+    {
+        if (function_exists("cal_days_in_month")) {
+            return cal_days_in_month(CAL_GREGORIAN, $month, $year);
+        } else {
+            return date('t', mktime(0, 0, 0, $month, 1, $year));
+        }
+    }
+}

+ 438 - 0
application/extend/fast/Tree.php

@@ -0,0 +1,438 @@
+<?php
+
+namespace fast;
+
+use think\Config;
+
+/**
+ * 通用的树型类
+ * @author XiaoYao <476552238li@gmail.com>
+ */
+class Tree
+{
+    protected static $instance;
+    //默认配置
+    protected $config = [];
+    public $options = [];
+
+    /**
+     * 生成树型结构所需要的2维数组
+     * @var array
+     */
+    public $arr = [];
+
+    /**
+     * 生成树型结构所需修饰符号,可以换成图片
+     * @var array
+     */
+    public $icon = array('│', '├', '└');
+    public $nbsp = "&nbsp;";
+    public $pidname = 'pid';
+
+    public function __construct($options = [])
+    {
+        if ($config = Config::get('tree')) {
+            $this->options = array_merge($this->config, $config);
+        }
+        $this->options = array_merge($this->config, $options);
+    }
+
+    /**
+     * 初始化
+     * @access public
+     * @param array $options 参数
+     * @return Tree
+     */
+    public static function instance($options = [])
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new static($options);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 初始化方法
+     * @param array  $arr     2维数组,例如:
+     *      array(
+     *      1 => array('id'=>'1','pid'=>0,'name'=>'一级栏目一'),
+     *      2 => array('id'=>'2','pid'=>0,'name'=>'一级栏目二'),
+     *      3 => array('id'=>'3','pid'=>1,'name'=>'二级栏目一'),
+     *      4 => array('id'=>'4','pid'=>1,'name'=>'二级栏目二'),
+     *      5 => array('id'=>'5','pid'=>2,'name'=>'二级栏目三'),
+     *      6 => array('id'=>'6','pid'=>3,'name'=>'三级栏目一'),
+     *      7 => array('id'=>'7','pid'=>3,'name'=>'三级栏目二')
+     *      )
+     * @param string $pidname 父字段名称
+     * @param string $nbsp    空格占位符
+     * @return Tree
+     */
+    public function init($arr = [], $pidname = null, $nbsp = null)
+    {
+        $this->arr = $arr;
+        if (!is_null($pidname)) {
+            $this->pidname = $pidname;
+        }
+        if (!is_null($nbsp)) {
+            $this->nbsp = $nbsp;
+        }
+        return $this;
+    }
+
+    /**
+     * 得到子级数组
+     * @param int
+     * @return array
+     */
+    public function getChild($myid)
+    {
+        $newarr = [];
+        foreach ($this->arr as $value) {
+            if (!isset($value['id'])) {
+                continue;
+            }
+            if ($value[$this->pidname] == $myid) {
+                $newarr[$value['id']] = $value;
+            }
+        }
+        return $newarr;
+    }
+
+    /**
+     * 读取指定节点的所有孩子节点
+     * @param int     $myid     节点ID
+     * @param boolean $withself 是否包含自身
+     * @return array
+     */
+    public function getChildren($myid, $withself = false)
+    {
+        $newarr = [];
+        foreach ($this->arr as $value) {
+            if (!isset($value['id'])) {
+                continue;
+            }
+            if ((string)$value[$this->pidname] == (string)$myid) {
+                $newarr[] = $value;
+                $newarr = array_merge($newarr, $this->getChildren($value['id']));
+            } elseif ($withself && (string)$value['id'] == (string)$myid) {
+                $newarr[] = $value;
+            }
+        }
+        return $newarr;
+    }
+
+    /**
+     * 读取指定节点的所有孩子节点ID
+     * @param int     $myid     节点ID
+     * @param boolean $withself 是否包含自身
+     * @return array
+     */
+    public function getChildrenIds($myid, $withself = false)
+    {
+        $childrenlist = $this->getChildren($myid, $withself);
+        $childrenids = [];
+        foreach ($childrenlist as $k => $v) {
+            $childrenids[] = $v['id'];
+        }
+        return $childrenids;
+    }
+
+    /**
+     * 得到当前位置父辈数组
+     * @param int
+     * @return array
+     */
+    public function getParent($myid)
+    {
+        $pid = 0;
+        $newarr = [];
+        foreach ($this->arr as $value) {
+            if (!isset($value['id'])) {
+                continue;
+            }
+            if ($value['id'] == $myid) {
+                $pid = $value[$this->pidname];
+                break;
+            }
+        }
+        if ($pid) {
+            foreach ($this->arr as $value) {
+                if ($value['id'] == $pid) {
+                    $newarr[] = $value;
+                    break;
+                }
+            }
+        }
+        return $newarr;
+    }
+
+    /**
+     * 得到当前位置所有父辈数组
+     * @param int
+     * @param bool $withself 是否包含自己
+     * @return array
+     */
+    public function getParents($myid, $withself = false)
+    {
+        $pid = 0;
+        $newarr = [];
+        foreach ($this->arr as $value) {
+            if (!isset($value['id'])) {
+                continue;
+            }
+            if ($value['id'] == $myid) {
+                if ($withself) {
+                    $newarr[] = $value;
+                }
+                $pid = $value[$this->pidname];
+                break;
+            }
+        }
+        if ($pid) {
+            $arr = $this->getParents($pid, true);
+            $newarr = array_merge($arr, $newarr);
+        }
+        return $newarr;
+    }
+
+    /**
+     * 读取指定节点所有父类节点ID
+     * @param int     $myid
+     * @param boolean $withself
+     * @return array
+     */
+    public function getParentsIds($myid, $withself = false)
+    {
+        $parentlist = $this->getParents($myid, $withself);
+        $parentsids = [];
+        foreach ($parentlist as $k => $v) {
+            $parentsids[] = $v['id'];
+        }
+        return $parentsids;
+    }
+
+    /**
+     * 树型结构Option
+     * @param int    $myid        表示获得这个ID下的所有子级
+     * @param string $itemtpl     条目模板 如:"<option value=@id @selected @disabled>@spacer@name</option>"
+     * @param mixed  $selectedids 被选中的ID,比如在做树型下拉框的时候需要用到
+     * @param mixed  $disabledids 被禁用的ID,比如在做树型下拉框的时候需要用到
+     * @param string $itemprefix  每一项前缀
+     * @param string $toptpl      顶级栏目的模板
+     * @return string
+     */
+    public function getTree($myid, $itemtpl = "<option value=@id @selected @disabled>@spacer@name</option>", $selectedids = '', $disabledids = '', $itemprefix = '', $toptpl = '')
+    {
+        $ret = '';
+        $number = 1;
+        $childs = $this->getChild($myid);
+        if ($childs) {
+            $total = count($childs);
+            foreach ($childs as $value) {
+                $id = $value['id'];
+                $j = $k = '';
+                if ($number == $total) {
+                    $j .= $this->icon[2];
+                    $k = $itemprefix ? $this->nbsp : '';
+                } else {
+                    $j .= $this->icon[1];
+                    $k = $itemprefix ? $this->icon[0] : '';
+                }
+                $spacer = $itemprefix ? $itemprefix . $j : '';
+                $selected = $selectedids && in_array($id, (is_array($selectedids) ? $selectedids : explode(',', $selectedids))) ? 'selected' : '';
+                $disabled = $disabledids && in_array($id, (is_array($disabledids) ? $disabledids : explode(',', $disabledids))) ? 'disabled' : '';
+                $value = array_merge($value, array('selected' => $selected, 'disabled' => $disabled, 'spacer' => $spacer));
+                $value = array_combine(array_map(function ($k) {
+                    return '@' . $k;
+                }, array_keys($value)), $value);
+                $nstr = strtr((($value["@{$this->pidname}"] == 0 || $this->getChild($id)) && $toptpl ? $toptpl : $itemtpl), $value);
+                $ret .= $nstr;
+                $ret .= $this->getTree($id, $itemtpl, $selectedids, $disabledids, $itemprefix . $k . $this->nbsp, $toptpl);
+                $number++;
+            }
+        }
+        return $ret;
+    }
+
+    /**
+     * 树型结构UL
+     * @param int    $myid        表示获得这个ID下的所有子级
+     * @param string $itemtpl     条目模板 如:"<li value=@id @selected @disabled>@name @childlist</li>"
+     * @param string $selectedids 选中的ID
+     * @param string $disabledids 禁用的ID
+     * @param string $wraptag     子列表包裹标签
+     * @param string $wrapattr    子列表包裹属性
+     * @return string
+     */
+    public function getTreeUl($myid, $itemtpl, $selectedids = '', $disabledids = '', $wraptag = 'ul', $wrapattr = '')
+    {
+        $str = '';
+        $childs = $this->getChild($myid);
+        if ($childs) {
+            foreach ($childs as $value) {
+                $id = $value['id'];
+                unset($value['child']);
+                $selected = $selectedids && in_array($id, (is_array($selectedids) ? $selectedids : explode(',', $selectedids))) ? 'selected' : '';
+                $disabled = $disabledids && in_array($id, (is_array($disabledids) ? $disabledids : explode(',', $disabledids))) ? 'disabled' : '';
+                $value = array_merge($value, array('selected' => $selected, 'disabled' => $disabled));
+                $value = array_combine(array_map(function ($k) {
+                    return '@' . $k;
+                }, array_keys($value)), $value);
+                $nstr = strtr($itemtpl, $value);
+                $childdata = $this->getTreeUl($id, $itemtpl, $selectedids, $disabledids, $wraptag, $wrapattr);
+                $childlist = $childdata ? "<{$wraptag} {$wrapattr}>" . $childdata . "</{$wraptag}>" : "";
+                $str .= strtr($nstr, array('@childlist' => $childlist));
+            }
+        }
+        return $str;
+    }
+
+    /**
+     * 菜单数据
+     * @param int    $myid
+     * @param string $itemtpl
+     * @param mixed  $selectedids
+     * @param mixed  $disabledids
+     * @param string $wraptag
+     * @param string $wrapattr
+     * @param int    $deeplevel
+     * @return string
+     */
+    public function getTreeMenu($myid, $itemtpl, $selectedids = '', $disabledids = '', $wraptag = 'ul', $wrapattr = '', $deeplevel = 0)
+    {
+        $str = '';
+        $childs = $this->getChild($myid);
+        if ($childs) {
+            foreach ($childs as $value) {
+                $id = $value['id'];
+                unset($value['child']);
+                $selected = in_array($id, (is_array($selectedids) ? $selectedids : explode(',', $selectedids))) ? 'selected' : '';
+                $disabled = in_array($id, (is_array($disabledids) ? $disabledids : explode(',', $disabledids))) ? 'disabled' : '';
+                $value = array_merge($value, array('selected' => $selected, 'disabled' => $disabled));
+                $value = array_combine(array_map(function ($k) {
+                    return '@' . $k;
+                }, array_keys($value)), $value);
+                $bakvalue = array_intersect_key($value, array_flip(['@url', '@caret', '@class']));
+                $value = array_diff_key($value, $bakvalue);
+                $nstr = strtr($itemtpl, $value);
+                $value = array_merge($value, $bakvalue);
+                $childdata = $this->getTreeMenu($id, $itemtpl, $selectedids, $disabledids, $wraptag, $wrapattr, $deeplevel + 1);
+                $childlist = $childdata ? "<{$wraptag} {$wrapattr}>" . $childdata . "</{$wraptag}>" : "";
+                $childlist = strtr($childlist, array('@class' => $childdata ? 'last' : ''));
+                $value = array(
+                    '@childlist' => $childlist,
+                    '@url'       => $childdata || !isset($value['@url']) ? "javascript:;" : $value['@url'],
+                    '@addtabs'   => $childdata || !isset($value['@url']) ? "" : (stripos($value['@url'], "?") !== false ? "&" : "?") . "ref=addtabs",
+                    '@caret'     => ($childdata && (!isset($value['@badge']) || !$value['@badge']) ? '<i class="fa fa-angle-left"></i>' : ''),
+                    '@badge'     => isset($value['@badge']) ? $value['@badge'] : '',
+                    '@class'     => ($selected ? ' active' : '') . ($disabled ? ' disabled' : '') . ($childdata ? ' treeview' . (config('fastadmin.show_submenu') ? ' treeview-open' : '') : ''),
+                );
+                $str .= strtr($nstr, $value);
+            }
+        }
+        return $str;
+    }
+
+    /**
+     * 特殊
+     * @param integer $myid        要查询的ID
+     * @param string  $itemtpl1    第一种HTML代码方式
+     * @param string  $itemtpl2    第二种HTML代码方式
+     * @param mixed   $selectedids 默认选中
+     * @param mixed   $disabledids 禁用
+     * @param string  $itemprefix  前缀
+     * @return string
+     */
+    public function getTreeSpecial($myid, $itemtpl1, $itemtpl2, $selectedids = 0, $disabledids = 0, $itemprefix = '')
+    {
+        $ret = '';
+        $number = 1;
+        $childs = $this->getChild($myid);
+        if ($childs) {
+            $total = count($childs);
+            foreach ($childs as $id => $value) {
+                $j = $k = '';
+                if ($number == $total) {
+                    $j .= $this->icon[2];
+                    $k = $itemprefix ? $this->nbsp : '';
+                } else {
+                    $j .= $this->icon[1];
+                    $k = $itemprefix ? $this->icon[0] : '';
+                }
+                $spacer = $itemprefix ? $itemprefix . $j : '';
+                $selected = $selectedids && in_array($id, (is_array($selectedids) ? $selectedids : explode(',', $selectedids))) ? 'selected' : '';
+                $disabled = $disabledids && in_array($id, (is_array($disabledids) ? $disabledids : explode(',', $disabledids))) ? 'disabled' : '';
+                $value = array_merge($value, array('selected' => $selected, 'disabled' => $disabled, 'spacer' => $spacer));
+                $value = array_combine(array_map(function ($k) {
+                    return '@' . $k;
+                }, array_keys($value)), $value);
+                $nstr = strtr(!isset($value['@disabled']) || !$value['@disabled'] ? $itemtpl1 : $itemtpl2, $value);
+
+                $ret .= $nstr;
+                $ret .= $this->getTreeSpecial($id, $itemtpl1, $itemtpl2, $selectedids, $disabledids, $itemprefix . $k . $this->nbsp);
+                $number++;
+            }
+        }
+        return $ret;
+    }
+
+    /**
+     *
+     * 获取树状数组
+     * @param string $myid       要查询的ID
+     * @param string $itemprefix 前缀
+     * @return array
+     */
+    public function getTreeArray($myid, $itemprefix = '')
+    {
+        $childs = $this->getChild($myid);
+        $n = 0;
+        $data = [];
+        $number = 1;
+        if ($childs) {
+            $total = count($childs);
+            foreach ($childs as $id => $value) {
+                $j = $k = '';
+                if ($number == $total) {
+                    $j .= $this->icon[2];
+                    $k = $itemprefix ? $this->nbsp : '';
+                } else {
+                    $j .= $this->icon[1];
+                    $k = $itemprefix ? $this->icon[0] : '';
+                }
+                $spacer = $itemprefix ? $itemprefix . $j : '';
+                $value['spacer'] = $spacer;
+                $data[$n] = $value;
+                $data[$n]['childlist'] = $this->getTreeArray($id, $itemprefix . $k . $this->nbsp);
+                $n++;
+                $number++;
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * 将getTreeArray的结果返回为二维数组
+     * @param array  $data
+     * @param string $field
+     * @return array
+     */
+    public function getTreeList($data = [], $field = 'name')
+    {
+        $arr = [];
+        foreach ($data as $k => $v) {
+            $childlist = isset($v['childlist']) ? $v['childlist'] : [];
+            unset($v['childlist']);
+            $v[$field] = $v['spacer'] . ' ' . $v[$field];
+            $v['haschild'] = $childlist ? 1 : 0;
+            if ($v['id']) {
+                $arr[] = $v;
+            }
+            if ($childlist) {
+                $arr = array_merge($arr, $this->getTreeList($childlist, $field));
+            }
+        }
+        return $arr;
+    }
+}

+ 1 - 1
application/extra/site.php

@@ -4,7 +4,7 @@ return array (
   'name' => '生猪屠宰溯源平台',
   'beian' => '皖ICP备2022001240号',
   'cdnurl' => '',
-  'version' => '1.0.5',
+  'version' => '1.0.6',
   'timezone' => 'Asia/Shanghai',
   'forbiddenip' => '',
   'languages' => 

+ 6 - 3
application/index/controller/User.php

@@ -230,9 +230,9 @@ class User extends Frontend
             $renewpassword = $this->request->post("renewpassword");
             $token = $this->request->post('__token__');
             $rule = [
-                'oldpassword'   => 'require|length:6,30',
-                'newpassword'   => 'require|length:6,30',
-                'renewpassword' => 'require|length:6,30|confirm:newpassword',
+                'oldpassword'   => 'require|regex:\S{6,30}',
+                'newpassword'   => 'require|regex:\S{6,30}',
+                'renewpassword' => 'require|regex:\S{6,30}|confirm:newpassword',
                 '__token__'     => 'token',
             ];
 
@@ -328,6 +328,9 @@ class User extends Frontend
 
             return json($result);
         }
+        $mimetype = $this->request->get('mimetype', '');
+        $mimetype = substr($mimetype, -1) === '/' ? $mimetype . '*' : $mimetype;
+        $this->view->assign('mimetype', $mimetype);
         $this->view->assign("mimetypeList", \app\common\model\Attachment::getMimetypeList());
         return $this->view->fetch();
     }

+ 1 - 0
application/index/lang/zh-cn/user.php

@@ -23,6 +23,7 @@ return [
     'Email active successful'                    => '邮箱激活成功',
     'Username can not be empty'                  => '用户名不能为空',
     'Username must be 3 to 30 characters'        => '用户名必须3-30个字符',
+    'Username must be 6 to 30 characters'        => '用户名必须3-30个字符',
     'Account must be 3 to 50 characters'         => '账户必须3-50个字符',
     'Password can not be empty'                  => '密码不能为空',
     'Password must be 6 to 30 characters'        => '密码必须6-30个字符',

+ 1 - 1
application/index/view/user/attachment.html

@@ -45,7 +45,7 @@
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
                         <a href="javascript:;" class="btn btn-primary btn-refresh" title="刷新"><i class="fa fa-refresh"></i> </a>
-                        <span><button type="button" id="faupload-image" class="btn btn-success faupload" data-mimetype="{$Think.get.mimetype|default=''|htmlentities}" data-multiple="true"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                        <span><button type="button" id="faupload-image" class="btn btn-success faupload" data-mimetype="{$mimetype|default=''|htmlentities}" data-multiple="true"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                         {if request()->get('multiple') == 'true'}
                         <a class="btn btn-danger btn-choose-multi"><i class="fa fa-check"></i> {:__('Choose')}</a>
                         {/if}

+ 3 - 3
application/index/view/user/changepwd.html

@@ -12,19 +12,19 @@
                         <div class="form-group">
                             <label for="oldpassword" class="control-label col-xs-12 col-sm-2">{:__('Old password')}:</label>
                             <div class="col-xs-12 col-sm-4">
-                                <input type="password" class="form-control" id="oldpassword" name="oldpassword" value="" data-rule="required" placeholder="{:__('Old password')}">
+                                <input type="password" class="form-control" id="oldpassword" name="oldpassword" value="" data-rule="required;password" placeholder="{:__('Old password')}">
                             </div>
                         </div>
                         <div class="form-group">
                             <label for="newpassword" class="control-label col-xs-12 col-sm-2">{:__('New password')}:</label>
                             <div class="col-xs-12 col-sm-4">
-                                <input type="password" class="form-control" id="newpassword" name="newpassword" value="" data-rule="required" placeholder="{:__('New password')}" />
+                                <input type="password" class="form-control" id="newpassword" name="newpassword" value="" data-rule="required;password" placeholder="{:__('New password')}" />
                             </div>
                         </div>
                         <div class="form-group">
                             <label for="renewpassword" class="control-label col-xs-12 col-sm-2">{:__('Renew password')}:</label>
                             <div class="col-xs-12 col-sm-4">
-                                <input type="password" class="form-control" id="renewpassword" name="renewpassword" value="" data-rule="required" placeholder="{:__('Renew password')}" />
+                                <input type="password" class="form-control" id="renewpassword" name="renewpassword" value="" data-rule="required;password" placeholder="{:__('Renew password')}" />
                             </div>
                         </div>
 

+ 783 - 0
application/public/assets/js/backend/addon.js

@@ -0,0 +1,783 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function ($, undefined, Backend, Table, Form, Template) {
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: Config.api_url ? Config.api_url + '/addon/index' : "addon/downloaded",
+                    add_url: '',
+                    edit_url: '',
+                    del_url: '',
+                    multi_url: ''
+                }
+            });
+
+            var table = $("#table");
+
+            // 弹窗自适应宽高
+            var area = Fast.config.openArea != undefined ? Fast.config.openArea : [$(window).width() > 800 ? '800px' : '95%', $(window).height() > 600 ? '600px' : '95%'];
+
+            var switch_local = function () {
+                if ($(".btn-switch.active").data("type") != "local") {
+                    Layer.confirm(__('Store not available tips'), {
+                        title: __('Warmtips'),
+                        btn: [__('Switch to the local'), __('Try to reload')]
+                    }, function (index) {
+                        layer.close(index);
+                        $(".panel .nav-tabs").hide();
+                        $(".toolbar > *:not(:first)").hide();
+                        $(".btn-switch[data-type='local']").trigger("click");
+                    }, function (index) {
+                        layer.close(index);
+                        table.bootstrapTable('refresh');
+                    });
+                    return false;
+                }
+            };
+            table.on('load-success.bs.table', function (e, json) {
+                if (json && typeof json.category != 'undefined' && $(".nav-category li").size() == 2) {
+                    $.each(json.category, function (i, j) {
+                        $("<li><a href='javascript:;' data-id='" + j.id + "'>" + j.name + "</a></li>").insertBefore($(".nav-category li:last"));
+                    });
+                }
+                if (typeof json.rows === 'undefined' && typeof json.code != 'undefined') {
+                    switch_local();
+                }
+            });
+            table.on('load-error.bs.table', function (e, status, res) {
+                console.log(e, status, res);
+                switch_local();
+            });
+            table.on('post-body.bs.table', function (e, settings, json, xhr) {
+                var parenttable = table.closest('.bootstrap-table');
+                var d = $(".fixed-table-toolbar", parenttable).find(".search input");
+                d.off("keyup drop blur");
+                d.on("keyup", function (e) {
+                    if (e.keyCode == 13) {
+                        var that = this;
+                        var options = table.bootstrapTable('getOptions');
+                        var queryParams = options.queryParams;
+                        options.pageNumber = 1;
+                        options.queryParams = function (params) {
+                            var params = queryParams(params);
+                            params.search = $(that).val();
+                            return params;
+                        };
+                        table.bootstrapTable('refresh', {});
+                    }
+                });
+            });
+
+            //当表格分页变更时
+            table.on('page-change.bs.table', function (e, page, pagesize) {
+                if (!isNaN(pagesize)) {
+                    localStorage.setItem("pagesize-addon", pagesize);
+                }
+            });
+
+            Template.helper("Moment", Moment);
+            Template.helper("addons", Config['addons']);
+
+            $("#faupload-addon").data("params", function () {
+                var userinfo = Controller.api.userinfo.get();
+                return {
+                    uid: userinfo ? userinfo.id : '',
+                    token: userinfo ? userinfo.token : '',
+                    version: Config.faversion
+                };
+            });
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                queryParams: function (params) {
+                    var userinfo = Controller.api.userinfo.get();
+                    $.extend(params, {
+                        uid: userinfo ? userinfo.id : '',
+                        token: userinfo ? userinfo.token : '',
+                        domain: Config.domain,
+                        version: Config.faversion
+                    });
+                    return params;
+                },
+                columns: [
+                    [
+                        {field: 'id', title: 'ID', operate: false, visible: false},
+                        {
+                            field: 'home',
+                            title: __('Index'),
+                            width: '50px',
+                            formatter: Controller.api.formatter.home
+                        },
+                        {field: 'name', title: __('Name'), operate: false, visible: false, width: '120px'},
+                        {
+                            field: 'title',
+                            title: __('Title'),
+                            operate: 'LIKE',
+                            align: 'left',
+                            formatter: Controller.api.formatter.title
+                        },
+                        {field: 'intro', title: __('Intro'), operate: 'LIKE', align: 'left', class: 'visible-lg'},
+                        {
+                            field: 'author',
+                            title: __('Author'),
+                            operate: 'LIKE',
+                            width: '100px',
+                            formatter: Controller.api.formatter.author
+                        },
+                        {
+                            field: 'price',
+                            title: __('Price'),
+                            operate: 'LIKE',
+                            width: '100px',
+                            align: 'center',
+                            formatter: Controller.api.formatter.price
+                        },
+                        {
+                            field: 'downloads',
+                            title: __('Downloads'),
+                            operate: 'LIKE',
+                            width: '80px',
+                            align: 'center',
+                            formatter: Controller.api.formatter.downloads
+                        },
+                        {
+                            field: 'version',
+                            title: __('Version'),
+                            operate: 'LIKE',
+                            width: '80px',
+                            align: 'center',
+                            formatter: Controller.api.formatter.version
+                        },
+                        {
+                            field: 'toggle',
+                            title: __('Status'),
+                            width: '80px',
+                            formatter: Controller.api.formatter.toggle
+                        },
+                        {
+                            field: 'id',
+                            title: __('Operate'),
+                            table: table,
+                            formatter: Controller.api.formatter.operate,
+                            align: 'right'
+                        },
+                    ]
+                ],
+                responseHandler: function (res) {
+                    $.each(res.rows, function (i, j) {
+                        j.addon = typeof Config.addons[j.name] != 'undefined' ? Config.addons[j.name] : null;
+                    });
+                    return res;
+                },
+                dataType: 'jsonp',
+                templateView: false,
+                clickToSelect: false,
+                search: true,
+                showColumns: false,
+                showToggle: false,
+                showExport: false,
+                showSearch: false,
+                commonSearch: true,
+                searchFormVisible: true,
+                searchFormTemplate: 'searchformtpl',
+                pageSize: localStorage.getItem('pagesize-addon') || 50,
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+
+            // 离线安装
+            require(['upload'], function (Upload) {
+                Upload.api.upload("#faupload-addon", function (data, ret) {
+                    Config['addons'][data.addon.name] = data.addon;
+                    var addon = data.addon;
+                    var testdata = data.addon.testdata;
+                    operate(data.addon.name, 'enable', false, function (data, ret) {
+                        Layer.alert(__('Offline installed tips') + (testdata ? __('Testdata tips') : ""), {
+                            btn: testdata ? [__('Import testdata'), __('Skip testdata')] : [__('OK')],
+                            title: __('Warning'),
+                            yes: function (index) {
+                                if (testdata) {
+                                    Fast.api.ajax({
+                                        url: 'addon/testdata',
+                                        data: {
+                                            name: addon.name,
+                                            version: addon.version,
+                                            faversion: Config.faversion
+                                        }
+                                    }, function (data, ret) {
+                                        Layer.close(index);
+                                    });
+                                } else {
+                                    Layer.close(index);
+                                }
+                            },
+                            icon: 1
+                        });
+                    });
+                    return false;
+                }, function (data, ret) {
+                    if (ret.msg && ret.msg.match(/(login|登录)/g)) {
+                        return Layer.alert(ret.msg, {
+                            title: __('Warning'),
+                            btn: [__('Login now')],
+                            yes: function (index, layero) {
+                                $(".btn-userinfo").trigger("click");
+                            }
+                        });
+                    }
+                });
+
+                // 检测是否登录
+                $(document).on("mousedown", "#faupload-addon", function (e) {
+                    var userinfo = Controller.api.userinfo.get();
+                    var uid = userinfo ? userinfo.id : 0;
+
+                    if (parseInt(uid) === 0) {
+                        $(".btn-userinfo").trigger("click");
+                        return false;
+                    }
+                });
+            });
+
+            // 查看插件首页
+            $(document).on("click", ".btn-addonindex", function () {
+                if ($(this).attr("href") == 'javascript:;') {
+                    Layer.msg(__('Not installed tips'), {icon: 7});
+                } else if ($(this).closest(".operate").find("a.btn-enable").size() > 0) {
+                    Layer.msg(__('Not enabled tips'), {icon: 7});
+                    return false;
+                }
+            });
+
+            // 切换
+            $(document).on("click", ".btn-switch", function () {
+                $(".btn-switch").removeClass("active");
+                $(this).addClass("active");
+                $("form.form-commonsearch input[name='type']").val($(this).data("type"));
+                var method = $(this).data("type") == 'local' ? 'hideColumn' : 'showColumn';
+                table.bootstrapTable(method, 'price');
+                table.bootstrapTable(method, 'downloads');
+                table.bootstrapTable('refresh', {url: ($(this).data("url") ? $(this).data("url") : $.fn.bootstrapTable.defaults.extend.index_url), pageNumber: 1});
+                return false;
+            });
+
+            // 切换分类
+            $(document).on("click", ".nav-category li a", function () {
+                $(".nav-category li").removeClass("active");
+                $(this).parent().addClass("active");
+                $("form.form-commonsearch input[name='category_id']").val($(this).data("id"));
+                table.bootstrapTable('refresh', {url: $(this).data("url"), pageNumber: 1});
+                return false;
+            });
+            var tables = [];
+            $(document).on("click", "#droptables", function () {
+                if ($(this).prop("checked")) {
+                    Fast.api.ajax({
+                        url: "addon/get_table_list",
+                        async: false,
+                        data: {name: $(this).data("name")}
+                    }, function (data) {
+                        tables = data.tables;
+                        return false;
+                    });
+                    var html;
+                    html = tables.length > 0 ? '<div class="alert alert-warning-light droptablestips" style="max-width:480px;max-height:300px;overflow-y: auto;">' + __('The following data tables will be deleted') + ':<br>' + tables.join("<br>") + '</div>'
+                        : '<div class="alert alert-warning-light droptablestips">' + __('The Addon did not create a data table') + '</div>';
+                    $(html).insertAfter($(this).closest("p"));
+                } else {
+                    $(".droptablestips").remove();
+                }
+                $(window).resize();
+            });
+
+            // 会员信息
+            $(document).on("click", ".btn-userinfo", function (e, name, version) {
+                var that = this;
+                var area = [$(window).width() > 800 ? '500px' : '95%', $(window).height() > 600 ? '400px' : '95%'];
+                var userinfo = Controller.api.userinfo.get();
+                if (!userinfo) {
+                    Layer.open({
+                        content: Template("logintpl", {}),
+                        zIndex: 99,
+                        area: area,
+                        title: __('Login FastAdmin'),
+                        resize: false,
+                        btn: [__('Login'), __('Register')],
+                        yes: function (index, layero) {
+                            Fast.api.ajax({
+                                url: Config.api_url + '/user/login',
+                                type: 'post',
+                                data: {
+                                    account: $("#inputAccount", layero).val(),
+                                    password: $("#inputPassword", layero).val(),
+                                    version: Config.faversion,
+                                }
+                            }, function (data, ret) {
+                                Controller.api.userinfo.set(data);
+                                Layer.closeAll();
+                                Layer.alert(ret.msg, {title: __('Warning'), icon: 1});
+                                return false;
+                            }, function (data, ret) {
+                            });
+                        },
+                        btn2: function () {
+                            return false;
+                        },
+                        success: function (layero, index) {
+                            this.checkEnterKey = function (event) {
+                                if (event.keyCode === 13) {
+                                    $(".layui-layer-btn0").trigger("click");
+                                    return false;
+                                }
+                            };
+                            $(document).on('keydown', this.checkEnterKey);
+                            $(".layui-layer-btn1", layero).prop("href", "https://www.fastadmin.net/user/register.html").prop("target", "_blank");
+                        },
+                        end: function () {
+                            $(document).off('keydown', this.checkEnterKey);
+                        }
+                    });
+                } else {
+                    Fast.api.ajax({
+                        url: Config.api_url + '/user/index',
+                        data: {
+                            uid: userinfo.id,
+                            token: userinfo.token,
+                            version: Config.faversion,
+                        }
+                    }, function (data) {
+                        Layer.open({
+                            content: Template("userinfotpl", userinfo),
+                            area: area,
+                            title: __('Userinfo'),
+                            resize: false,
+                            btn: [__('Logout'), __('Close')],
+                            yes: function () {
+                                Fast.api.ajax({
+                                    url: Config.api_url + '/user/logout',
+                                    data: {uid: userinfo.id, token: userinfo.token, version: Config.faversion}
+                                }, function (data, ret) {
+                                    Controller.api.userinfo.set(null);
+                                    Layer.closeAll();
+                                    Layer.alert(ret.msg, {title: __('Warning'), icon: 0});
+                                }, function (data, ret) {
+                                    Controller.api.userinfo.set(null);
+                                    Layer.closeAll();
+                                    Layer.alert(ret.msg, {title: __('Warning'), icon: 0});
+                                });
+                            }
+                        });
+                        return false;
+                    }, function (data) {
+                        Controller.api.userinfo.set(null);
+                        $(that).trigger('click');
+                        return false;
+                    });
+
+                }
+            });
+
+            //刷新授权
+            $(document).on("click", ".btn-authorization", function () {
+                var userinfo = Controller.api.userinfo.get();
+                if (!userinfo) {
+                    $(".btn-userinfo").trigger("click");
+                    return false;
+                }
+                Layer.confirm(__('Are you sure you want to refresh authorization?'), {icon: 3, title: __('Warmtips')}, function () {
+                    Fast.api.ajax({
+                        url: 'addon/authorization',
+                        data: {
+                            uid: userinfo.id,
+                            token: userinfo.token
+                        }
+                    }, function (data, ret) {
+                        $(".btn-refresh").trigger("click");
+                        Layer.closeAll();
+                    });
+                });
+                return false;
+            });
+
+            var install = function (name, version, force) {
+                var userinfo = Controller.api.userinfo.get();
+                var uid = userinfo ? userinfo.id : 0;
+                var token = userinfo ? userinfo.token : '';
+                Fast.api.ajax({
+                    url: 'addon/install',
+                    data: {
+                        name: name,
+                        force: force ? 1 : 0,
+                        uid: uid,
+                        token: token,
+                        version: version,
+                        faversion: Config.faversion
+                    }
+                }, function (data, ret) {
+                    Layer.closeAll();
+                    Config['addons'][data.addon.name] = ret.data.addon;
+                    operate(data.addon.name, 'enable', false, function () {
+                        Layer.alert(__('Online installed tips') + (data.addon.testdata ? __('Testdata tips') : ""), {
+                            btn: data.addon.testdata ? [__('Import testdata'), __('Skip testdata')] : [__('OK')],
+                            title: __('Warning'),
+                            yes: function (index) {
+                                if (data.addon.testdata) {
+                                    Fast.api.ajax({
+                                        url: 'addon/testdata',
+                                        data: {
+                                            name: name,
+                                            uid: uid,
+                                            token: token,
+                                            version: version,
+                                            faversion: Config.faversion
+                                        }
+                                    }, function (data, ret) {
+                                        Layer.close(index);
+                                    });
+                                } else {
+                                    Layer.close(index);
+                                }
+                            },
+                            icon: 1
+                        });
+                        Controller.api.refresh(table, name);
+                    });
+                }, function (data, ret) {
+                    var area = Fast.config.openArea != undefined ? Fast.config.openArea : [$(window).width() > 650 ? '650px' : '95%', $(window).height() > 710 ? '710px' : '95%'];
+                    if (ret && ret.code === -2) {
+                        //如果登录已经超时,重新提醒登录
+                        if (uid && uid != ret.data.uid) {
+                            Controller.api.userinfo.set(null);
+                            $(".operate[data-name='" + name + "'] .btn-install").trigger("click");
+                            return;
+                        }
+                        top.Fast.api.open(ret.data.payurl, __('Pay now'), {
+                            area: area,
+                            end: function () {
+                                Fast.api.ajax({
+                                    url: 'addon/isbuy',
+                                    data: {
+                                        name: name,
+                                        force: force ? 1 : 0,
+                                        uid: uid,
+                                        token: token,
+                                        version: version,
+                                        faversion: Config.faversion
+                                    }
+                                }, function () {
+                                    top.Layer.alert(__('Pay successful tips'), {
+                                        btn: [__('Continue installation')],
+                                        title: __('Warning'),
+                                        icon: 1,
+                                        yes: function (index) {
+                                            top.Layer.close(index);
+                                            install(name, version);
+                                        }
+                                    });
+                                    return false;
+                                }, function () {
+                                    console.log(__('Canceled'));
+                                    return false;
+                                });
+                            }
+                        });
+                    } else if (ret && ret.code === -3) {
+                        //插件目录发现影响全局的文件
+                        Layer.open({
+                            content: Template("conflicttpl", ret.data),
+                            shade: 0.8,
+                            area: area,
+                            title: __('Warning'),
+                            btn: [__('Continue install'), __('Cancel')],
+                            end: function () {
+
+                            },
+                            yes: function () {
+                                install(name, version, true);
+                            }
+                        });
+
+                    } else {
+                        Layer.alert(ret.msg, {title: __('Warning'), icon: 0});
+                    }
+                    return false;
+                });
+            };
+
+            var uninstall = function (name, force, droptables) {
+                Fast.api.ajax({
+                    url: 'addon/uninstall',
+                    data: {name: name, force: force ? 1 : 0, droptables: droptables ? 1 : 0}
+                }, function (data, ret) {
+                    delete Config['addons'][name];
+                    Layer.closeAll();
+                    Controller.api.refresh(table, name);
+                }, function (data, ret) {
+                    if (ret && ret.code === -3) {
+                        //插件目录发现影响全局的文件
+                        Layer.open({
+                            content: Template("conflicttpl", ret.data),
+                            shade: 0.8,
+                            area: area,
+                            title: __('Warning'),
+                            btn: [__('Continue uninstall'), __('Cancel')],
+                            end: function () {
+
+                            },
+                            yes: function () {
+                                uninstall(name, true, droptables);
+                            }
+                        });
+
+                    } else {
+                        Layer.alert(ret.msg, {title: __('Warning'), icon: 0});
+                    }
+                    return false;
+                });
+            };
+
+            var operate = function (name, action, force, success) {
+                Fast.api.ajax({
+                    url: 'addon/state',
+                    data: {name: name, action: action, force: force ? 1 : 0}
+                }, function (data, ret) {
+                    var addon = Config['addons'][name];
+                    addon.state = action === 'enable' ? 1 : 0;
+                    Layer.closeAll();
+                    if (typeof success === 'function') {
+                        success(data, ret);
+                    }
+                    Controller.api.refresh(table, name);
+                }, function (data, ret) {
+                    if (ret && ret.code === -3) {
+                        //插件目录发现影响全局的文件
+                        Layer.open({
+                            content: Template("conflicttpl", ret.data),
+                            shade: 0.8,
+                            area: area,
+                            title: __('Warning'),
+                            btn: [__('Continue operate'), __('Cancel')],
+                            end: function () {
+
+                            },
+                            yes: function () {
+                                operate(name, action, true, success);
+                            }
+                        });
+
+                    } else {
+                        Layer.alert(ret.msg, {title: __('Warning'), icon: 0});
+                    }
+                    return false;
+                });
+            };
+
+            var upgrade = function (name, version) {
+                var userinfo = Controller.api.userinfo.get();
+                var uid = userinfo ? userinfo.id : 0;
+                var token = userinfo ? userinfo.token : '';
+                Fast.api.ajax({
+                    url: 'addon/upgrade',
+                    data: {name: name, uid: uid, token: token, version: version, faversion: Config.faversion}
+                }, function (data, ret) {
+                    Config['addons'][name] = data.addon;
+                    Layer.closeAll();
+                    Controller.api.refresh(table, name);
+                }, function (data, ret) {
+                    Layer.alert(ret.msg, {title: __('Warning')});
+                    return false;
+                });
+            };
+
+            // 点击安装
+            $(document).on("click", ".btn-install", function () {
+                var that = this;
+                var name = $(this).closest(".operate").data("name");
+                var version = $(this).data("version");
+
+                var userinfo = Controller.api.userinfo.get();
+                var uid = userinfo ? userinfo.id : 0;
+
+                if (parseInt(uid) === 0) {
+                    return Layer.alert(__('Not login tips'), {
+                        title: __('Warning'),
+                        btn: [__('Login now')],
+                        yes: function (index, layero) {
+                            $(".btn-userinfo").trigger("click", name, version);
+                        },
+                        btn2: function () {
+                            install(name, version, false);
+                        }
+                    });
+                }
+                install(name, version, false);
+            });
+
+            // 点击卸载
+            $(document).on("click", ".btn-uninstall", function () {
+                var name = $(this).closest(".operate").data('name');
+                if (Config['addons'][name].state == 1) {
+                    Layer.alert(__('Please disable the add before trying to uninstall'), {icon: 7});
+                    return false;
+                }
+                Template.helper("__", __);
+                Layer.confirm(Template("uninstalltpl", {addon: Config['addons'][name]}), {focusBtn: false}, function (index, layero) {
+                    uninstall(name, false, $("input[name='droptables']", layero).prop("checked"));
+                });
+            });
+
+            // 点击配置
+            $(document).on("click", ".btn-config", function () {
+                var name = $(this).closest(".operate").data("name");
+                Fast.api.open("addon/config?name=" + name, __('Setting'));
+            });
+
+            // 点击启用/禁用
+            $(document).on("click", ".btn-enable,.btn-disable", function () {
+                var name = $(this).data("name");
+                var action = $(this).data("action");
+                operate(name, action, false);
+            });
+
+            // 点击升级
+            $(document).on("click", ".btn-upgrade", function () {
+                var name = $(this).closest(".operate").data('name');
+                if (Config['addons'][name].state == 1) {
+                    Layer.alert(__('Please disable the add before trying to upgrade'), {icon: 7});
+                    return false;
+                }
+                var version = $(this).data("version");
+
+                Layer.confirm(__('Upgrade tips', Config['addons'][name].title), function (index, layero) {
+                    upgrade(name, version);
+                });
+            });
+
+            $(document).on("click", ".operate .btn-group .dropdown-toggle", function () {
+                $(this).closest(".btn-group").toggleClass("dropup", $(document).height() - $(this).offset().top <= 200);
+            });
+
+            $(document).on("click", ".view-screenshots", function () {
+                var row = Table.api.getrowbyindex(table, parseInt($(this).data("index")));
+                var data = [];
+                $.each(row.screenshots, function (i, j) {
+                    data.push({
+                        "src": j
+                    });
+                });
+                var json = {
+                    "title": row.title,
+                    "data": data
+                };
+                top.Layer.photos(top.JSON.parse(JSON.stringify({photos: json})));
+            });
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        config: function () {
+            $(document).on("click", ".nav-group li a[data-toggle='tab']", function () {
+                if ($(this).attr("href") == "#all") {
+                    $(".tab-pane").addClass("active in");
+                }
+                return;
+                var type = $(this).attr("href").substring(1);
+                if (type == 'all') {
+                    $(".table-config tr").show();
+                } else {
+                    $(".table-config tr").hide();
+                    $(".table-config tr[data-group='" + type + "']").show();
+                }
+            });
+
+            Controller.api.bindevent();
+        },
+        api: {
+            formatter: {
+                title: function (value, row, index) {
+                    if ($(".btn-switch.active").data("type") == "local") {
+                        // return value;
+                    }
+                    var title = '<a class="title" href="' + row.url + '" data-toggle="tooltip" title="' + __('View addon home page') + '" target="_blank">' + value + '</a>';
+                    if (row.screenshots && row.screenshots.length > 0) {
+                        title += ' <a href="javascript:;" data-index="' + index + '" class="view-screenshots text-success" title="' + __('View addon screenshots') + '" data-toggle="tooltip"><i class="fa fa-image"></i></a>';
+                    }
+                    return title;
+                },
+                operate: function (value, row, index) {
+                    return Template("operatetpl", {item: row, index: index});
+                },
+                toggle: function (value, row, index) {
+                    if (!row.addon) {
+                        return '';
+                    }
+                    return '<a href="javascript:;" data-toggle="tooltip" title="' + __('Click to toggle status') + '" class="btn btn-toggle btn-' + (row.addon.state == 1 ? "disable" : "enable") + '" data-action="' + (row.addon.state == 1 ? "disable" : "enable") + '" data-name="' + row.name + '"><i class="fa ' + (row.addon.state == 0 ? 'fa-toggle-on fa-rotate-180 text-gray' : 'fa-toggle-on text-success') + ' fa-2x"></i></a>';
+                },
+                author: function (value, row, index) {
+                    var url = 'javascript:';
+                    if (typeof row.homepage !== 'undefined') {
+                        url = row.homepage;
+                    } else if (typeof row.qq !== 'undefined' && row.qq) {
+                        url = 'https://wpa.qq.com/msgrd?v=3&uin=' + row.qq + '&site=fastadmin.net&menu=yes';
+                    }
+                    return '<a href="' + url + '" target="_blank" data-toggle="tooltip" class="text-primary">' + value + '</a>';
+                },
+                price: function (value, row, index) {
+                    if (isNaN(value)) {
+                        return value;
+                    }
+                    return parseFloat(value) == 0 ? '<span class="text-success">' + __('Free') + '</span>' : '<span class="text-danger">¥' + value + '</span>';
+                },
+                downloads: function (value, row, index) {
+                    return value;
+                },
+                version: function (value, row, index) {
+                    return row.addon && row.addon.version != row.version ? '<a href="' + row.url + '?version=' + row.version + '" target="_blank"><span class="releasetips text-primary" data-toggle="tooltip" title="' + __('New version tips', row.version) + '">' + row.addon.version + '<i></i></span></a>' : row.version;
+                },
+                home: function (value, row, index) {
+                    return row.addon && parseInt(row.addon.state) > 0 ? '<a href="' + row.addon.url + '" data-toggle="tooltip" title="' + __('View addon index page') + '" target="_blank"><i class="fa fa-home text-primary"></i></a>' : '<a href="javascript:;"><i class="fa fa-home text-gray"></i></a>';
+                },
+            },
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            },
+            userinfo: {
+                get: function () {
+                    var userinfo = localStorage.getItem("fastadmin_userinfo");
+                    return userinfo ? JSON.parse(userinfo) : null;
+                },
+                set: function (data) {
+                    if (data) {
+                        localStorage.setItem("fastadmin_userinfo", JSON.stringify(data));
+                    } else {
+                        localStorage.removeItem("fastadmin_userinfo");
+                    }
+                }
+            },
+            refresh: function (table, name) {
+                //刷新左侧边栏
+                Fast.api.refreshmenu();
+                //刷新插件JS缓存
+                Fast.api.ajax({url: require.toUrl('addons.js'), loading: false}, function () {
+                    return false;
+                }, function () {
+                    return false;
+                });
+
+                //刷新行数据
+                if ($(".operate[data-name='" + name + "']").length > 0) {
+                    var tr = $(".operate[data-name='" + name + "']").closest("tr[data-index]");
+                    var index = tr.data("index");
+                    var row = Table.api.getrowbyindex(table, index);
+                    row.addon = typeof Config['addons'][name] !== 'undefined' ? Config['addons'][name] : undefined;
+                    table.bootstrapTable("updateRow", {index: index, row: row});
+                } else if ($(".btn-switch.active").data("type") == "local") {
+                    $(".btn-refresh").trigger("click");
+                }
+            }
+        }
+    };
+    return Controller;
+});

+ 64 - 0
application/public/assets/js/backend/auth/adminlog.js

@@ -0,0 +1,64 @@
+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,
+                fixedColumns: true,
+                fixedRightNumber: 1,
+                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: '模糊搜索'},
+                        {field: 'url', title: __('Url'), formatter: Table.api.formatter.url},
+                        {field: 'ip', title: __('IP'), events: Table.api.events.ip, formatter: Table.api.formatter.search},
+                        {field: 'browser', title: __('Browser'), operate: false, formatter: Controller.api.formatter.browser},
+                        {field: 'createtime', title: __('Create time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true},
+                        {
+                            field: 'operate', title: __('Operate'), table: table,
+                            events: Table.api.events.operate,
+                            buttons: [{
+                                name: 'detail',
+                                text: __('Detail'),
+                                icon: 'fa fa-list',
+                                classname: 'btn btn-info btn-xs btn-detail btn-dialog',
+                                url: 'auth/adminlog/detail'
+                            }],
+                            formatter: Table.api.formatter.operate
+                        }
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            },
+            formatter: {
+                browser: function (value, row, index) {
+                    return '<a class="btn btn-xs btn-browser">' + row.useragent.split(" ")[0] + '</a>';
+                },
+            },
+        }
+    };
+    return Controller;
+});

+ 362 - 0
application/public/assets/js/fast.js

@@ -0,0 +1,362 @@
+define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang'], function ($, undefined, Toastr, Layer, Lang) {
+    var Fast = {
+        config: {
+            //toastr默认配置
+            toastr: {
+                "closeButton": true,
+                "debug": false,
+                "newestOnTop": false,
+                "progressBar": false,
+                "positionClass": "toast-top-center",
+                "preventDuplicates": false,
+                "onclick": null,
+                "showDuration": "300",
+                "hideDuration": "1000",
+                "timeOut": "5000",
+                "extendedTimeOut": "1000",
+                "showEasing": "swing",
+                "hideEasing": "linear",
+                "showMethod": "fadeIn",
+                "hideMethod": "fadeOut"
+            }
+        },
+        events: {
+            //请求成功的回调
+            onAjaxSuccess: function (ret, onAjaxSuccess) {
+                var data = typeof ret.data !== 'undefined' ? ret.data : null;
+                var msg = typeof ret.msg !== 'undefined' && ret.msg ? ret.msg : __('Operation completed');
+
+                if (typeof onAjaxSuccess === 'function') {
+                    var result = onAjaxSuccess.call(this, data, ret);
+                    if (result === false)
+                        return;
+                }
+                Toastr.success(msg);
+            },
+            //请求错误的回调
+            onAjaxError: function (ret, onAjaxError) {
+                var data = typeof ret.data !== 'undefined' ? ret.data : null;
+                if (typeof onAjaxError === 'function') {
+                    var result = onAjaxError.call(this, data, ret);
+                    if (result === false) {
+                        return;
+                    }
+                }
+                Toastr.error(ret.msg);
+            },
+            //服务器响应数据后
+            onAjaxResponse: function (response) {
+                try {
+                    var ret = typeof response === 'object' ? response : JSON.parse(response);
+                    if (!ret.hasOwnProperty('code')) {
+                        $.extend(ret, {code: -2, msg: response, data: null});
+                    }
+                } catch (e) {
+                    var ret = {code: -1, msg: e.message, data: null};
+                }
+                return ret;
+            }
+        },
+        api: {
+            //发送Ajax请求
+            ajax: function (options, success, error) {
+                options = typeof options === 'string' ? {url: options} : options;
+                var index;
+                if (typeof options.loading === 'undefined' || options.loading) {
+                    index = Layer.load(options.loading || 0);
+                }
+                options = $.extend({
+                    type: "POST",
+                    dataType: "json",
+                    xhrFields: {
+                        withCredentials: true
+                    },
+                    success: function (ret) {
+                        index && Layer.close(index);
+                        ret = Fast.events.onAjaxResponse(ret);
+                        if (ret.code === 1) {
+                            Fast.events.onAjaxSuccess(ret, success);
+                        } else {
+                            Fast.events.onAjaxError(ret, error);
+                        }
+                    },
+                    error: function (xhr) {
+                        index && Layer.close(index);
+                        var ret = {code: xhr.status, msg: xhr.statusText, data: null};
+                        Fast.events.onAjaxError(ret, error);
+                    }
+                }, options);
+                return $.ajax(options);
+            },
+            //修复URL
+            fixurl: function (url) {
+                if (url.substr(0, 1) !== "/") {
+                    var r = new RegExp('^(?:[a-z]+:)?//', 'i');
+                    if (!r.test(url)) {
+                        url = Config.moduleurl + "/" + url;
+                    }
+                } else if (url.substr(0, 8) === "/addons/") {
+                    url = Config.__PUBLIC__.replace(/(\/*$)/g, "") + url;
+                }
+                return url;
+            },
+            //获取修复后可访问的cdn链接
+            cdnurl: function (url, domain) {
+                var rule = new RegExp("^((?:[a-z]+:)?\\/\\/|data:image\\/)", "i");
+                var cdnurl = Config.upload.cdnurl;
+                url = rule.test(url) || (cdnurl && url.indexOf(cdnurl) === 0) ? url : cdnurl + url;
+                if (domain && !rule.test(url)) {
+                    domain = typeof domain === 'string' ? domain : location.origin;
+                    url = domain + url;
+                }
+                return url;
+            },
+            //查询Url参数
+            query: function (name, url) {
+                if (!url) {
+                    url = window.location.href;
+                }
+                name = name.replace(/[\[\]]/g, "\\$&");
+                var regex = new RegExp("[?&/]" + name + "([=/]([^&#/?]*)|&|#|$)"),
+                    results = regex.exec(url);
+                if (!results)
+                    return null;
+                if (!results[2])
+                    return '';
+                return decodeURIComponent(results[2].replace(/\+/g, " "));
+            },
+            //打开一个弹出窗口
+            open: function (url, title, options) {
+                title = options && options.title ? options.title : (title ? title : "");
+                url = Fast.api.fixurl(url);
+                url = url + (url.indexOf("?") > -1 ? "&" : "?") + "dialog=1";
+                var area = Fast.config.openArea != undefined ? Fast.config.openArea : [$(window).width() > 800 ? '800px' : '95%', $(window).height() > 600 ? '600px' : '95%'];
+                options = $.extend({
+                    type: 2,
+                    title: title,
+                    shadeClose: true,
+                    shade: false,
+                    maxmin: true,
+                    moveOut: true,
+                    area: area,
+                    content: url,
+                    zIndex: Layer.zIndex,
+                    success: function (layero, index) {
+                        var that = this;
+                        //存储callback事件
+                        $(layero).data("callback", that.callback);
+                        //$(layero).removeClass("layui-layer-border");
+                        Layer.setTop(layero);
+                        try {
+                            var frame = Layer.getChildFrame('html', index);
+                            var layerfooter = frame.find(".layer-footer");
+                            Fast.api.layerfooter(layero, index, that);
+
+                            //绑定事件
+                            if (layerfooter.size() > 0) {
+                                // 监听窗口内的元素及属性变化
+                                // Firefox和Chrome早期版本中带有前缀
+                                var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
+                                if (MutationObserver) {
+                                    // 选择目标节点
+                                    var target = layerfooter[0];
+                                    // 创建观察者对象
+                                    var observer = new MutationObserver(function (mutations) {
+                                        Fast.api.layerfooter(layero, index, that);
+                                        mutations.forEach(function (mutation) {
+                                        });
+                                    });
+                                    // 配置观察选项:
+                                    var config = {attributes: true, childList: true, characterData: true, subtree: true}
+                                    // 传入目标节点和观察选项
+                                    observer.observe(target, config);
+                                    // 随后,你还可以停止观察
+                                    // observer.disconnect();
+                                }
+                            }
+                        } catch (e) {
+
+                        }
+                        if ($(layero).height() > $(window).height()) {
+                            //当弹出窗口大于浏览器可视高度时,重定位
+                            Layer.style(index, {
+                                top: 0,
+                                height: $(window).height()
+                            });
+                        }
+                    }
+                }, options ? options : {});
+                if ($(window).width() < 480 || (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream && top.$(".tab-pane.active").size() > 0)) {
+                    if (top.$(".tab-pane.active").length > 0) {
+                        options.area = [top.$(".tab-pane.active").width() + "px", top.$(".tab-pane.active").height() + "px"];
+                        options.offset = [top.$(".tab-pane.active").scrollTop() + "px", "0px"];
+                    } else {
+                        options.area = [$(window).width() + "px", $(window).height() + "px"];
+                        options.offset = ["0px", "0px"];
+                    }
+                }
+                return Layer.open(options);
+            },
+            //关闭窗口并回传数据
+            close: function (data) {
+                var index = parent.Layer.getFrameIndex(window.name);
+                var callback = parent.$("#layui-layer" + index).data("callback");
+                //再执行关闭
+                parent.Layer.close(index);
+                //再调用回传函数
+                if (typeof callback === 'function') {
+                    callback.call(undefined, data);
+                }
+            },
+            layerfooter: function (layero, index, that) {
+                var frame = Layer.getChildFrame('html', index);
+                var layerfooter = frame.find(".layer-footer");
+                if (layerfooter.size() > 0) {
+                    $(".layui-layer-footer", layero).remove();
+                    var footer = $("<div />").addClass('layui-layer-btn layui-layer-footer');
+                    footer.html(layerfooter.html());
+                    if ($(".row", footer).size() === 0) {
+                        $(">", footer).wrapAll("<div class='row'></div>");
+                    }
+                    footer.insertAfter(layero.find('.layui-layer-content'));
+                    //绑定事件
+                    footer.on("click", ".btn", function () {
+                        if ($(this).hasClass("disabled") || $(this).parent().hasClass("disabled")) {
+                            return;
+                        }
+                        var index = footer.find('.btn').index(this);
+                        $(".btn:eq(" + index + ")", layerfooter).trigger("click");
+                    });
+
+                    var titHeight = layero.find('.layui-layer-title').outerHeight() || 0;
+                    var btnHeight = layero.find('.layui-layer-btn').outerHeight() || 0;
+                    //重设iframe高度
+                    $("iframe", layero).height(layero.height() - titHeight - btnHeight);
+                }
+                //修复iOS下弹出窗口的高度和iOS下iframe无法滚动的BUG
+                if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
+                    var titHeight = layero.find('.layui-layer-title').outerHeight() || 0;
+                    var btnHeight = layero.find('.layui-layer-btn').outerHeight() || 0;
+                    $("iframe", layero).parent().css("height", layero.height() - titHeight - btnHeight);
+                    $("iframe", layero).css("height", "100%");
+                }
+            },
+            success: function (options, callback) {
+                var type = typeof options === 'function';
+                if (type) {
+                    callback = options;
+                }
+                return Layer.msg(__('Operation completed'), $.extend({
+                    offset: 0, icon: 1
+                }, type ? {} : options), callback);
+            },
+            error: function (options, callback) {
+                var type = typeof options === 'function';
+                if (type) {
+                    callback = options;
+                }
+                return Layer.msg(__('Operation failed'), $.extend({
+                    offset: 0, icon: 2
+                }, type ? {} : options), callback);
+            },
+            msg: function (message, url) {
+                var callback = typeof url === 'function' ? url : function () {
+                    if (typeof url !== 'undefined' && url) {
+                        location.href = url;
+                    }
+                };
+                Layer.msg(message, {
+                    time: 2000
+                }, callback);
+            },
+            toastr: Toastr,
+            layer: Layer
+        },
+        lang: function () {
+            var args = arguments,
+                string = args[0],
+                i = 1;
+            string = string.toLowerCase();
+            //string = typeof Lang[string] != 'undefined' ? Lang[string] : string;
+            if (typeof Lang !== 'undefined' && typeof Lang[string] !== 'undefined') {
+                if (typeof Lang[string] == 'object')
+                    return Lang[string];
+                string = Lang[string];
+            } else if (string.indexOf('.') !== -1 && false) {
+                var arr = string.split('.');
+                var current = Lang[arr[0]];
+                for (var i = 1; i < arr.length; i++) {
+                    current = typeof current[arr[i]] != 'undefined' ? current[arr[i]] : '';
+                    if (typeof current != 'object')
+                        break;
+                }
+                if (typeof current == 'object')
+                    return current;
+                string = current;
+            } else {
+                string = args[0];
+            }
+            return string.replace(/%((%)|s|d)/g, function (m) {
+                // m is the matched format, e.g. %s, %d
+                var val = null;
+                if (m[2]) {
+                    val = m[2];
+                } else {
+                    val = args[i];
+                    // A switch statement so that the formatter can be extended. Default is %s
+                    switch (m) {
+                        case '%d':
+                            val = parseFloat(val);
+                            if (isNaN(val)) {
+                                val = 0;
+                            }
+                            break;
+                    }
+                    i++;
+                }
+                return val;
+            });
+        },
+        init: function () {
+            // 对相对地址进行处理
+            $.ajaxSetup({
+                beforeSend: function (xhr, setting) {
+                    setting.url = Fast.api.fixurl(setting.url);
+                }
+            });
+            Layer.config({
+                skin: 'layui-layer-fast'
+            });
+            // 绑定ESC关闭窗口事件
+            $(window).keyup(function (e) {
+                if (e.keyCode == 27) {
+                    if ($(".layui-layer").size() > 0) {
+                        var index = 0;
+                        $(".layui-layer").each(function () {
+                            index = Math.max(index, parseInt($(this).attr("times")));
+                        });
+                        if (index) {
+                            Layer.close(index);
+                        }
+                    }
+                }
+            });
+
+            //公共代码
+            //配置Toastr的参数
+            Toastr.options = Fast.config.toastr;
+        }
+    };
+    //将Layer暴露到全局中去
+    window.Layer = Layer;
+    //将Toastr暴露到全局中去
+    window.Toastr = Toastr;
+    //将语言方法暴露到全局中去
+    window.__ = Fast.lang;
+    //将Fast渲染至全局
+    window.Fast = Fast;
+
+    //默认初始化执行的代码
+    Fast.init();
+    return Fast;
+});

File diff ditekan karena terlalu besar
+ 1 - 0
application/public/assets/js/require-backend.min.js


+ 686 - 0
application/public/assets/js/require-form.js

@@ -0,0 +1,686 @@
+define(['jquery', 'bootstrap', 'upload', 'validator', 'validator-lang'], function ($, undefined, Upload, Validator, undefined) {
+    var Form = {
+        config: {
+            fieldlisttpl: '<dd class="form-inline"><input type="text" name="<%=name%>[<%=index%>][key]" class="form-control" value="<%=key%>" placeholder="<%=options.keyPlaceholder||\'\'%>" size="10" /> <input type="text" name="<%=name%>[<%=index%>][value]" class="form-control" value="<%=value%>" placeholder="<%=options.valuePlaceholder||\'\'%>" /> <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span></dd>'
+        },
+        events: {
+            validator: function (form, success, error, submit) {
+                if (!form.is("form"))
+                    return;
+                //绑定表单事件
+                form.validator($.extend({
+                    rules: {
+                        username: [/^\w{3,30}$/, __('Username must be 3 to 30 characters')],
+                        password: [/^[\S]{6,30}$/, __('Password must be 6 to 30 characters')]
+                    },
+                    validClass: 'has-success',
+                    invalidClass: 'has-error',
+                    bindClassTo: '.form-group',
+                    formClass: 'n-default n-bootstrap',
+                    msgClass: 'n-right',
+                    stopOnError: true,
+                    display: function (elem) {
+                        return $(elem).closest('.form-group').find(".control-label").text().replace(/\:/, '');
+                    },
+                    dataFilter: function (data) {
+                        if (data.code === 1) {
+                            return data.msg ? {"ok": data.msg} : '';
+                        } else {
+                            return data.msg;
+                        }
+                    },
+                    target: function (input) {
+                        var target = $(input).data("target");
+                        if (target && $(target).size() > 0) {
+                            return $(target);
+                        }
+                        var $formitem = $(input).closest('.form-group'),
+                            $msgbox = $formitem.find('span.msg-box');
+                        if (!$msgbox.length) {
+                            return [];
+                        }
+                        return $msgbox;
+                    },
+                    valid: function (ret) {
+                        var that = this, submitBtn = $(".layer-footer [type=submit]", form);
+                        that.holdSubmit(true);
+                        submitBtn.addClass("disabled");
+                        //验证通过提交表单
+                        var submitResult = Form.api.submit($(ret), function (data, ret) {
+                            that.holdSubmit(false);
+                            submitBtn.removeClass("disabled");
+                            if (false === $(this).triggerHandler("success.form", [data, ret])) {
+                                return false;
+                            }
+                            if (typeof success === 'function') {
+                                if (false === success.call($(this), data, ret)) {
+                                    return false;
+                                }
+                            }
+                            //提示及关闭当前窗口
+                            var msg = ret.hasOwnProperty("msg") && ret.msg !== "" ? ret.msg : __('Operation completed');
+                            parent.Toastr.success(msg);
+                            parent.$(".btn-refresh").trigger("click");
+                            var index = parent.Layer.getFrameIndex(window.name);
+                            parent.Layer.close(index);
+                            return false;
+                        }, function (data, ret) {
+                            that.holdSubmit(false);
+                            if (false === $(this).triggerHandler("error.form", [data, ret])) {
+                                return false;
+                            }
+                            submitBtn.removeClass("disabled");
+                            if (typeof error === 'function') {
+                                if (false === error.call($(this), data, ret)) {
+                                    return false;
+                                }
+                            }
+                        }, submit);
+                        //如果提交失败则释放锁定
+                        if (!submitResult) {
+                            that.holdSubmit(false);
+                            submitBtn.removeClass("disabled");
+                        }
+                        return false;
+                    }
+                }, form.data("validator-options") || {}));
+
+                //移除提交按钮的disabled类
+                $(".layer-footer [type=submit],.fixed-footer [type=submit],.normal-footer [type=submit]", form).removeClass("disabled");
+                //自定义关闭按钮事件
+                form.on("click", ".layer-close", function () {
+                    var index = parent.Layer.getFrameIndex(window.name);
+                    parent.Layer.close(index);
+                    return false;
+                });
+            },
+            selectpicker: function (form) {
+                //绑定select元素事件
+                if ($(".selectpicker", form).size() > 0) {
+                    require(['bootstrap-select', 'bootstrap-select-lang'], function () {
+                        $('.selectpicker', form).selectpicker();
+                        $(form).on("reset", function () {
+                            setTimeout(function () {
+                                $('.selectpicker').selectpicker('refresh').trigger("change");
+                            }, 1);
+                        });
+                    });
+                }
+            },
+            selectpage: function (form) {
+                //绑定selectpage元素事件
+                if ($(".selectpage", form).size() > 0) {
+                    require(['selectpage'], function () {
+                        $('.selectpage', form).selectPage({
+                            eAjaxSuccess: function (data) {
+                                data.list = typeof data.rows !== 'undefined' ? data.rows : (typeof data.list !== 'undefined' ? data.list : []);
+                                data.totalRow = typeof data.total !== 'undefined' ? data.total : (typeof data.totalRow !== 'undefined' ? data.totalRow : data.list.length);
+                                return data;
+                            }
+                        });
+                    });
+                    //给隐藏的元素添加上validate验证触发事件
+                    $(document).on("change", ".sp_hidden", function () {
+                        $(this).trigger("validate");
+                    });
+                    $(document).on("change", ".sp_input", function () {
+                        $(this).closest(".sp_container").find(".sp_hidden").trigger("change");
+                    });
+                    $(form).on("reset", function () {
+                        setTimeout(function () {
+                            $('.selectpage', form).selectPageClear();
+                        }, 1);
+                    });
+                }
+            },
+            cxselect: function (form) {
+                //绑定cxselect元素事件
+                if ($("[data-toggle='cxselect']", form).size() > 0) {
+                    require(['cxselect'], function () {
+                        $.cxSelect.defaults.jsonName = 'name';
+                        $.cxSelect.defaults.jsonValue = 'value';
+                        $.cxSelect.defaults.jsonSpace = 'data';
+                        $("[data-toggle='cxselect']", form).cxSelect();
+                    });
+                }
+            },
+            citypicker: function (form) {
+                //绑定城市远程插件
+                if ($("[data-toggle='city-picker']", form).size() > 0) {
+                    require(['citypicker'], function () {
+                        $(form).on("reset", function () {
+                            setTimeout(function () {
+                                $("[data-toggle='city-picker']").citypicker('refresh');
+                            }, 1);
+                        });
+                    });
+                }
+            },
+            datetimepicker: function (form) {
+                //绑定日期时间元素事件
+                if ($(".datetimepicker", form).size() > 0) {
+                    require(['bootstrap-datetimepicker'], function () {
+                        var options = {
+                            format: 'YYYY-MM-DD HH:mm:ss',
+                            icons: {
+                                time: 'fa fa-clock-o',
+                                date: 'fa fa-calendar',
+                                up: 'fa fa-chevron-up',
+                                down: 'fa fa-chevron-down',
+                                previous: 'fa fa-chevron-left',
+                                next: 'fa fa-chevron-right',
+                                today: 'fa fa-history',
+                                clear: 'fa fa-trash',
+                                close: 'fa fa-remove'
+                            },
+                            showTodayButton: true,
+                            showClose: true
+                        };
+                        $('.datetimepicker', form).parent().css('position', 'relative');
+                        $('.datetimepicker', form).datetimepicker(options).on('dp.change', function (e) {
+                            $(this, document).trigger("changed");
+                        });
+                    });
+                }
+            },
+            daterangepicker: function (form) {
+                //绑定日期时间元素事件
+                if ($(".datetimerange", form).size() > 0) {
+                    require(['bootstrap-daterangepicker'], function () {
+                        var ranges = {};
+                        ranges[__('Today')] = [Moment().startOf('day'), Moment().endOf('day')];
+                        ranges[__('Yesterday')] = [Moment().subtract(1, 'days').startOf('day'), Moment().subtract(1, 'days').endOf('day')];
+                        ranges[__('Last 7 Days')] = [Moment().subtract(6, 'days').startOf('day'), Moment().endOf('day')];
+                        ranges[__('Last 30 Days')] = [Moment().subtract(29, 'days').startOf('day'), Moment().endOf('day')];
+                        ranges[__('This Month')] = [Moment().startOf('month'), Moment().endOf('month')];
+                        ranges[__('Last Month')] = [Moment().subtract(1, 'month').startOf('month'), Moment().subtract(1, 'month').endOf('month')];
+                        var options = {
+                            timePicker: false,
+                            autoUpdateInput: false,
+                            timePickerSeconds: true,
+                            timePicker24Hour: true,
+                            autoApply: true,
+                            locale: {
+                                format: 'YYYY-MM-DD HH:mm:ss',
+                                customRangeLabel: __("Custom Range"),
+                                applyLabel: __("Apply"),
+                                cancelLabel: __("Clear"),
+                            },
+                            ranges: ranges,
+                        };
+                        var origincallback = function (start, end) {
+                            $(this.element).val(start.format(this.locale.format) + " - " + end.format(this.locale.format));
+                            $(this.element).trigger('blur');
+                        };
+                        $(".datetimerange", form).each(function () {
+                            var callback = typeof $(this).data('callback') == 'function' ? $(this).data('callback') : origincallback;
+                            $(this).on('apply.daterangepicker', function (ev, picker) {
+                                callback.call(picker, picker.startDate, picker.endDate);
+                            });
+                            $(this).on('cancel.daterangepicker', function (ev, picker) {
+                                $(this).val('').trigger('blur');
+                            });
+                            $(this).daterangepicker($.extend(true, options, $(this).data() || {}, $(this).data("daterangepicker-options") || {}));
+                        });
+                    });
+                }
+            },
+            /**
+             * 绑定上传事件
+             * @param form
+             * @deprecated Use faupload instead.
+             */
+            plupload: function (form) {
+                Form.events.faupload(form);
+            },
+            /**
+             * 绑定上传事件
+             * @param form
+             */
+            faupload: function (form) {
+                //绑定上传元素事件
+                if ($(".plupload,.faupload", form).size() > 0) {
+                    Upload.api.upload($(".plupload,.faupload", form));
+                }
+            },
+            faselect: function (form) {
+                //绑定fachoose选择附件事件
+                if ($(".faselect,.fachoose", form).size() > 0) {
+                    $(".faselect,.fachoose", form).off('click').on('click', function () {
+                        var that = this;
+                        var multiple = $(this).data("multiple") ? $(this).data("multiple") : false;
+                        var mimetype = $(this).data("mimetype") ? $(this).data("mimetype") : '';
+                        var admin_id = $(this).data("admin-id") ? $(this).data("admin-id") : '';
+                        var user_id = $(this).data("user-id") ? $(this).data("user-id") : '';
+                        mimetype = mimetype.replace(/\/\*/ig, '/');
+                        var url = $(this).data("url") ? $(this).data("url") : (typeof Backend !== 'undefined' ? "general/attachment/select" : "user/attachment");
+                        parent.Fast.api.open(url + "?element_id=" + $(this).attr("id") + "&multiple=" + multiple + "&mimetype=" + mimetype + "&admin_id=" + admin_id + "&user_id=" + user_id, __('Choose'), {
+                            callback: function (data) {
+                                var button = $("#" + $(that).attr("id"));
+                                var maxcount = $(button).data("maxcount");
+                                var input_id = $(button).data("input-id") ? $(button).data("input-id") : "";
+                                maxcount = typeof maxcount !== "undefined" ? maxcount : 0;
+                                if (input_id && data.multiple) {
+                                    var urlArr = [];
+                                    var inputObj = $("#" + input_id);
+                                    var value = $.trim(inputObj.val());
+                                    if (value !== "") {
+                                        urlArr.push(inputObj.val());
+                                    }
+                                    var nums = value === '' ? 0 : value.split(/\,/).length;
+                                    var files = data.url !== "" ? data.url.split(/\,/) : [];
+                                    $.each(files, function (i, j) {
+                                        var url = Config.upload.fullmode ? Fast.api.cdnurl(j) : j;
+                                        urlArr.push(url);
+                                    });
+                                    if (maxcount > 0) {
+                                        var remains = maxcount - nums;
+                                        if (files.length > remains) {
+                                            Toastr.error(__('You can choose up to %d file%s', remains));
+                                            return false;
+                                        }
+                                    }
+                                    var result = urlArr.join(",");
+                                    inputObj.val(result).trigger("change").trigger("validate");
+                                } else {
+                                    var url = Config.upload.fullmode ? Fast.api.cdnurl(data.url) : data.url;
+                                    $("#" + input_id).val(url).trigger("change").trigger("validate");
+                                }
+                            }
+                        });
+                        return false;
+                    });
+                }
+            },
+            fieldlist: function (form) {
+                //绑定fieldlist
+                if ($(".fieldlist", form).size() > 0) {
+                    require(['dragsort', 'template'], function (undefined, Template) {
+                        //刷新隐藏textarea的值
+                        var refresh = function (container) {
+                            var data = {};
+                            var name = container.data("name");
+                            var textarea = $("textarea[name='" + name + "']", form);
+                            var template = container.data("template");
+                            $.each($("input,select,textarea", container).serializeArray(), function (i, j) {
+                                var reg = /\[(\w+)\]\[(\w+)\]$/g;
+                                var match = reg.exec(j.name);
+                                if (!match)
+                                    return true;
+                                match[1] = "x" + parseInt(match[1]);
+                                if (typeof data[match[1]] == 'undefined') {
+                                    data[match[1]] = {};
+                                }
+                                data[match[1]][match[2]] = j.value;
+                            });
+                            var result = template ? [] : {};
+                            $.each(data, function (i, j) {
+                                if (j) {
+                                    if (!template) {
+                                        if (j.key != '') {
+                                            result[j.key] = j.value;
+                                        }
+                                    } else {
+                                        result.push(j);
+                                    }
+                                }
+                            });
+                            textarea.val(JSON.stringify(result));
+                        };
+                        //追加一行数据
+                        var append = function (container, row, initial) {
+                            var tagName = container.data("tag") || (container.is("table") ? "tr" : "dd");
+                            var index = container.data("index");
+                            var name = container.data("name");
+                            var template = container.data("template");
+                            var data = container.data();
+                            index = index ? parseInt(index) : 0;
+                            container.data("index", index + 1);
+                            row = row ? row : {};
+                            row = typeof row.key === 'undefined' || typeof row.value === 'undefined' ? {key: '', value: row} : row;
+                            var options = container.data("fieldlist-options") || {};
+                            var vars = {index: index, name: name, data: data, options: options, key: row.key, value: row.value, row: row.value};
+                            var html = template ? Template(template, vars) : Template.render(Form.config.fieldlisttpl, vars);
+                            var obj = $(html);
+                            if ((options.deleteBtn === false || options.removeBtn === false) && initial)
+                                obj.find(".btn-remove").remove();
+                            if (options.dragsortBtn === false && initial)
+                                obj.find(".btn-dragsort").remove();
+                            if ((options.readonlyKey === true || options.disableKey === true) && initial) {
+                                obj.find("input[name$='[key]']").prop("readonly", true);
+                            }
+                            obj.attr("fieldlist-item", true);
+                            obj.insertAfter($(tagName + "[fieldlist-item]", container).length > 0 ? $(tagName + "[fieldlist-item]:last", container) : $(tagName + ":first", container));
+                            if ($(".btn-append,.append", container).length > 0) {
+                                //兼容旧版本事件
+                                $(".btn-append,.append", container).trigger("fa.event.appendfieldlist", obj);
+                            } else {
+                                //新版本事件
+                                container.trigger("fa.event.appendfieldlist", obj);
+                            }
+                            return obj;
+                        };
+                        var fieldlist = $(".fieldlist", form);
+                        //监听文本框改变事件
+                        $(document).on('change keyup changed', ".fieldlist input,.fieldlist textarea,.fieldlist select", function () {
+                            var container = $(this).closest(".fieldlist");
+                            refresh(container);
+                        });
+                        //追加控制(点击按钮)
+                        fieldlist.on("click", ".btn-append,.append", function (e, row) {
+                            var container = $(this).closest(".fieldlist");
+                            append(container, row);
+                            // refresh(container);
+                        });
+                        //移除控制(点击按钮)
+                        fieldlist.on("click", ".btn-remove", function () {
+                            var container = $(this).closest(".fieldlist");
+                            var tagName = container.data("tag") || (container.is("table") ? "tr" : "dd");
+                            $(this).closest(tagName).remove();
+                            refresh(container);
+                        });
+                        //追加控制(通过事件)
+                        fieldlist.on("fa.event.appendtofieldlist", function (e, row) {
+                            var container = $(this);
+                            append(container, row);
+                            refresh(container);
+                        });
+                        //根据textarea内容重新渲染
+                        fieldlist.on("fa.event.refreshfieldlist", function () {
+                            var container = $(this);
+                            var textarea = $("textarea[name='" + container.data("name") + "']", form);
+                            //先清空已有的数据
+                            $("[fieldlist-item]", container).remove();
+                            var json = {};
+                            try {
+                                json = JSON.parse(textarea.val());
+                            } catch (e) {
+                            }
+                            $.each(json, function (i, j) {
+                                append(container, {key: i, value: j}, true);
+                            });
+                        });
+                        //拖拽排序
+                        fieldlist.each(function () {
+                            var container = $(this);
+                            var tagName = container.data("tag") || (container.is("table") ? "tr" : "dd");
+                            container.dragsort({
+                                itemSelector: tagName,
+                                dragSelector: ".btn-dragsort",
+                                dragEnd: function () {
+                                    refresh(container);
+                                },
+                                placeHolderTemplate: $("<" + tagName + "/>")
+                            });
+                            if (typeof container.data("options") === 'object' && container.data("options").appendBtn === false) {
+                                $(".btn-append,.append", container).hide();
+                            }
+                            $("textarea[name='" + container.data("name") + "']", form).on("fa.event.refreshfieldlist", function () {
+                                //兼容旧版本事件
+                                $(this).closest(".fieldlist").trigger("fa.event.refreshfieldlist");
+                            });
+                        });
+                        fieldlist.trigger("fa.event.refreshfieldlist");
+                    });
+                }
+            },
+            switcher: function (form) {
+                form.on("click", "[data-toggle='switcher']", function () {
+                    if ($(this).hasClass("disabled")) {
+                        return false;
+                    }
+                    var switcher = $.proxy(function () {
+                        var input = $(this).prev("input");
+                        input = $(this).data("input-id") ? $("#" + $(this).data("input-id")) : input;
+                        if (input.size() > 0) {
+                            var yes = $(this).data("yes");
+                            var no = $(this).data("no");
+                            if (input.val() == yes) {
+                                input.val(no);
+                                $("i", this).addClass("fa-flip-horizontal text-gray");
+                            } else {
+                                input.val(yes);
+                                $("i", this).removeClass("fa-flip-horizontal text-gray");
+                            }
+                            input.trigger('change');
+                        }
+                    }, this);
+                    if (typeof $(this).data("confirm") !== 'undefined') {
+                        Layer.confirm($(this).data("confirm"), function (index) {
+                            switcher();
+                            Layer.close(index);
+                        });
+                    } else {
+                        switcher();
+                    }
+
+                    return false;
+                });
+            },
+            bindevent: function (form) {
+
+            },
+            slider: function (form) {
+                if ($(".slider", form).size() > 0) {
+                    require(['bootstrap-slider'], function () {
+                        $('.slider').removeClass('hidden').css('width', function (index, value) {
+                            return $(this).parents('.form-control').width();
+                        }).slider().on('slide', function (ev) {
+                            var data = $(this).data();
+                            if (typeof data.unit !== 'undefined') {
+                                $(this).parents('.form-control').siblings('.value').text(ev.value + data.unit);
+                            }
+                        });
+                    });
+                }
+            },
+            tagsinput: function (form) {
+                if ($("[data-role='tagsinput']", form).size() > 0) {
+                    require(['tagsinput', 'autocomplete'], function () {
+                        $("[data-role='tagsinput']").tagsinput();
+                    });
+                }
+            },
+            autocomplete: function (form) {
+                if ($("[data-role='autocomplete']", form).size() > 0) {
+                    require(['autocomplete'], function () {
+                        $("[data-role='autocomplete']").autocomplete();
+                    });
+                }
+            },
+            favisible: function (form) {
+                if ($("[data-favisible]", form).length == 0) {
+                    return;
+                }
+                var checkCondition = function (condition) {
+                    var conditionArr = condition.split(/&&/);
+                    var success = 0;
+                    var baseregex = /^([a-z0-9\_]+)([>|<|=|\!]=?)(.*)$/i, strregex = /^('|")(.*)('|")$/, regregex = /^regex:(.*)$/;
+                    // @formatter:off
+                    var operator_result = {
+                        '>': function(a, b) { return a > b; },
+                        '>=': function(a, b) { return a >= b; },
+                        '<': function(a, b) { return a < b; },
+                        '<=': function(a, b) { return a <= b; },
+                        '==': function(a, b) { return a == b; },
+                        '!=': function(a, b) { return a != b; },
+                        'in': function(a, b) { return b.split(/\,/).indexOf(a) > -1; },
+                        'regex': function(a, b) {
+                            var regParts = b.match(/^\/(.*?)\/([gim]*)$/);
+                            var regexp = regParts ? new RegExp(regParts[1], regParts[2]) : new RegExp(b);
+                            return regexp.test(a);
+                        }
+                    };
+                    // @formatter:on
+                    var dataArr = form.serializeArray(), dataObj = {};
+                    $(dataArr).each(function (i, field) {
+                        dataObj[field.name] = field.value;
+                    });
+
+                    $.each(conditionArr, function (i, item) {
+                        var basematches = baseregex.exec(item);
+                        if (basematches) {
+                            var name = basematches[1], operator = basematches[2], value = basematches[3].toString();
+                            if (operator === '=') {
+                                var strmatches = strregex.exec(value);
+                                operator = strmatches ? '==' : 'in';
+                                value = strmatches ? strmatches[2] : value;
+                            }
+                            var regmatches = regregex.exec(value);
+                            if (regmatches) {
+                                operator = 'regex';
+                                value = regmatches[1];
+                            }
+                            var chkname = "row[" + name + "]";
+                            if (typeof dataObj[chkname] === 'undefined') {
+                                return false;
+                            }
+                            var objvalue = dataObj[chkname];
+                            if (['>', '>=', '<', '<='].indexOf(operator) > -1) {
+                                objvalue = parseFloat(objvalue);
+                                value = parseFloat(value);
+                            }
+                            var result = operator_result[operator](objvalue, value);
+                            success += (result ? 1 : 0);
+                        }
+                    });
+                    return success === conditionArr.length;
+                };
+                form.on("keyup change click configchange", "input,select", function () {
+                    $("[data-favisible][data-favisible!='']", form).each(function () {
+                        var visible = $(this).data("favisible");
+                        var groupArr = visible.split(/\|\|/);
+                        var success = 0;
+                        $.each(groupArr, function (i, j) {
+                            if (checkCondition(j)) {
+                                success++;
+                            }
+                        });
+                        if (success > 0) {
+                            $(this).removeClass("hidden");
+                        } else {
+                            $(this).addClass("hidden");
+                        }
+                    });
+                });
+
+                //追加上忽略元素
+                setTimeout(function () {
+                    form.data('validator').options.ignore += ((form.data('validator').options.ignore ? ',' : '') + '[data-favisible] :hidden,[data-favisible]:hidden');
+                }, 0);
+
+                $("input,select", form).trigger("configchange");
+            }
+        },
+        api: {
+            submit: function (form, success, error, submit) {
+                if (form.size() === 0) {
+                    Toastr.error("表单未初始化完成,无法提交");
+                    return false;
+                }
+                if (typeof submit === 'function') {
+                    if (false === submit.call(form, success, error)) {
+                        return false;
+                    }
+                }
+                var type = form.attr("method") ? form.attr("method").toUpperCase() : 'GET';
+                type = type && (type === 'GET' || type === 'POST') ? type : 'GET';
+                url = form.attr("action");
+                url = url ? url : location.href;
+                //修复当存在多选项元素时提交的BUG
+                var params = {};
+                var multipleList = $("[name$='[]']", form);
+                if (multipleList.size() > 0) {
+                    var postFields = form.serializeArray().map(function (obj) {
+                        return $(obj).prop("name");
+                    });
+                    $.each(multipleList, function (i, j) {
+                        if (postFields.indexOf($(this).prop("name")) < 0) {
+                            params[$(this).prop("name")] = '';
+                        }
+                    });
+                }
+                //调用Ajax请求方法
+                Fast.api.ajax({
+                    type: type,
+                    url: url,
+                    data: form.serialize() + (Object.keys(params).length > 0 ? '&' + $.param(params) : ''),
+                    dataType: 'json',
+                    complete: function (xhr) {
+                        var token = xhr.getResponseHeader('__token__');
+                        if (token) {
+                            $("input[name='__token__']").val(token);
+                        }
+                    }
+                }, function (data, ret) {
+                    $('.form-group', form).removeClass('has-feedback has-success has-error');
+                    if (data && typeof data === 'object') {
+                        //刷新客户端token
+                        if (typeof data.token !== 'undefined') {
+                            $("input[name='__token__']").val(data.token);
+                        }
+                        //调用客户端事件
+                        if (typeof data.callback !== 'undefined' && typeof data.callback === 'function') {
+                            data.callback.call(form, data);
+                        }
+                    }
+                    if (typeof success === 'function') {
+                        if (false === success.call(form, data, ret)) {
+                            return false;
+                        }
+                    }
+                }, function (data, ret) {
+                    if (data && typeof data === 'object' && typeof data.token !== 'undefined') {
+                        $("input[name='__token__']").val(data.token);
+                    }
+                    if (typeof error === 'function') {
+                        if (false === error.call(form, data, ret)) {
+                            return false;
+                        }
+                    }
+                });
+                return true;
+            },
+            bindevent: function (form, success, error, submit) {
+
+                form = typeof form === 'object' ? form : $(form);
+
+                var events = Form.events;
+
+                events.bindevent(form);
+
+                events.validator(form, success, error, submit);
+
+                events.selectpicker(form);
+
+                events.daterangepicker(form);
+
+                events.selectpage(form);
+
+                events.cxselect(form);
+
+                events.citypicker(form);
+
+                events.datetimepicker(form);
+
+                events.faupload(form);
+
+                events.faselect(form);
+
+                events.fieldlist(form);
+
+                events.slider(form);
+
+                events.switcher(form);
+
+                events.tagsinput(form);
+
+                events.autocomplete(form);
+
+                events.favisible(form);
+            },
+            custom: {}
+        },
+    };
+    return Form;
+});

File diff ditekan karena terlalu besar
+ 1 - 0
application/public/assets/js/require-frontend.min.js


+ 983 - 0
application/public/assets/js/require-table.js

@@ -0,0 +1,983 @@
+define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table', 'bootstrap-table-lang', 'bootstrap-table-export', 'bootstrap-table-commonsearch', 'bootstrap-table-template', 'bootstrap-table-jumpto', 'bootstrap-table-fixed-columns'], function ($, undefined, Moment) {
+    var Table = {
+        list: {},
+        // Bootstrap-table 基础配置
+        defaults: {
+            url: '',
+            sidePagination: 'server',
+            method: 'get', //请求方法
+            toolbar: ".toolbar", //工具栏
+            search: true, //是否启用快速搜索
+            cache: false,
+            commonSearch: true, //是否启用通用搜索
+            searchFormVisible: false, //是否始终显示搜索表单
+            titleForm: '', //为空则不显示标题,不定义默认显示:普通搜索
+            idTable: 'commonTable',
+            showExport: true,
+            exportDataType: "auto",
+            exportTypes: ['json', 'xml', 'csv', 'txt', 'doc', 'excel'],
+            exportOptions: {
+                fileName: 'export_' + Moment().format("YYYY-MM-DD"),
+                preventInjection: false,
+                mso: {
+                    onMsoNumberFormat: function (cell, row, col) {
+                        return !isNaN($(cell).text()) ? '\\@' : '';
+                    },
+                },
+                ignoreColumn: [0, 'operate'] //默认不导出第一列(checkbox)与操作(operate)列
+            },
+            pageSize: localStorage.getItem("pagesize") || 10,
+            pageList: [10, 15, 20, 25, 50, 'All'],
+            pagination: true,
+            clickToSelect: true, //是否启用点击选中
+            dblClickToEdit: true, //是否启用双击编辑
+            singleSelect: false, //是否启用单选
+            showRefresh: false,
+            showJumpto: true,
+            locale: Config.language == 'zh-cn' ? 'zh-CN' : 'en-US',
+            showToggle: true,
+            showColumns: true,
+            pk: 'id',
+            sortName: 'id',
+            sortOrder: 'desc',
+            paginationFirstText: __("First"),
+            paginationPreText: __("Previous"),
+            paginationNextText: __("Next"),
+            paginationLastText: __("Last"),
+            cardView: false, //卡片视图
+            iosCardView: true, //ios卡片视图
+            checkOnInit: true, //是否在初始化时判断
+            escape: true, //是否对内容进行转义
+            fixDropdownPosition: true, //是否修复下拉的定位
+            selectedIds: [],
+            selectedData: [],
+            extend: {
+                index_url: '',
+                add_url: '',
+                edit_url: '',
+                del_url: '',
+                import_url: '',
+                multi_url: '',
+                dragsort_url: 'ajax/weigh',
+            }
+        },
+        // Bootstrap-table 列配置
+        columnDefaults: {
+            align: 'center',
+            valign: 'middle',
+        },
+        config: {
+            checkboxtd: 'tbody>tr>td.bs-checkbox',
+            toolbar: '.toolbar',
+            refreshbtn: '.btn-refresh',
+            addbtn: '.btn-add',
+            editbtn: '.btn-edit',
+            delbtn: '.btn-del',
+            importbtn: '.btn-import',
+            multibtn: '.btn-multi',
+            disabledbtn: '.btn-disabled',
+            editonebtn: '.btn-editone',
+            restoreonebtn: '.btn-restoreone',
+            destroyonebtn: '.btn-destroyone',
+            restoreallbtn: '.btn-restoreall',
+            destroyallbtn: '.btn-destroyall',
+            dragsortfield: 'weigh',
+        },
+        button: {
+            edit: {
+                name: 'edit',
+                icon: 'fa fa-pencil',
+                title: __('Edit'),
+                extend: 'data-toggle="tooltip"',
+                classname: 'btn btn-xs btn-success btn-editone'
+            },
+            del: {
+                name: 'del',
+                icon: 'fa fa-trash',
+                title: __('Del'),
+                extend: 'data-toggle="tooltip"',
+                classname: 'btn btn-xs btn-danger btn-delone'
+            },
+            dragsort: {
+                name: 'dragsort',
+                icon: 'fa fa-arrows',
+                title: __('Drag to sort'),
+                extend: 'data-toggle="tooltip"',
+                classname: 'btn btn-xs btn-primary btn-dragsort'
+            }
+        },
+        api: {
+            init: function (defaults, columnDefaults, locales) {
+                defaults = defaults ? defaults : {};
+                columnDefaults = columnDefaults ? columnDefaults : {};
+                locales = locales ? locales : {};
+                $.fn.bootstrapTable.Constructor.prototype.getSelectItem = function () {
+                    return this.$selectItem;
+                };
+                // 写入bootstrap-table默认配置
+                $.extend(true, $.fn.bootstrapTable.defaults, Table.defaults, defaults);
+                // 写入bootstrap-table column配置
+                $.extend($.fn.bootstrapTable.columnDefaults, Table.columnDefaults, columnDefaults);
+                // 写入bootstrap-table locale配置
+                $.extend($.fn.bootstrapTable.locales[Table.defaults.locale], {
+                    formatCommonSearch: function () {
+                        return __('Common search');
+                    },
+                    formatCommonSubmitButton: function () {
+                        return __('Submit');
+                    },
+                    formatCommonResetButton: function () {
+                        return __('Reset');
+                    },
+                    formatCommonCloseButton: function () {
+                        return __('Close');
+                    },
+                    formatCommonChoose: function () {
+                        return __('Choose');
+                    },
+                    formatJumpto: function () {
+                        return __('Go');
+                    }
+                }, locales);
+                // 如果是iOS设备则判断是否启用卡片视图
+                if ($.fn.bootstrapTable.defaults.iosCardView && navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
+                    Table.defaults.cardView = true;
+                    $.fn.bootstrapTable.defaults.cardView = true;
+                }
+                if (typeof defaults.exportTypes != 'undefined') {
+                    $.fn.bootstrapTable.defaults.exportTypes = defaults.exportTypes;
+                }
+            },
+            // 绑定事件
+            bindevent: function (table) {
+                //Bootstrap-table的父元素,包含table,toolbar,pagnation
+                var parenttable = table.closest('.bootstrap-table');
+                //Bootstrap-table配置
+                var options = table.bootstrapTable('getOptions');
+                //Bootstrap操作区
+                var toolbar = $(options.toolbar, parenttable);
+                //跨页提示按钮
+                var tipsBtn = $(".btn-selected-tips", parenttable);
+                if (tipsBtn.size() === 0) {
+                    tipsBtn = $('<a href="javascript:" class="btn btn-warning-light btn-selected-tips hide" data-animation="false" data-toggle="tooltip" data-title="' + __("Click to uncheck all") + '"><i class="fa fa-info-circle"></i> ' + __("Multiple selection mode: %s checked", "<b>0</b>") + '</a>').appendTo(toolbar);
+                }
+                //点击提示按钮
+                tipsBtn.off("click").on("click", function (e) {
+                    table.trigger("uncheckbox");
+                    table.bootstrapTable("refresh");
+                });
+                //当刷新表格时
+                table.on('uncheckbox', function (status, res, e) {
+                    options.selectedIds = [];
+                    options.selectedData = [];
+                    tipsBtn.tooltip('hide');
+                    tipsBtn.addClass('hide');
+                });
+                //表格加载出错时
+                table.on('load-error.bs.table', function (status, res, e) {
+                    if (e.status === 0) {
+                        return;
+                    }
+                    Toastr.error(__('Unknown data format'));
+                });
+                //当加载数据成功时
+                table.on('load-success.bs.table', function (e, data) {
+                    if (typeof data.rows === 'undefined' && typeof data.code != 'undefined') {
+                        Toastr.error(data.msg);
+                    }
+                });
+                //当刷新表格时
+                table.on('refresh.bs.table', function (e, settings, data) {
+                    $(Table.config.refreshbtn, toolbar).find(".fa").addClass("fa-spin");
+                });
+                //当表格分页变更时
+                table.on('page-change.bs.table', function (e, page, pagesize) {
+                    if (!isNaN(pagesize)) {
+                        localStorage.setItem("pagesize", pagesize);
+                    }
+                });
+                //当执行搜索时
+                table.on('search.bs.table common-search.bs.table', function (e, settings, data) {
+                    table.trigger("uncheckbox");
+                });
+                if (options.dblClickToEdit) {
+                    //当双击单元格时
+                    table.on('dbl-click-row.bs.table', function (e, row, element, field) {
+                        $(Table.config.editonebtn, element).trigger("click");
+                    });
+                }
+                //渲染内容前
+                table.on('pre-body.bs.table', function (e, data) {
+                    if (options.maintainSelected) {
+                        $.each(data, function (i, row) {
+                            row[options.stateField] = $.inArray(row[options.pk], options.selectedIds) > -1;
+                        });
+                    }
+                });
+                //当内容渲染完成后
+                table.on('post-body.bs.table', function (e, data) {
+                    $(Table.config.refreshbtn, toolbar).find(".fa").removeClass("fa-spin");
+                    if ($(Table.config.checkboxtd + ":first", table).find("input[type='checkbox'][data-index]").size() > 0) {
+                        // 拖拽选择,需要重新绑定事件
+                        require(['drag', 'drop'], function () {
+                            var checkboxtd = $(Table.config.checkboxtd, table);
+                            checkboxtd.drag("start", function (ev, dd) {
+                                return $('<div class="selection" />').css('opacity', .65).appendTo(document.body);
+                            }).drag(function (ev, dd) {
+                                $(dd.proxy).css({
+                                    top: Math.min(ev.pageY, dd.startY),
+                                    left: Math.min(ev.pageX, dd.startX),
+                                    height: Math.abs(ev.pageY - dd.startY),
+                                    width: Math.abs(ev.pageX - dd.startX)
+                                });
+                            }).drag("end", function (ev, dd) {
+                                $(dd.proxy).remove();
+                            });
+                            checkboxtd.drop("start", function () {
+                                Table.api.toggleattr(this);
+                            }).drop(function () {
+                                // Table.api.toggleattr(this);
+                            }).drop("end", function (e) {
+                                var that = this;
+                                setTimeout(function () {
+                                    if (e.type === 'mousemove') {
+                                        Table.api.toggleattr(that);
+                                    }
+                                }, 0);
+                            });
+                            $.drop({
+                                multi: true
+                            });
+                        });
+                    }
+                });
+                var exportDataType = options.exportDataType;
+                // 处理选中筛选框后按钮的状态统一变更
+                table.on('check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table post-body.bs.table', function (e) {
+                    var allIds = [];
+                    $.each(table.bootstrapTable("getData"), function (i, item) {
+                        allIds.push(typeof item[options.pk] != 'undefined' ? item[options.pk] : '');
+                    });
+                    var selectedIds = Table.api.selectedids(table, true),
+                        selectedData = Table.api.selecteddata(table, true);
+                    //开启分页checkbox分页记忆
+                    if (options.maintainSelected) {
+                        options.selectedIds = options.selectedIds.filter(function (element, index, self) {
+                            return $.inArray(element, allIds) === -1;
+                        }).concat(selectedIds);
+                        options.selectedData = options.selectedData.filter(function (element, index, self) {
+                            return $.inArray(element[options.pk], allIds) === -1;
+                        }).concat(selectedData);
+                        if (options.selectedIds.length > selectedIds.length) {
+                            $("b", tipsBtn).text(options.selectedIds.length);
+                            tipsBtn.removeClass('hide');
+                        } else {
+                            tipsBtn.addClass('hide');
+                        }
+                    } else {
+                        options.selectedIds = selectedIds;
+                        options.selectedData = selectedData;
+                    }
+                    //如果导出类型为auto时则自动判断
+                    if (exportDataType === 'auto') {
+                        options.exportDataType = selectedIds.length > 0 ? 'selected' : 'all';
+                    }
+                    $(Table.config.disabledbtn, toolbar).toggleClass('disabled', !options.selectedIds.length);
+                });
+                // 绑定TAB事件
+                $('.panel-heading [data-field] a[data-toggle="tab"]', table.closest(".panel-intro")).on('shown.bs.tab', function (e) {
+                    var field = $(this).closest("[data-field]").data("field");
+                    var value = $(this).data("value");
+                    var object = $("[name='" + field + "']", table.closest(".bootstrap-table").find(".commonsearch-table"));
+                    if (object.prop('tagName') == "SELECT") {
+                        $("option[value='" + value + "']", object).prop("selected", true);
+                    } else {
+                        object.val(value);
+                    }
+                    table.trigger("uncheckbox");
+                    table.bootstrapTable('refresh', {pageNumber: 1});
+                    return false;
+                });
+                // 修复重置事件
+                $("form", table.closest(".bootstrap-table").find(".commonsearch-table")).on('reset', function () {
+                    setTimeout(function () {
+                        // $('.panel-heading [data-field] li.active a[data-toggle="tab"]').trigger('shown.bs.tab');
+                    }, 0);
+                    $('.panel-heading [data-field] li', table.closest(".panel-intro")).removeClass('active');
+                    $('.panel-heading [data-field] li:first', table.closest(".panel-intro")).addClass('active');
+                });
+                // 刷新按钮事件
+                toolbar.on('click', Table.config.refreshbtn, function () {
+                    table.bootstrapTable('refresh');
+                });
+                // 添加按钮事件
+                toolbar.on('click', Table.config.addbtn, function () {
+                    var ids = Table.api.selectedids(table);
+                    var url = options.extend.add_url;
+                    if (url.indexOf("{ids}") !== -1) {
+                        url = Table.api.replaceurl(url, {ids: ids.length > 0 ? ids.join(",") : 0}, table);
+                    }
+                    Fast.api.open(url, $(this).data("original-title") || $(this).attr("title") || __('Add'), $(this).data() || {});
+                });
+                // 导入按钮事件
+                if ($(Table.config.importbtn, toolbar).size() > 0) {
+                    require(['upload'], function (Upload) {
+                        Upload.api.upload($(Table.config.importbtn, toolbar), function (data, ret) {
+                            Fast.api.ajax({
+                                url: options.extend.import_url,
+                                data: {file: data.url},
+                            }, function (data, ret) {
+                                table.trigger("uncheckbox");
+                                table.bootstrapTable('refresh');
+                            });
+                        });
+                    });
+                }
+                // 批量编辑按钮事件
+                toolbar.on('click', Table.config.editbtn, function () {
+                    var that = this;
+                    var ids = Table.api.selectedids(table);
+                    if (ids.length > 10) {
+                        return;
+                    }
+                    var title = $(that).data('title') || $(that).attr("title") || __('Edit');
+                    var data = $(that).data() || {};
+                    delete data.title;
+                    //循环弹出多个编辑框
+                    $.each(Table.api.selecteddata(table), function (index, row) {
+                        var url = options.extend.edit_url;
+                        row = $.extend({}, row ? row : {}, {ids: row[options.pk]});
+                        url = Table.api.replaceurl(url, row, table);
+                        Fast.api.open(url, typeof title === 'function' ? title.call(table, row) : title, data);
+                    });
+                });
+                //清空回收站
+                $(document).on('click', Table.config.destroyallbtn, function () {
+                    var that = this;
+                    Layer.confirm(__('Are you sure you want to truncate?'), function () {
+                        var url = $(that).data("url") ? $(that).data("url") : $(that).attr("href");
+                        Fast.api.ajax(url, function () {
+                            Layer.closeAll();
+                            table.trigger("uncheckbox");
+                            table.bootstrapTable('refresh');
+                        }, function () {
+                            Layer.closeAll();
+                        });
+                    });
+                    return false;
+                });
+                //全部还原
+                $(document).on('click', Table.config.restoreallbtn, function () {
+                    var that = this;
+                    var url = $(that).data("url") ? $(that).data("url") : $(that).attr("href");
+                    Fast.api.ajax(url, function () {
+                        Layer.closeAll();
+                        table.trigger("uncheckbox");
+                        table.bootstrapTable('refresh');
+                    }, function () {
+                        Layer.closeAll();
+                    });
+                    return false;
+                });
+                //销毁或删除
+                $(document).on('click', Table.config.restoreonebtn + ',' + Table.config.destroyonebtn, function () {
+                    var that = this;
+                    var url = $(that).data("url") ? $(that).data("url") : $(that).attr("href");
+                    var row = Fast.api.getrowbyindex(table, $(that).data("row-index"));
+                    Fast.api.ajax({
+                        url: url,
+                        data: {ids: row[options.pk]}
+                    }, function () {
+                        table.trigger("uncheckbox");
+                        table.bootstrapTable('refresh');
+                    });
+                    return false;
+                });
+                // 批量操作按钮事件
+                toolbar.on('click', Table.config.multibtn, function () {
+                    var ids = Table.api.selectedids(table);
+                    Table.api.multi($(this).data("action"), ids, table, this);
+                });
+                // 批量删除按钮事件
+                toolbar.on('click', Table.config.delbtn, function () {
+                    var that = this;
+                    var ids = Table.api.selectedids(table);
+                    Layer.confirm(
+                        __('Are you sure you want to delete the %s selected item?', ids.length),
+                        {icon: 3, title: __('Warning'), offset: 0, shadeClose: true, btn: [__('OK'), __('Cancel')]},
+                        function (index) {
+                            Table.api.multi("del", ids, table, that);
+                            Layer.close(index);
+                        }
+                    );
+                });
+                // 拖拽排序
+                require(['dragsort'], function () {
+                    //绑定拖动排序
+                    $("tbody", table).dragsort({
+                        itemSelector: 'tr:visible',
+                        dragSelector: "a.btn-dragsort",
+                        dragEnd: function (a, b) {
+                            var element = $("a.btn-dragsort", this);
+                            var data = table.bootstrapTable('getData');
+                            var current = data[parseInt($(this).data("index"))];
+                            var options = table.bootstrapTable('getOptions');
+                            //改变的值和改变的ID集合
+                            var ids = $.map($("tbody tr:visible", table), function (tr) {
+                                return data[parseInt($(tr).data("index"))][options.pk];
+                            });
+                            var changeid = current[options.pk];
+                            var pid = typeof current.pid != 'undefined' ? current.pid : '';
+                            var params = {
+                                url: table.bootstrapTable('getOptions').extend.dragsort_url,
+                                data: {
+                                    ids: ids.join(','),
+                                    changeid: changeid,
+                                    pid: pid,
+                                    field: Table.config.dragsortfield,
+                                    orderway: options.sortOrder,
+                                    table: options.extend.table,
+                                    pk: options.pk
+                                }
+                            };
+                            Fast.api.ajax(params, function (data, ret) {
+                                var success = $(element).data("success") || $.noop;
+                                if (typeof success === 'function') {
+                                    if (false === success.call(element, data, ret)) {
+                                        return false;
+                                    }
+                                }
+                                table.bootstrapTable('refresh');
+                            }, function (data, ret) {
+                                var error = $(element).data("error") || $.noop;
+                                if (typeof error === 'function') {
+                                    if (false === error.call(element, data, ret)) {
+                                        return false;
+                                    }
+                                }
+                                table.bootstrapTable('refresh');
+                            });
+                        },
+                        placeHolderTemplate: ""
+                    });
+                });
+                table.on("click", "input[data-id][name='checkbox']", function (e) {
+                    var ids = $(this).data("id");
+                    table.bootstrapTable($(this).prop("checked") ? 'checkBy' : 'uncheckBy', {field: options.pk, values: [ids]});
+                });
+                table.on("click", "[data-id].btn-change", function (e) {
+                    e.preventDefault();
+                    var changer = $.proxy(function () {
+                        Table.api.multi($(this).data("action") ? $(this).data("action") : '', [$(this).data("id")], table, this);
+                    }, this);
+                    if (typeof $(this).data("confirm") !== 'undefined') {
+                        Layer.confirm($(this).data("confirm"), function (index) {
+                            changer();
+                            Layer.close(index);
+                        });
+                    } else {
+                        changer();
+                    }
+                });
+                table.on("click", "[data-id].btn-edit", function (e) {
+                    e.preventDefault();
+                    var ids = $(this).data("id");
+                    var row = Table.api.getrowbyid(table, ids);
+                    row.ids = ids;
+                    var url = Table.api.replaceurl(options.extend.edit_url, row, table);
+                    Fast.api.open(url, $(this).data("original-title") || $(this).attr("title") || __('Edit'), $(this).data() || {});
+                });
+                table.on("click", "[data-id].btn-del", function (e) {
+                    e.preventDefault();
+                    var id = $(this).data("id");
+                    var that = this;
+                    Layer.confirm(
+                        __('Are you sure you want to delete this item?'),
+                        {icon: 3, title: __('Warning'), shadeClose: true, btn: [__('OK'), __('Cancel')]},
+                        function (index) {
+                            Table.api.multi("del", id, table, that);
+                            Layer.close(index);
+                        }
+                    );
+                });
+
+                //修复dropdown定位溢出的情况
+                if (options.fixDropdownPosition) {
+                    var tableBody = table.closest(".fixed-table-body");
+                    table.on('show.bs.dropdown fa.event.refreshdropdown', ".btn-group", function (e) {
+                        var dropdownMenu = $(".dropdown-menu", this);
+                        var btnGroup = $(this);
+                        var isPullRight = dropdownMenu.hasClass("pull-right") || dropdownMenu.hasClass("dropdown-menu-right");
+                        var left, top, position;
+                        if (true || dropdownMenu.outerHeight() + btnGroup.outerHeight() > tableBody.outerHeight() - 41) {
+                            position = 'fixed';
+                            top = btnGroup.offset().top - $(window).scrollTop() + btnGroup.outerHeight();
+                            if ((top + dropdownMenu.outerHeight()) > $(window).height()) {
+                                top = btnGroup.offset().top - dropdownMenu.outerHeight() - 5;
+                            }
+                            left = isPullRight ? btnGroup.offset().left + btnGroup.outerWidth() - dropdownMenu.outerWidth() : btnGroup.offset().left;
+                        }
+                        if (left || top) {
+                            dropdownMenu.css({
+                                position: position, left: left, top: top, right: 'inherit'
+                            });
+                        }
+                    });
+                    var checkdropdown = function () {
+                        if ($(".btn-group.open", table).length > 0 && $(".btn-group.open .dropdown-menu", table).css("position") == 'fixed') {
+                            $(".btn-group.open", table).trigger("fa.event.refreshdropdown");
+                        }
+                    };
+                    $(window).on("scroll", function () {
+                        checkdropdown();
+                    });
+                    tableBody.on("scroll", function () {
+                        checkdropdown();
+                    });
+                }
+
+                var id = table.attr("id");
+                Table.list[id] = table;
+                return table;
+            },
+            // 批量操作请求
+            multi: function (action, ids, table, element) {
+                var options = table.bootstrapTable('getOptions');
+                var data = element ? $(element).data() : {};
+                ids = ($.isArray(ids) ? ids.join(",") : ids);
+                var url = typeof data.url !== "undefined" ? data.url : (action == "del" ? options.extend.del_url : options.extend.multi_url);
+                var params = typeof data.params !== "undefined" ? (typeof data.params == 'object' ? $.param(data.params) : data.params) : '';
+                options = {url: url, data: {action: action, ids: ids, params: params}};
+                Fast.api.ajax(options, function (data, ret) {
+                    table.trigger("uncheckbox");
+                    var success = $(element).data("success") || $.noop;
+                    if (typeof success === 'function') {
+                        if (false === success.call(element, data, ret)) {
+                            return false;
+                        }
+                    }
+                    table.bootstrapTable('refresh');
+                }, function (data, ret) {
+                    var error = $(element).data("error") || $.noop;
+                    if (typeof error === 'function') {
+                        if (false === error.call(element, data, ret)) {
+                            return false;
+                        }
+                    }
+                });
+            },
+            // 单元格元素事件
+            events: {
+                operate: {
+                    'click .btn-editone': function (e, value, row, index) {
+                        e.stopPropagation();
+                        e.preventDefault();
+                        var table = $(this).closest('table');
+                        var options = table.bootstrapTable('getOptions');
+                        var ids = row[options.pk];
+                        row = $.extend({}, row ? row : {}, {ids: ids});
+                        var url = options.extend.edit_url;
+                        Fast.api.open(Table.api.replaceurl(url, row, table), $(this).data("original-title") || $(this).attr("title") || __('Edit'), $(this).data() || {});
+                    },
+                    'click .btn-delone': function (e, value, row, index) {
+                        e.stopPropagation();
+                        e.preventDefault();
+                        var that = this;
+                        var top = $(that).offset().top - $(window).scrollTop();
+                        var left = $(that).offset().left - $(window).scrollLeft() - 260;
+                        if (top + 154 > $(window).height()) {
+                            top = top - 154;
+                        }
+                        if ($(window).width() < 480) {
+                            top = left = undefined;
+                        }
+                        Layer.confirm(
+                            __('Are you sure you want to delete this item?'),
+                            {icon: 3, title: __('Warning'), offset: [top, left], shadeClose: true, btn: [__('OK'), __('Cancel')]},
+                            function (index) {
+                                var table = $(that).closest('table');
+                                var options = table.bootstrapTable('getOptions');
+                                Table.api.multi("del", row[options.pk], table, that);
+                                Layer.close(index);
+                            }
+                        );
+                    }
+                },//单元格图片预览
+                image: {
+                    'click .img-center': function (e, value, row, index) {
+                        var data = [];
+                        value = value === null ? '' : value.toString();
+                        var arr = value != '' ? value.split(",") : [];
+                        var url;
+                        $.each(arr, function (index, value) {
+                            url = Fast.api.cdnurl(value);
+                            data.push({
+                                src: url,
+                                thumb: url.match(/^(\/|data:image\\)/) ? url : url + Config.upload.thumbstyle
+                            });
+                        });
+                        Layer.photos({
+                            photos: {
+                                "start": $(this).parent().index(),
+                                "data": data
+                            },
+                            anim: 5 //0-6的选择,指定弹出图片动画类型,默认随机(请注意,3.0之前的版本用shift参数)
+                        });
+                    },
+                }
+            },
+            // 单元格数据格式化
+            formatter: {
+                icon: function (value, row, index) {
+                    value = value === null ? '' : value.toString();
+                    value = value.indexOf(" ") > -1 ? value : "fa fa-" + value;
+                    //渲染fontawesome图标
+                    return '<i class="' + value + '"></i> ' + value;
+                },
+                image: function (value, row, index) {
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    value = value ? value : '/assets/img/blank.gif';
+                    var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
+                    var url = Fast.api.cdnurl(value, true);
+                    url = url.match(/^(\/|data:image\\)/) ? url : url + Config.upload.thumbstyle;
+                    return '<a href="javascript:"><img class="' + classname + '" src="' + url + '" /></a>';
+                },
+                images: function (value, row, index) {
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
+                    var arr = value != '' ? value.split(',') : [];
+                    var html = [];
+                    var url;
+                    $.each(arr, function (i, value) {
+                        value = value ? value : '/assets/img/blank.gif';
+                        url = Fast.api.cdnurl(value, true);
+                        url = url.match(/^(\/|data:image\\)/) ? url : url + Config.upload.thumbstyle;
+                        html.push('<a href="javascript:"><img class="' + classname + '" src="' + url + '" /></a>');
+                    });
+                    return html.join(' ');
+                },
+                file: function (value, row, index) {
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    value = Fast.api.cdnurl(value, true);
+                    var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
+                    var suffix = /[\.]?([a-zA-Z0-9]+)$/.exec(value);
+                    suffix = suffix ? suffix[1] : 'file';
+                    var url = Fast.api.fixurl("ajax/icon?suffix=" + suffix);
+                    return '<a href="' + value + '" target="_blank"><img src="' + url + '" class="' + classname + '"></a>';
+                },
+                files: function (value, row, index) {
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
+                    var arr = value != '' ? value.split(',') : [];
+                    var html = [];
+                    var suffix, url;
+                    $.each(arr, function (i, value) {
+                        value = Fast.api.cdnurl(value, true);
+                        suffix = /[\.]?([a-zA-Z0-9]+)$/.exec(value);
+                        suffix = suffix ? suffix[1] : 'file';
+                        url = Fast.api.fixurl("ajax/icon?suffix=" + suffix);
+                        html.push('<a href="' + value + '" target="_blank"><img src="' + url + '" class="' + classname + '"></a>');
+                    });
+                    return html.join(' ');
+                },
+                content: function (value, row, index) {
+                    var width = this.width != undefined ? (this.width.match(/^\d+$/) ? this.width + "px" : this.width) : "250px";
+                    return "<div style='white-space: nowrap; text-overflow:ellipsis; overflow: hidden; max-width:" + width + ";'>" + value + "</div>";
+                },
+                status: function (value, row, index) {
+                    var custom = {normal: 'success', hidden: 'gray', deleted: 'danger', locked: 'info'};
+                    if (typeof this.custom !== 'undefined') {
+                        custom = $.extend(custom, this.custom);
+                    }
+                    this.custom = custom;
+                    this.icon = 'fa fa-circle';
+                    return Table.api.formatter.normal.call(this, value, row, index);
+                },
+                normal: function (value, row, index) {
+                    var colorArr = ["primary", "success", "danger", "warning", "info", "gray", "red", "yellow", "aqua", "blue", "navy", "teal", "olive", "lime", "fuchsia", "purple", "maroon"];
+                    var custom = {};
+                    if (typeof this.custom !== 'undefined') {
+                        custom = $.extend(custom, this.custom);
+                    }
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    var keys = typeof this.searchList === 'object' ? Object.keys(this.searchList) : [];
+                    var index = keys.indexOf(value);
+                    var color = value && typeof custom[value] !== 'undefined' ? custom[value] : null;
+                    var display = index > -1 ? this.searchList[value] : null;
+                    var icon = typeof this.icon !== 'undefined' ? this.icon : null;
+                    if (!color) {
+                        color = index > -1 && typeof colorArr[index] !== 'undefined' ? colorArr[index] : 'primary';
+                    }
+                    if (!display) {
+                        display = __(value.charAt(0).toUpperCase() + value.slice(1));
+                    }
+                    var html = '<span class="text-' + color + '">' + (icon ? '<i class="' + icon + '"></i> ' : '') + display + '</span>';
+                    if (this.operate != false) {
+                        html = '<a href="javascript:;" class="searchit" data-toggle="tooltip" title="' + __('Click to search %s', display) + '" data-field="' + this.field + '" data-value="' + value + '">' + html + '</a>';
+                    }
+                    return html;
+                },
+                toggle: function (value, row, index) {
+                    var table = this.table;
+                    var options = table ? table.bootstrapTable('getOptions') : {};
+                    var pk = options.pk || "id";
+                    var color = typeof this.color !== 'undefined' ? this.color : 'success';
+                    var yes = typeof this.yes !== 'undefined' ? this.yes : 1;
+                    var no = typeof this.no !== 'undefined' ? this.no : 0;
+                    var url = typeof this.url !== 'undefined' ? this.url : '';
+                    var confirm = '';
+                    var disable = false;
+                    if (typeof this.confirm !== "undefined") {
+                        confirm = typeof this.confirm === "function" ? this.confirm.call(this, value, row, index) : this.confirm;
+                    }
+                    if (typeof this.disable !== "undefined") {
+                        disable = typeof this.disable === "function" ? this.disable.call(this, value, row, index) : this.disable;
+                    }
+                    return "<a href='javascript:;' data-toggle='tooltip' title='" + __('Click to toggle') + "' class='btn-change " + (disable ? 'btn disabled no-padding' : '') + "' data-index='" + index + "' data-id='"
+                        + row[pk] + "' " + (url ? "data-url='" + url + "'" : "") + (confirm ? "data-confirm='" + confirm + "'" : "") + " data-params='" + this.field + "=" + (value == yes ? no : yes) + "'><i class='fa fa-toggle-on text-success text-" + color + " " + (value == yes ? '' : 'fa-flip-horizontal text-gray') + " fa-2x'></i></a>";
+                },
+                url: function (value, row, index) {
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    return '<div class="input-group input-group-sm" style="width:250px;margin:0 auto;"><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>';
+                },
+                search: function (value, row, index) {
+                    var field = this.field;
+                    if (typeof this.customField !== 'undefined' && typeof row[this.customField] !== 'undefined') {
+                        value = row[this.customField];
+                        field = this.customField;
+                    }
+                    return '<a href="javascript:;" class="searchit" data-toggle="tooltip" title="' + __('Click to search %s', value) + '" data-field="' + field + '" data-value="' + value + '">' + value + '</a>';
+                },
+                addtabs: function (value, row, index) {
+                    var url = Table.api.replaceurl(this.url || '', row, this.table);
+                    var title = this.atitle ? this.atitle : __("Search %s", value);
+                    return '<a href="' + Fast.api.fixurl(url) + '" class="addtabsit" data-value="' + value + '" title="' + title + '">' + value + '</a>';
+                },
+                dialog: function (value, row, index) {
+                    var url = Table.api.replaceurl(this.url || '', row, this.table);
+                    var title = this.atitle ? this.atitle : __("View %s", value);
+                    return '<a href="' + Fast.api.fixurl(url) + '" class="dialogit" data-value="' + value + '" title="' + title + '">' + value + '</a>';
+                },
+                flag: function (value, row, index) {
+                    var that = this;
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    var colorArr = {index: 'success', hot: 'warning', recommend: 'danger', 'new': 'info'};
+                    //如果字段列有定义custom
+                    if (typeof this.custom !== 'undefined') {
+                        colorArr = $.extend(colorArr, this.custom);
+                    }
+                    var field = this.field;
+                    if (typeof this.customField !== 'undefined' && typeof row[this.customField] !== 'undefined') {
+                        value = row[this.customField];
+                        field = this.customField;
+                    }
+                    if (typeof that.searchList === 'object' && typeof that.custom === 'undefined') {
+                        var i = 0;
+                        var searchValues = Object.values(colorArr);
+                        $.each(that.searchList, function (key, val) {
+                            if (typeof colorArr[key] == 'undefined') {
+                                colorArr[key] = searchValues[i];
+                                i = typeof searchValues[i + 1] === 'undefined' ? 0 : i + 1;
+                            }
+                        });
+                    }
+
+                    //渲染Flag
+                    var html = [];
+                    var arr = value != '' ? value.split(',') : [];
+                    var color, display, label;
+                    $.each(arr, function (i, value) {
+                        value = value == null || value.length === 0 ? '' : value.toString();
+                        if (value == '')
+                            return true;
+                        color = value && typeof colorArr[value] !== 'undefined' ? colorArr[value] : 'primary';
+                        display = typeof that.searchList !== 'undefined' && typeof that.searchList[value] !== 'undefined' ? that.searchList[value] : __(value.charAt(0).toUpperCase() + value.slice(1));
+                        label = '<span class="label label-' + color + '">' + display + '</span>';
+                        if (that.operate) {
+                            html.push('<a href="javascript:;" class="searchit" data-toggle="tooltip" title="' + __('Click to search %s', display) + '" data-field="' + field + '" data-value="' + value + '">' + label + '</a>');
+                        } else {
+                            html.push(label);
+                        }
+                    });
+                    return html.join(' ');
+                },
+                label: function (value, row, index) {
+                    return Table.api.formatter.flag.call(this, value, row, index);
+                },
+                datetime: function (value, row, index) {
+                    var datetimeFormat = typeof this.datetimeFormat === 'undefined' ? 'YYYY-MM-DD HH:mm:ss' : this.datetimeFormat;
+                    if (isNaN(value)) {
+                        return value ? Moment(value).format(datetimeFormat) : __('None');
+                    } else {
+                        return value ? Moment(parseInt(value) * 1000).format(datetimeFormat) : __('None');
+                    }
+                },
+                operate: function (value, row, index) {
+                    var table = this.table;
+                    // 操作配置
+                    var options = table ? table.bootstrapTable('getOptions') : {};
+                    // 默认按钮组
+                    var buttons = $.extend([], this.buttons || []);
+                    // 所有按钮名称
+                    var names = [];
+                    buttons.forEach(function (item) {
+                        names.push(item.name);
+                    });
+                    if (options.extend.dragsort_url !== '' && names.indexOf('dragsort') === -1) {
+                        buttons.push(Table.button.dragsort);
+                    }
+                    if (options.extend.edit_url !== '' && names.indexOf('edit') === -1) {
+                        Table.button.edit.url = options.extend.edit_url;
+                        buttons.push(Table.button.edit);
+                    }
+                    if (options.extend.del_url !== '' && names.indexOf('del') === -1) {
+                        buttons.push(Table.button.del);
+                    }
+                    return Table.api.buttonlink(this, buttons, value, row, index, 'operate');
+                }
+                ,
+                buttons: function (value, row, index) {
+                    // 默认按钮组
+                    var buttons = $.extend([], this.buttons || []);
+                    return Table.api.buttonlink(this, buttons, value, row, index, 'buttons');
+                }
+            },
+            buttonlink: function (column, buttons, value, row, index, type) {
+                var table = column.table;
+                column.clickToSelect = false;
+                type = typeof type === 'undefined' ? 'buttons' : type;
+                var options = table ? table.bootstrapTable('getOptions') : {};
+                var html = [];
+                var hidden, visible, disable, url, classname, icon, text, title, refresh, confirm, extend,
+                    dropdown, link;
+                var fieldIndex = column.fieldIndex;
+                var dropdowns = {};
+
+                $.each(buttons, function (i, j) {
+                    if (type === 'operate') {
+                        if (j.name === 'dragsort' && typeof row[Table.config.dragsortfield] === 'undefined') {
+                            return true;
+                        }
+                        if (['add', 'edit', 'del', 'multi', 'dragsort'].indexOf(j.name) > -1 && !options.extend[j.name + "_url"]) {
+                            return true;
+                        }
+                    }
+                    var attr = table.data(type + "-" + j.name);
+                    if (typeof attr === 'undefined' || attr) {
+                        hidden = typeof j.hidden === 'function' ? j.hidden.call(table, row, j) : (typeof j.hidden !== 'undefined' ? j.hidden : false);
+                        if (hidden) {
+                            return true;
+                        }
+                        visible = typeof j.visible === 'function' ? j.visible.call(table, row, j) : (typeof j.visible !== 'undefined' ? j.visible : true);
+                        if (!visible) {
+                            return true;
+                        }
+                        dropdown = j.dropdown ? j.dropdown : '';
+                        url = j.url ? j.url : '';
+                        url = typeof url === 'function' ? url.call(table, row, j) : (url ? Fast.api.fixurl(Table.api.replaceurl(url, row, table)) : 'javascript:;');
+                        classname = j.classname ? j.classname : (dropdown ? 'btn-' + name + 'one' : 'btn-primary btn-' + name + 'one');
+                        icon = j.icon ? j.icon : '';
+                        text = typeof j.text === 'function' ? j.text.call(table, row, j) : j.text ? j.text : '';
+                        title = typeof j.title === 'function' ? j.title.call(table, row, j) : j.title ? j.title : text;
+                        refresh = j.refresh ? 'data-refresh="' + j.refresh + '"' : '';
+                        confirm = typeof j.confirm === 'function' ? j.confirm.call(table, row, j) : (typeof j.confirm !== 'undefined' ? j.confirm : false);
+                        confirm = confirm ? 'data-confirm="' + confirm + '"' : '';
+                        extend = j.extend ? j.extend : '';
+                        disable = typeof j.disable === 'function' ? j.disable.call(table, row, j) : (typeof j.disable !== 'undefined' ? j.disable : false);
+                        if (disable) {
+                            classname = classname + ' disabled';
+                        }
+                        link = '<a href="' + url + '" class="' + classname + '" ' + (confirm ? confirm + ' ' : '') + (refresh ? refresh + ' ' : '') + extend + ' title="' + title + '" data-table-id="' + (table ? table.attr("id") : '') + '" data-field-index="' + fieldIndex + '" data-row-index="' + index + '" data-button-index="' + i + '"><i class="' + icon + '"></i>' + (text ? ' ' + text : '') + '</a>';
+                        if (dropdown) {
+                            if (typeof dropdowns[dropdown] == 'undefined') {
+                                dropdowns[dropdown] = [];
+                            }
+                            dropdowns[dropdown].push(link);
+                        } else {
+                            html.push(link);
+                        }
+                    }
+                });
+                if (!$.isEmptyObject(dropdowns)) {
+                    var dropdownHtml = [];
+                    $.each(dropdowns, function (i, j) {
+                        dropdownHtml.push('<div class="btn-group"><button type="button" class="btn btn-primary dropdown-toggle btn-xs" data-toggle="dropdown">' + i + '</button><button type="button" class="btn btn-primary dropdown-toggle btn-xs" data-toggle="dropdown"><span class="caret"></span></button><ul class="dropdown-menu dropdown-menu-right"><li>' + j.join('</li><li>') + '</li></ul></div>');
+                    });
+                    html.unshift(dropdownHtml);
+                }
+                return html.join(' ');
+            },
+            //替换URL中的数据
+            replaceurl: function (url, row, table) {
+                var options = table ? table.bootstrapTable('getOptions') : null;
+                var ids = options ? row[options.pk] : 0;
+                row.ids = ids ? ids : (typeof row.ids !== 'undefined' ? row.ids : 0);
+                url = url == null || url.length === 0 ? '' : url.toString();
+                //自动添加ids参数
+                url = !url.match(/\{ids\}/i) ? url + (url.match(/(\?|&)+/) ? "&ids=" : "/ids/") + '{ids}' : url;
+                url = url.replace(/\{(.*?)\}/gi, function (matched) {
+                    matched = matched.substring(1, matched.length - 1);
+                    if (matched.indexOf(".") !== -1) {
+                        var temp = row;
+                        var arr = matched.split(/\./);
+                        for (var i = 0; i < arr.length; i++) {
+                            if (typeof temp[arr[i]] !== 'undefined') {
+                                temp = temp[arr[i]];
+                            }
+                        }
+                        return typeof temp === 'object' ? '' : temp;
+                    }
+                    return row[matched];
+                });
+                return url;
+            },
+            // 获取选中的条目ID集合
+            selectedids: function (table, current) {
+                var options = table.bootstrapTable('getOptions');
+                //如果有设置翻页记忆模式
+                if (!current && options.maintainSelected) {
+                    return options.selectedIds;
+                }
+                return $.map(table.bootstrapTable('getSelections'), function (row) {
+                    return row[options.pk];
+                });
+            },
+            //获取选中的数据
+            selecteddata: function (table, current) {
+                var options = table.bootstrapTable('getOptions');
+                //如果有设置翻页记忆模式
+                if (!current && options.maintainSelected) {
+                    return options.selectedData;
+                }
+                return table.bootstrapTable('getSelections');
+            },
+            // 切换复选框状态
+            toggleattr: function (table) {
+                $("input[type='checkbox']", table).trigger('click');
+            },
+            // 根据行索引获取行数据
+            getrowdata: function (table, index) {
+                index = parseInt(index);
+                var data = table.bootstrapTable('getData');
+                return typeof data[index] !== 'undefined' ? data[index] : null;
+            },
+            // 根据行索引获取行数据
+            getrowbyindex: function (table, index) {
+                return Table.api.getrowdata(table, index);
+            },
+            // 根据主键ID获取行数据
+            getrowbyid: function (table, id) {
+                var row = {};
+                var options = table.bootstrapTable("getOptions");
+                $.each(Table.api.selecteddata(table), function (i, j) {
+                    if (j[options.pk] == id) {
+                        row = j;
+                        return false;
+                    }
+                });
+                return row;
+            }
+        },
+    };
+    return Table;
+});

+ 2843 - 0
application/vendor/composer/installed.json

@@ -0,0 +1,2843 @@
+[
+    {
+        "name": "topthink/think-installer",
+        "version": "v1.0.14",
+        "version_normalized": "1.0.14.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/top-think/think-installer.git",
+            "reference": "eae1740ac264a55c06134b6685dfb9f837d004d1"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/top-think/think-installer/zipball/eae1740ac264a55c06134b6685dfb9f837d004d1",
+            "reference": "eae1740ac264a55c06134b6685dfb9f837d004d1",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "composer-plugin-api": "^1.0||^2.0"
+        },
+        "require-dev": {
+            "composer/composer": "^1.0||^2.0"
+        },
+        "time": "2021-03-25 08:34:02",
+        "type": "composer-plugin",
+        "extra": {
+            "class": "think\\composer\\Plugin"
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "think\\composer\\": "src"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "yunwuxin",
+                "email": "448901948@qq.com"
+            }
+        ]
+    },
+    {
+        "name": "topthink/think-helper",
+        "version": "v1.0.7",
+        "version_normalized": "1.0.7.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/top-think/think-helper.git",
+            "reference": "5f92178606c8ce131d36b37a57c58eb71e55f019"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/top-think/think-helper/zipball/5f92178606c8ce131d36b37a57c58eb71e55f019",
+            "reference": "5f92178606c8ce131d36b37a57c58eb71e55f019",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "time": "2018-10-05 00:43:21",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "think\\helper\\": "src"
+            },
+            "files": [
+                "src/helper.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "yunwuxin",
+                "email": "448901948@qq.com"
+            }
+        ],
+        "description": "The ThinkPHP5 Helper Package"
+    },
+    {
+        "name": "topthink/think-queue",
+        "version": "v1.1.6",
+        "version_normalized": "1.1.6.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/top-think/think-queue.git",
+            "reference": "250650eb0e8ea5af4cfdc7ae46f3f4e0a24ac245"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/top-think/think-queue/zipball/250650eb0e8ea5af4cfdc7ae46f3f4e0a24ac245",
+            "reference": "250650eb0e8ea5af4cfdc7ae46f3f4e0a24ac245",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "topthink/think-helper": ">=1.0.4",
+            "topthink/think-installer": ">=1.0.10"
+        },
+        "require-dev": {
+            "topthink/framework": "~5.0.0"
+        },
+        "time": "2018-10-15 10:16:55",
+        "type": "think-extend",
+        "extra": {
+            "think-config": {
+                "queue": "src/config.php"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "think\\": "src"
+            },
+            "files": [
+                "src/common.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "yunwuxin",
+                "email": "448901948@qq.com"
+            }
+        ],
+        "description": "The ThinkPHP5 Queue Package"
+    },
+    {
+        "name": "psr/simple-cache",
+        "version": "1.0.1",
+        "version_normalized": "1.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/simple-cache.git",
+            "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+            "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2017-10-23 01:57:42",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\SimpleCache\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common interfaces for simple caching",
+        "keywords": [
+            "cache",
+            "caching",
+            "psr",
+            "psr-16",
+            "simple-cache"
+        ]
+    },
+    {
+        "name": "markbaker/matrix",
+        "version": "1.2.3",
+        "version_normalized": "1.2.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/MarkBaker/PHPMatrix.git",
+            "reference": "44bb1ab01811116f01fe216ab37d921dccc6c10d"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/44bb1ab01811116f01fe216ab37d921dccc6c10d",
+            "reference": "44bb1ab01811116f01fe216ab37d921dccc6c10d",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": "^5.6.0|^7.0.0"
+        },
+        "require-dev": {
+            "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+            "phpcompatibility/php-compatibility": "dev-master",
+            "phploc/phploc": "^4",
+            "phpmd/phpmd": "dev-master",
+            "phpunit/phpunit": "^5.7|^6.0|7.0",
+            "sebastian/phpcpd": "^3.0",
+            "squizlabs/php_codesniffer": "^3.0@dev"
+        },
+        "time": "2021-01-26 14:36:01",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Matrix\\": "classes/src/"
+            },
+            "files": [
+                "classes/src/Functions/adjoint.php",
+                "classes/src/Functions/antidiagonal.php",
+                "classes/src/Functions/cofactors.php",
+                "classes/src/Functions/determinant.php",
+                "classes/src/Functions/diagonal.php",
+                "classes/src/Functions/identity.php",
+                "classes/src/Functions/inverse.php",
+                "classes/src/Functions/minors.php",
+                "classes/src/Functions/trace.php",
+                "classes/src/Functions/transpose.php",
+                "classes/src/Operations/add.php",
+                "classes/src/Operations/directsum.php",
+                "classes/src/Operations/subtract.php",
+                "classes/src/Operations/multiply.php",
+                "classes/src/Operations/divideby.php",
+                "classes/src/Operations/divideinto.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Mark Baker",
+                "email": "mark@lange.demon.co.uk"
+            }
+        ],
+        "description": "PHP Class for working with matrices",
+        "homepage": "https://github.com/MarkBaker/PHPMatrix",
+        "keywords": [
+            "mathematics",
+            "matrix",
+            "vector"
+        ]
+    },
+    {
+        "name": "markbaker/complex",
+        "version": "1.5.0",
+        "version_normalized": "1.5.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/MarkBaker/PHPComplex.git",
+            "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/c3131244e29c08d44fefb49e0dd35021e9e39dd2",
+            "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": "^5.6.0|^7.0"
+        },
+        "require-dev": {
+            "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
+            "phpcompatibility/php-compatibility": "^9.0",
+            "phpdocumentor/phpdocumentor": "2.*",
+            "phploc/phploc": "^4.0|^5.0|^6.0|^7.0",
+            "phpmd/phpmd": "2.*",
+            "phpunit/phpunit": "^4.8.35|^5.0|^6.0|^7.0",
+            "sebastian/phpcpd": "2.*",
+            "squizlabs/php_codesniffer": "^3.4.0"
+        },
+        "time": "2020-08-26 19:47:57",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Complex\\": "classes/src/"
+            },
+            "files": [
+                "classes/src/functions/abs.php",
+                "classes/src/functions/acos.php",
+                "classes/src/functions/acosh.php",
+                "classes/src/functions/acot.php",
+                "classes/src/functions/acoth.php",
+                "classes/src/functions/acsc.php",
+                "classes/src/functions/acsch.php",
+                "classes/src/functions/argument.php",
+                "classes/src/functions/asec.php",
+                "classes/src/functions/asech.php",
+                "classes/src/functions/asin.php",
+                "classes/src/functions/asinh.php",
+                "classes/src/functions/atan.php",
+                "classes/src/functions/atanh.php",
+                "classes/src/functions/conjugate.php",
+                "classes/src/functions/cos.php",
+                "classes/src/functions/cosh.php",
+                "classes/src/functions/cot.php",
+                "classes/src/functions/coth.php",
+                "classes/src/functions/csc.php",
+                "classes/src/functions/csch.php",
+                "classes/src/functions/exp.php",
+                "classes/src/functions/inverse.php",
+                "classes/src/functions/ln.php",
+                "classes/src/functions/log2.php",
+                "classes/src/functions/log10.php",
+                "classes/src/functions/negative.php",
+                "classes/src/functions/pow.php",
+                "classes/src/functions/rho.php",
+                "classes/src/functions/sec.php",
+                "classes/src/functions/sech.php",
+                "classes/src/functions/sin.php",
+                "classes/src/functions/sinh.php",
+                "classes/src/functions/sqrt.php",
+                "classes/src/functions/tan.php",
+                "classes/src/functions/tanh.php",
+                "classes/src/functions/theta.php",
+                "classes/src/operations/add.php",
+                "classes/src/operations/subtract.php",
+                "classes/src/operations/multiply.php",
+                "classes/src/operations/divideby.php",
+                "classes/src/operations/divideinto.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Mark Baker",
+                "email": "mark@lange.demon.co.uk"
+            }
+        ],
+        "description": "PHP Class for working with complex numbers",
+        "homepage": "https://github.com/MarkBaker/PHPComplex",
+        "keywords": [
+            "complex",
+            "mathematics"
+        ]
+    },
+    {
+        "name": "phpoffice/phpspreadsheet",
+        "version": "1.12.0",
+        "version_normalized": "1.12.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+            "reference": "f79611d6dc1f6b7e8e30b738fc371b392001dbfd"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/f79611d6dc1f6b7e8e30b738fc371b392001dbfd",
+            "reference": "f79611d6dc1f6b7e8e30b738fc371b392001dbfd",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "ext-ctype": "*",
+            "ext-dom": "*",
+            "ext-fileinfo": "*",
+            "ext-gd": "*",
+            "ext-iconv": "*",
+            "ext-libxml": "*",
+            "ext-mbstring": "*",
+            "ext-simplexml": "*",
+            "ext-xml": "*",
+            "ext-xmlreader": "*",
+            "ext-xmlwriter": "*",
+            "ext-zip": "*",
+            "ext-zlib": "*",
+            "markbaker/complex": "^1.4",
+            "markbaker/matrix": "^1.2",
+            "php": "^7.1",
+            "psr/simple-cache": "^1.0"
+        },
+        "require-dev": {
+            "dompdf/dompdf": "^0.8.3",
+            "friendsofphp/php-cs-fixer": "^2.16",
+            "jpgraph/jpgraph": "^4.0",
+            "mpdf/mpdf": "^8.0",
+            "phpcompatibility/php-compatibility": "^9.3",
+            "phpunit/phpunit": "^7.5",
+            "squizlabs/php_codesniffer": "^3.5",
+            "tecnickcom/tcpdf": "^6.3"
+        },
+        "suggest": {
+            "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+            "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+            "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+            "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+        },
+        "time": "2020-04-27 08:12:48",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Maarten Balliauw",
+                "homepage": "https://blog.maartenballiauw.be"
+            },
+            {
+                "name": "Mark Baker",
+                "homepage": "https://markbakeruk.net"
+            },
+            {
+                "name": "Franck Lefevre",
+                "homepage": "https://rootslabs.net"
+            },
+            {
+                "name": "Erik Tilt"
+            },
+            {
+                "name": "Adrien Crivelli"
+            }
+        ],
+        "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+        "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+        "keywords": [
+            "OpenXML",
+            "excel",
+            "gnumeric",
+            "ods",
+            "php",
+            "spreadsheet",
+            "xls",
+            "xlsx"
+        ]
+    },
+    {
+        "name": "paragonie/random_compat",
+        "version": "v9.99.100",
+        "version_normalized": "9.99.100.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/paragonie/random_compat.git",
+            "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+            "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">= 7"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "4.*|5.*",
+            "vimeo/psalm": "^1"
+        },
+        "suggest": {
+            "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+        },
+        "time": "2020-10-15 08:29:30",
+        "type": "library",
+        "installation-source": "dist",
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Paragon Initiative Enterprises",
+                "email": "security@paragonie.com",
+                "homepage": "https://paragonie.com"
+            }
+        ],
+        "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+        "keywords": [
+            "csprng",
+            "polyfill",
+            "pseudorandom",
+            "random"
+        ]
+    },
+    {
+        "name": "psr/http-message",
+        "version": "1.0.1",
+        "version_normalized": "1.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/http-message.git",
+            "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+            "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2016-08-06 14:39:51",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Http\\Message\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common interface for HTTP messages",
+        "homepage": "https://github.com/php-fig/http-message",
+        "keywords": [
+            "http",
+            "http-message",
+            "psr",
+            "psr-7",
+            "request",
+            "response"
+        ]
+    },
+    {
+        "name": "symfony/psr-http-message-bridge",
+        "version": "v1.3.0",
+        "version_normalized": "1.3.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/psr-http-message-bridge.git",
+            "reference": "9d3e80d54d9ae747ad573cad796e8e247df7b796"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/9d3e80d54d9ae747ad573cad796e8e247df7b796",
+            "reference": "9d3e80d54d9ae747ad573cad796e8e247df7b796",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": "^7.1",
+            "psr/http-message": "^1.0",
+            "symfony/http-foundation": "^4.4 || ^5.0"
+        },
+        "require-dev": {
+            "nyholm/psr7": "^1.1",
+            "symfony/phpunit-bridge": "^4.4 || ^5.0",
+            "zendframework/zend-diactoros": "^1.4.1 || ^2.0"
+        },
+        "suggest": {
+            "nyholm/psr7": "For a super lightweight PSR-7/17 implementation"
+        },
+        "time": "2019-11-25 19:33:50",
+        "type": "symfony-bridge",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.3-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Bridge\\PsrHttpMessage\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Fabien Potencier",
+                "email": "fabien@symfony.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "http://symfony.com/contributors"
+            }
+        ],
+        "description": "PSR HTTP message bridge",
+        "homepage": "http://symfony.com",
+        "keywords": [
+            "http",
+            "http-message",
+            "psr-17",
+            "psr-7"
+        ]
+    },
+    {
+        "name": "psr/container",
+        "version": "1.0.0",
+        "version_normalized": "1.0.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/container.git",
+            "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+            "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2017-02-14 16:28:37",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Container\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common Container Interface (PHP FIG PSR-11)",
+        "homepage": "https://github.com/php-fig/container",
+        "keywords": [
+            "PSR-11",
+            "container",
+            "container-interface",
+            "container-interop",
+            "psr"
+        ]
+    },
+    {
+        "name": "psr/cache",
+        "version": "1.0.1",
+        "version_normalized": "1.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/cache.git",
+            "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+            "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2016-08-06 20:24:11",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Cache\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common interface for caching libraries",
+        "keywords": [
+            "cache",
+            "psr",
+            "psr-6"
+        ]
+    },
+    {
+        "name": "pimple/pimple",
+        "version": "v3.2.3",
+        "version_normalized": "3.2.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/silexphp/Pimple.git",
+            "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32",
+            "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3.0",
+            "psr/container": "^1.0"
+        },
+        "require-dev": {
+            "symfony/phpunit-bridge": "^3.2"
+        },
+        "time": "2018-01-21 07:42:36",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "3.2.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-0": {
+                "Pimple": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Fabien Potencier",
+                "email": "fabien@symfony.com"
+            }
+        ],
+        "description": "Pimple, a simple Dependency Injection Container",
+        "homepage": "http://pimple.sensiolabs.org",
+        "keywords": [
+            "container",
+            "dependency injection"
+        ]
+    },
+    {
+        "name": "ralouphie/getallheaders",
+        "version": "3.0.3",
+        "version_normalized": "3.0.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/ralouphie/getallheaders.git",
+            "reference": "120b605dfeb996808c31b6477290a714d356e822"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+            "reference": "120b605dfeb996808c31b6477290a714d356e822",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.6"
+        },
+        "require-dev": {
+            "php-coveralls/php-coveralls": "^2.1",
+            "phpunit/phpunit": "^5 || ^6.5"
+        },
+        "time": "2019-03-08 08:55:37",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "files": [
+                "src/getallheaders.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Ralph Khattar",
+                "email": "ralph.khattar@gmail.com"
+            }
+        ],
+        "description": "A polyfill for getallheaders."
+    },
+    {
+        "name": "guzzlehttp/guzzle",
+        "version": "6.5.5",
+        "version_normalized": "6.5.5.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/guzzle/guzzle.git",
+            "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+            "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "ext-json": "*",
+            "guzzlehttp/promises": "^1.0",
+            "guzzlehttp/psr7": "^1.6.1",
+            "php": ">=5.5",
+            "symfony/polyfill-intl-idn": "^1.17.0"
+        },
+        "require-dev": {
+            "ext-curl": "*",
+            "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+            "psr/log": "^1.1"
+        },
+        "suggest": {
+            "psr/log": "Required for using the Log middleware"
+        },
+        "time": "2020-06-16 21:01:06",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "6.5-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "GuzzleHttp\\": "src/"
+            },
+            "files": [
+                "src/functions_include.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Michael Dowling",
+                "email": "mtdowling@gmail.com",
+                "homepage": "https://github.com/mtdowling"
+            }
+        ],
+        "description": "Guzzle is a PHP HTTP client library",
+        "homepage": "http://guzzlephp.org/",
+        "keywords": [
+            "client",
+            "curl",
+            "framework",
+            "http",
+            "http client",
+            "rest",
+            "web service"
+        ]
+    },
+    {
+        "name": "overtrue/wechat",
+        "version": "4.2.11",
+        "version_normalized": "4.2.11.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/w7corp/easywechat.git",
+            "reference": "853e0772e6aa53a71edf1b5d251c7ff1e6b2a2bf"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/w7corp/easywechat/zipball/853e0772e6aa53a71edf1b5d251c7ff1e6b2a2bf",
+            "reference": "853e0772e6aa53a71edf1b5d251c7ff1e6b2a2bf",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "easywechat-composer/easywechat-composer": "^1.1",
+            "ext-fileinfo": "*",
+            "ext-openssl": "*",
+            "ext-simplexml": "*",
+            "guzzlehttp/guzzle": "^6.2",
+            "monolog/monolog": "^1.22 || ^2.0",
+            "overtrue/socialite": "~2.0",
+            "php": ">=7.1",
+            "pimple/pimple": "^3.0",
+            "psr/simple-cache": "^1.0",
+            "symfony/cache": "^3.3 || ^4.3",
+            "symfony/event-dispatcher": "^4.3",
+            "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0",
+            "symfony/psr-http-message-bridge": "^0.3 || ^1.0"
+        },
+        "require-dev": {
+            "friendsofphp/php-cs-fixer": "^2.15",
+            "mikey179/vfsstream": "^1.6",
+            "mockery/mockery": "^1.2.3",
+            "phpstan/phpstan": "^0.11.12",
+            "phpunit/phpunit": "^7.5"
+        },
+        "time": "2019-11-27 16:38:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "EasyWeChat\\": "src/"
+            },
+            "files": [
+                "src/Kernel/Support/Helpers.php",
+                "src/Kernel/Helpers.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "overtrue",
+                "email": "anzhengchao@gmail.com"
+            }
+        ],
+        "description": "微信SDK",
+        "keywords": [
+            "sdk",
+            "wechat",
+            "weixin",
+            "weixin-sdk"
+        ]
+    },
+    {
+        "name": "topthink/think-captcha",
+        "version": "v1.0.7",
+        "version_normalized": "1.0.7.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/top-think/think-captcha.git",
+            "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/top-think/think-captcha/zipball/0c55455df26a1626a60d0dc35d2d89002b741d44",
+            "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "time": "2016-07-06 01:47:11",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "think\\captcha\\": "src/"
+            },
+            "files": [
+                "src/helper.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "yunwuxin",
+                "email": "448901948@qq.com"
+            }
+        ],
+        "description": "captcha package for thinkphp5"
+    },
+    {
+        "name": "nelexa/zip",
+        "version": "3.3.3",
+        "version_normalized": "3.3.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/Ne-Lexa/php-zip.git",
+            "reference": "501b52f6fc393a599b44ff348a42740e1eaac7c6"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/Ne-Lexa/php-zip/zipball/501b52f6fc393a599b44ff348a42740e1eaac7c6",
+            "reference": "501b52f6fc393a599b44ff348a42740e1eaac7c6",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "ext-zlib": "*",
+            "paragonie/random_compat": "*",
+            "php": "^5.5.9 || ^7.0",
+            "psr/http-message": "^1.0",
+            "symfony/finder": "^3.0|^4.0|^5.0"
+        },
+        "require-dev": {
+            "ext-bz2": "*",
+            "ext-fileinfo": "*",
+            "ext-openssl": "*",
+            "ext-xml": "*",
+            "guzzlehttp/psr7": "^1.6",
+            "phpunit/phpunit": "^4.8|^5.7",
+            "symfony/var-dumper": "^3.0|^4.0|^5.0"
+        },
+        "suggest": {
+            "ext-bz2": "Needed to support BZIP2 compression",
+            "ext-fileinfo": "Needed to get mime-type file",
+            "ext-mcrypt": "Needed to support encrypt zip entries or use ext-openssl",
+            "ext-openssl": "Needed to support encrypt zip entries or use ext-mcrypt"
+        },
+        "time": "2020-07-11 21:01:42",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "PhpZip\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Ne-Lexa",
+                "email": "alexey@nelexa.ru",
+                "role": "Developer"
+            }
+        ],
+        "description": "PhpZip is a php-library for extended work with ZIP-archives. Open, create, update, delete, extract and get info tool. Supports appending to existing ZIP files, WinZip AES encryption, Traditional PKWARE Encryption, ZipAlign tool, BZIP2 compression, external file attributes and ZIP64 extensions. Alternative ZipArchive. It does not require php-zip extension.",
+        "homepage": "https://github.com/Ne-Lexa/php-zip",
+        "keywords": [
+            "archive",
+            "extract",
+            "unzip",
+            "winzip",
+            "zip",
+            "zipalign",
+            "ziparchive"
+        ]
+    },
+    {
+        "name": "overtrue/pinyin",
+        "version": "3.0.6",
+        "version_normalized": "3.0.6.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/overtrue/pinyin.git",
+            "reference": "3b781d267197b74752daa32814d3a2cf5d140779"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/overtrue/pinyin/zipball/3b781d267197b74752daa32814d3a2cf5d140779",
+            "reference": "3b781d267197b74752daa32814d3a2cf5d140779",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "~4.8"
+        },
+        "time": "2017-07-10 07:20:01",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Overtrue\\Pinyin\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Carlos",
+                "homepage": "http://github.com/overtrue"
+            }
+        ],
+        "description": "Chinese to pinyin translator.",
+        "homepage": "https://github.com/overtrue/pinyin",
+        "keywords": [
+            "Chinese",
+            "Pinyin",
+            "cn2pinyin"
+        ]
+    },
+    {
+        "name": "txthinking/mailer",
+        "version": "v2.0.1",
+        "version_normalized": "2.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/txthinking/Mailer.git",
+            "reference": "09013cf9dad3aac195f66ae5309e8c3343c018e9"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/txthinking/Mailer/zipball/09013cf9dad3aac195f66ae5309e8c3343c018e9",
+            "reference": "09013cf9dad3aac195f66ae5309e8c3343c018e9",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3.2",
+            "psr/log": "~1.0"
+        },
+        "require-dev": {
+            "monolog/monolog": "~1.13",
+            "phpunit/phpunit": "~4.0"
+        },
+        "time": "2018-10-09 10:47:23",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Tx\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Cloud",
+                "email": "cloud@txthinking.com",
+                "homepage": "http://www.txthinking.com",
+                "role": "Thinker"
+            },
+            {
+                "name": "Matt Sowers",
+                "email": "msowers@erblearn.org"
+            }
+        ],
+        "description": "A very lightweight PHP SMTP mail sender",
+        "homepage": "http://github.com/txthinking/Mailer",
+        "keywords": [
+            "mail",
+            "smtp"
+        ]
+    },
+    {
+        "name": "topthink/framework",
+        "version": "dev-master",
+        "version_normalized": "9999999-dev",
+        "source": {
+            "type": "git",
+            "url": "https://gitee.com/karson/framework",
+            "reference": "7d08e64b4d8e3352c0f855e63513d85aeaa41349"
+        },
+        "require": {
+            "php": ">=5.4.0",
+            "topthink/think-installer": "~1.0"
+        },
+        "require-dev": {
+            "johnkary/phpunit-speedtrap": "^1.0",
+            "mikey179/vfsstream": "~1.6",
+            "phpdocumentor/reflection-docblock": "^2.0",
+            "phploc/phploc": "2.*",
+            "phpunit/phpunit": "4.8.*",
+            "sebastian/phpcpd": "2.*"
+        },
+        "time": "2021-03-17 09:43:15",
+        "type": "think-framework",
+        "installation-source": "source",
+        "autoload": {
+            "psr-4": {
+                "think\\": "library/think"
+            }
+        },
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "liu21st",
+                "email": "liu21st@gmail.com"
+            }
+        ],
+        "description": "the new thinkphp framework",
+        "homepage": "http://thinkphp.cn/",
+        "keywords": [
+            "ORM",
+            "framework",
+            "thinkphp"
+        ]
+    },
+    {
+        "name": "easywechat-composer/easywechat-composer",
+        "version": "1.4.1",
+        "version_normalized": "1.4.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/mingyoung/easywechat-composer.git",
+            "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/mingyoung/easywechat-composer/zipball/3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd",
+            "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "composer-plugin-api": "^1.0 || ^2.0",
+            "php": ">=7.0"
+        },
+        "require-dev": {
+            "composer/composer": "^1.0 || ^2.0",
+            "phpunit/phpunit": "^6.5 || ^7.0"
+        },
+        "time": "2021-07-05 04:03:22",
+        "type": "composer-plugin",
+        "extra": {
+            "class": "EasyWeChatComposer\\Plugin"
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "EasyWeChatComposer\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "张铭阳",
+                "email": "mingyoungcheung@gmail.com"
+            }
+        ],
+        "description": "The composer plugin for EasyWeChat"
+    },
+    {
+        "name": "symfony/polyfill-php80",
+        "version": "v1.23.1",
+        "version_normalized": "1.23.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-php80.git",
+            "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
+            "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1"
+        },
+        "time": "2021-07-28 13:41:28",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.23-dev"
+            },
+            "thanks": {
+                "name": "symfony/polyfill",
+                "url": "https://github.com/symfony/polyfill"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Php80\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ],
+            "classmap": [
+                "Resources/stubs"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Ion Bazan",
+                "email": "ion.bazan@gmail.com"
+            },
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "symfony/var-exporter",
+        "version": "v4.4.34",
+        "version_normalized": "4.4.34.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/var-exporter.git",
+            "reference": "75a297f25a87ce9343d39241679578886f3fd458"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/var-exporter/zipball/75a297f25a87ce9343d39241679578886f3fd458",
+            "reference": "75a297f25a87ce9343d39241679578886f3fd458",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3",
+            "symfony/polyfill-php80": "^1.16"
+        },
+        "require-dev": {
+            "symfony/var-dumper": "^4.4.9|^5.0.9"
+        },
+        "time": "2021-11-22 10:04:59",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\VarExporter\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Allows exporting any serializable PHP data structure to plain PHP code",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "clone",
+            "construct",
+            "export",
+            "hydrate",
+            "instantiate",
+            "serialize"
+        ]
+    },
+    {
+        "name": "symfony/finder",
+        "version": "v4.4.36",
+        "version_normalized": "4.4.36.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/finder.git",
+            "reference": "1fef05633cd61b629e963e5d8200fb6b67ecf42c"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/finder/zipball/1fef05633cd61b629e963e5d8200fb6b67ecf42c",
+            "reference": "1fef05633cd61b629e963e5d8200fb6b67ecf42c",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3",
+            "symfony/polyfill-php80": "^1.16"
+        },
+        "time": "2021-12-15 10:33:10",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\Finder\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Fabien Potencier",
+                "email": "fabien@symfony.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Finds files and directories via an intuitive fluent interface",
+        "homepage": "https://symfony.com"
+    },
+    {
+        "name": "psr/log",
+        "version": "1.1.4",
+        "version_normalized": "1.1.4.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/log.git",
+            "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+            "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2021-05-03 11:20:27",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.1.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Log\\": "Psr/Log/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "https://www.php-fig.org/"
+            }
+        ],
+        "description": "Common interface for logging libraries",
+        "homepage": "https://github.com/php-fig/log",
+        "keywords": [
+            "log",
+            "psr",
+            "psr-3"
+        ]
+    },
+    {
+        "name": "monolog/monolog",
+        "version": "1.26.1",
+        "version_normalized": "1.26.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/Seldaek/monolog.git",
+            "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c6b00f05152ae2c9b04a448f99c7590beb6042f5",
+            "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.3.0",
+            "psr/log": "~1.0"
+        },
+        "provide": {
+            "psr/log-implementation": "1.0.0"
+        },
+        "require-dev": {
+            "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+            "doctrine/couchdb": "~1.0@dev",
+            "graylog2/gelf-php": "~1.0",
+            "php-amqplib/php-amqplib": "~2.4",
+            "php-console/php-console": "^3.1.3",
+            "phpstan/phpstan": "^0.12.59",
+            "phpunit/phpunit": "~4.5",
+            "ruflin/elastica": ">=0.90 <3.0",
+            "sentry/sentry": "^0.13",
+            "swiftmailer/swiftmailer": "^5.3|^6.0"
+        },
+        "suggest": {
+            "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+            "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+            "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+            "ext-mongo": "Allow sending log messages to a MongoDB server",
+            "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+            "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+            "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+            "php-console/php-console": "Allow sending log messages to Google Chrome",
+            "rollbar/rollbar": "Allow sending log messages to Rollbar",
+            "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+            "sentry/sentry": "Allow sending log messages to a Sentry server"
+        },
+        "time": "2021-05-28 08:32:12",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Monolog\\": "src/Monolog"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Jordi Boggiano",
+                "email": "j.boggiano@seld.be",
+                "homepage": "http://seld.be"
+            }
+        ],
+        "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+        "homepage": "http://github.com/Seldaek/monolog",
+        "keywords": [
+            "log",
+            "logging",
+            "psr-3"
+        ]
+    },
+    {
+        "name": "symfony/polyfill-mbstring",
+        "version": "v1.23.1",
+        "version_normalized": "1.23.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-mbstring.git",
+            "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
+            "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1"
+        },
+        "suggest": {
+            "ext-mbstring": "For best performance"
+        },
+        "time": "2021-05-27 12:26:48",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.23-dev"
+            },
+            "thanks": {
+                "name": "symfony/polyfill",
+                "url": "https://github.com/symfony/polyfill"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Mbstring\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill for the Mbstring extension",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "mbstring",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "symfony/polyfill-php72",
+        "version": "v1.23.0",
+        "version_normalized": "1.23.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-php72.git",
+            "reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
+            "reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1"
+        },
+        "time": "2021-05-27 09:17:38",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.23-dev"
+            },
+            "thanks": {
+                "name": "symfony/polyfill",
+                "url": "https://github.com/symfony/polyfill"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Php72\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "symfony/polyfill-intl-normalizer",
+        "version": "v1.23.0",
+        "version_normalized": "1.23.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+            "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
+            "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1"
+        },
+        "suggest": {
+            "ext-intl": "For best performance"
+        },
+        "time": "2021-02-19 12:13:01",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.23-dev"
+            },
+            "thanks": {
+                "name": "symfony/polyfill",
+                "url": "https://github.com/symfony/polyfill"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ],
+            "classmap": [
+                "Resources/stubs"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill for intl's Normalizer class and related functions",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "intl",
+            "normalizer",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "symfony/polyfill-intl-idn",
+        "version": "v1.23.0",
+        "version_normalized": "1.23.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-intl-idn.git",
+            "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/65bd267525e82759e7d8c4e8ceea44f398838e65",
+            "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1",
+            "symfony/polyfill-intl-normalizer": "^1.10",
+            "symfony/polyfill-php72": "^1.10"
+        },
+        "suggest": {
+            "ext-intl": "For best performance"
+        },
+        "time": "2021-05-27 09:27:20",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.23-dev"
+            },
+            "thanks": {
+                "name": "symfony/polyfill",
+                "url": "https://github.com/symfony/polyfill"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Intl\\Idn\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Laurent Bassin",
+                "email": "laurent@bassin.info"
+            },
+            {
+                "name": "Trevor Rowbotham",
+                "email": "trevor.rowbotham@pm.me"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "idn",
+            "intl",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "symfony/mime",
+        "version": "v4.4.36",
+        "version_normalized": "4.4.36.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/mime.git",
+            "reference": "fee42d10c8920b2308f466269cbf924ddc4fce94"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/mime/zipball/fee42d10c8920b2308f466269cbf924ddc4fce94",
+            "reference": "fee42d10c8920b2308f466269cbf924ddc4fce94",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3",
+            "symfony/polyfill-intl-idn": "^1.10",
+            "symfony/polyfill-mbstring": "^1.0",
+            "symfony/polyfill-php80": "^1.16"
+        },
+        "conflict": {
+            "egulias/email-validator": "~3.0.0",
+            "symfony/mailer": "<4.4"
+        },
+        "require-dev": {
+            "egulias/email-validator": "^2.1.10|^3.1",
+            "symfony/dependency-injection": "^3.4|^4.1|^5.0"
+        },
+        "time": "2021-12-25 19:39:39",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\Mime\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Fabien Potencier",
+                "email": "fabien@symfony.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Allows manipulating MIME messages",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "mime",
+            "mime-type"
+        ]
+    },
+    {
+        "name": "symfony/http-foundation",
+        "version": "v4.4.36",
+        "version_normalized": "4.4.36.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/http-foundation.git",
+            "reference": "0948e99457615ddb05380adde3584484ffd951d4"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0948e99457615ddb05380adde3584484ffd951d4",
+            "reference": "0948e99457615ddb05380adde3584484ffd951d4",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3",
+            "symfony/mime": "^4.3|^5.0",
+            "symfony/polyfill-mbstring": "~1.1",
+            "symfony/polyfill-php80": "^1.16"
+        },
+        "require-dev": {
+            "predis/predis": "~1.0",
+            "symfony/expression-language": "^3.4|^4.0|^5.0"
+        },
+        "time": "2021-12-27 18:40:22",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\HttpFoundation\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Fabien Potencier",
+                "email": "fabien@symfony.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Defines an object-oriented layer for the HTTP specification",
+        "homepage": "https://symfony.com"
+    },
+    {
+        "name": "guzzlehttp/psr7",
+        "version": "1.8.3",
+        "version_normalized": "1.8.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/guzzle/psr7.git",
+            "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/guzzle/psr7/zipball/1afdd860a2566ed3c2b0b4a3de6e23434a79ec85",
+            "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.4.0",
+            "psr/http-message": "~1.0",
+            "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
+        },
+        "provide": {
+            "psr/http-message-implementation": "1.0"
+        },
+        "require-dev": {
+            "ext-zlib": "*",
+            "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
+        },
+        "suggest": {
+            "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+        },
+        "time": "2021-10-05 13:56:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.7-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "GuzzleHttp\\Psr7\\": "src/"
+            },
+            "files": [
+                "src/functions_include.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Graham Campbell",
+                "email": "hello@gjcampbell.co.uk",
+                "homepage": "https://github.com/GrahamCampbell"
+            },
+            {
+                "name": "Michael Dowling",
+                "email": "mtdowling@gmail.com",
+                "homepage": "https://github.com/mtdowling"
+            },
+            {
+                "name": "George Mponos",
+                "email": "gmponos@gmail.com",
+                "homepage": "https://github.com/gmponos"
+            },
+            {
+                "name": "Tobias Nyholm",
+                "email": "tobias.nyholm@gmail.com",
+                "homepage": "https://github.com/Nyholm"
+            },
+            {
+                "name": "Márk Sági-Kazár",
+                "email": "mark.sagikazar@gmail.com",
+                "homepage": "https://github.com/sagikazarmark"
+            },
+            {
+                "name": "Tobias Schultze",
+                "email": "webmaster@tubo-world.de",
+                "homepage": "https://github.com/Tobion"
+            }
+        ],
+        "description": "PSR-7 message implementation that also provides common utility methods",
+        "keywords": [
+            "http",
+            "message",
+            "psr-7",
+            "request",
+            "response",
+            "stream",
+            "uri",
+            "url"
+        ]
+    },
+    {
+        "name": "guzzlehttp/promises",
+        "version": "1.5.1",
+        "version_normalized": "1.5.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/guzzle/promises.git",
+            "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
+            "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=5.5"
+        },
+        "require-dev": {
+            "symfony/phpunit-bridge": "^4.4 || ^5.1"
+        },
+        "time": "2021-10-22 20:56:57",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.5-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "GuzzleHttp\\Promise\\": "src/"
+            },
+            "files": [
+                "src/functions_include.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Graham Campbell",
+                "email": "hello@gjcampbell.co.uk",
+                "homepage": "https://github.com/GrahamCampbell"
+            },
+            {
+                "name": "Michael Dowling",
+                "email": "mtdowling@gmail.com",
+                "homepage": "https://github.com/mtdowling"
+            },
+            {
+                "name": "Tobias Nyholm",
+                "email": "tobias.nyholm@gmail.com",
+                "homepage": "https://github.com/Nyholm"
+            },
+            {
+                "name": "Tobias Schultze",
+                "email": "webmaster@tubo-world.de",
+                "homepage": "https://github.com/Tobion"
+            }
+        ],
+        "description": "Guzzle promises library",
+        "keywords": [
+            "promise"
+        ]
+    },
+    {
+        "name": "overtrue/socialite",
+        "version": "2.0.24",
+        "version_normalized": "2.0.24.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/overtrue/socialite.git",
+            "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/overtrue/socialite/zipball/ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec",
+            "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "ext-json": "*",
+            "guzzlehttp/guzzle": "^5.0|^6.0|^7.0",
+            "php": ">=5.6",
+            "symfony/http-foundation": "^2.7|^3.0|^4.0|^5.0"
+        },
+        "require-dev": {
+            "mockery/mockery": "~1.2",
+            "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0"
+        },
+        "time": "2021-05-13 16:04:48",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Overtrue\\Socialite\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "overtrue",
+                "email": "anzhengchao@gmail.com"
+            }
+        ],
+        "description": "A collection of OAuth 2 packages that extracts from laravel/socialite.",
+        "keywords": [
+            "login",
+            "oauth",
+            "qq",
+            "social",
+            "wechat",
+            "weibo"
+        ]
+    },
+    {
+        "name": "symfony/service-contracts",
+        "version": "v1.1.11",
+        "version_normalized": "1.1.11.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/service-contracts.git",
+            "reference": "633df678bec3452e04a7b0337c9bcfe7354124b3"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/service-contracts/zipball/633df678bec3452e04a7b0337c9bcfe7354124b3",
+            "reference": "633df678bec3452e04a7b0337c9bcfe7354124b3",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3",
+            "psr/container": "^1.0"
+        },
+        "suggest": {
+            "symfony/service-implementation": ""
+        },
+        "time": "2021-11-04 13:32:43",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.1-dev"
+            },
+            "thanks": {
+                "name": "symfony/contracts",
+                "url": "https://github.com/symfony/contracts"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Contracts\\Service\\": ""
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Generic abstractions related to writing services",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "abstractions",
+            "contracts",
+            "decoupling",
+            "interfaces",
+            "interoperability",
+            "standards"
+        ]
+    },
+    {
+        "name": "symfony/polyfill-php73",
+        "version": "v1.23.0",
+        "version_normalized": "1.23.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-php73.git",
+            "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010",
+            "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1"
+        },
+        "time": "2021-02-19 12:13:01",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.23-dev"
+            },
+            "thanks": {
+                "name": "symfony/polyfill",
+                "url": "https://github.com/symfony/polyfill"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Php73\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ],
+            "classmap": [
+                "Resources/stubs"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "symfony/cache-contracts",
+        "version": "v1.1.11",
+        "version_normalized": "1.1.11.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/cache-contracts.git",
+            "reference": "41c956506500bea5502022f6be81da96fb9c7626"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/41c956506500bea5502022f6be81da96fb9c7626",
+            "reference": "41c956506500bea5502022f6be81da96fb9c7626",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3",
+            "psr/cache": "^1.0|^2.0|^3.0"
+        },
+        "suggest": {
+            "symfony/cache-implementation": ""
+        },
+        "time": "2021-07-13 09:33:53",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.1-dev"
+            },
+            "thanks": {
+                "name": "symfony/contracts",
+                "url": "https://github.com/symfony/contracts"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Contracts\\Cache\\": ""
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Generic abstractions related to caching",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "abstractions",
+            "contracts",
+            "decoupling",
+            "interfaces",
+            "interoperability",
+            "standards"
+        ]
+    },
+    {
+        "name": "symfony/cache",
+        "version": "v4.4.36",
+        "version_normalized": "4.4.36.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/cache.git",
+            "reference": "1caa6c63f0ebf3022b88263a2b90260cff33f6dc"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/cache/zipball/1caa6c63f0ebf3022b88263a2b90260cff33f6dc",
+            "reference": "1caa6c63f0ebf3022b88263a2b90260cff33f6dc",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3",
+            "psr/cache": "^1.0|^2.0",
+            "psr/log": "^1|^2|^3",
+            "symfony/cache-contracts": "^1.1.7|^2",
+            "symfony/polyfill-php73": "^1.9",
+            "symfony/polyfill-php80": "^1.16",
+            "symfony/service-contracts": "^1.1|^2",
+            "symfony/var-exporter": "^4.2|^5.0"
+        },
+        "conflict": {
+            "doctrine/dbal": "<2.7",
+            "symfony/dependency-injection": "<3.4",
+            "symfony/http-kernel": "<4.4|>=5.0",
+            "symfony/var-dumper": "<4.4"
+        },
+        "provide": {
+            "psr/cache-implementation": "1.0|2.0",
+            "psr/simple-cache-implementation": "1.0|2.0",
+            "symfony/cache-implementation": "1.0|2.0"
+        },
+        "require-dev": {
+            "cache/integration-tests": "dev-master",
+            "doctrine/cache": "^1.6|^2.0",
+            "doctrine/dbal": "^2.7|^3.0",
+            "predis/predis": "^1.1",
+            "psr/simple-cache": "^1.0|^2.0",
+            "symfony/config": "^4.2|^5.0",
+            "symfony/dependency-injection": "^3.4|^4.1|^5.0",
+            "symfony/filesystem": "^4.4|^5.0",
+            "symfony/http-kernel": "^4.4",
+            "symfony/var-dumper": "^4.4|^5.0"
+        },
+        "time": "2021-12-28 10:59:50",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\Cache\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Provides an extended PSR-6, PSR-16 (and tags) implementation",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "caching",
+            "psr6"
+        ]
+    },
+    {
+        "name": "symfony/event-dispatcher-contracts",
+        "version": "v1.1.11",
+        "version_normalized": "1.1.11.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+            "reference": "01e9a4efac0ee33a05dfdf93b346f62e7d0e998c"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/01e9a4efac0ee33a05dfdf93b346f62e7d0e998c",
+            "reference": "01e9a4efac0ee33a05dfdf93b346f62e7d0e998c",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3"
+        },
+        "suggest": {
+            "psr/event-dispatcher": "",
+            "symfony/event-dispatcher-implementation": ""
+        },
+        "time": "2021-03-23 15:25:38",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-main": "1.1-dev"
+            },
+            "thanks": {
+                "name": "symfony/contracts",
+                "url": "https://github.com/symfony/contracts"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Contracts\\EventDispatcher\\": ""
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Generic abstractions related to dispatching event",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "abstractions",
+            "contracts",
+            "decoupling",
+            "interfaces",
+            "interoperability",
+            "standards"
+        ]
+    },
+    {
+        "name": "symfony/event-dispatcher",
+        "version": "v4.4.34",
+        "version_normalized": "4.4.34.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/event-dispatcher.git",
+            "reference": "1a024b45369c9d55d76b6b8a241bd20c9ea1cbd8"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1a024b45369c9d55d76b6b8a241bd20c9ea1cbd8",
+            "reference": "1a024b45369c9d55d76b6b8a241bd20c9ea1cbd8",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "php": ">=7.1.3",
+            "symfony/event-dispatcher-contracts": "^1.1",
+            "symfony/polyfill-php80": "^1.16"
+        },
+        "conflict": {
+            "symfony/dependency-injection": "<3.4"
+        },
+        "provide": {
+            "psr/event-dispatcher-implementation": "1.0",
+            "symfony/event-dispatcher-implementation": "1.1"
+        },
+        "require-dev": {
+            "psr/log": "^1|^2|^3",
+            "symfony/config": "^3.4|^4.0|^5.0",
+            "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+            "symfony/error-handler": "~3.4|~4.4",
+            "symfony/expression-language": "^3.4|^4.0|^5.0",
+            "symfony/http-foundation": "^3.4|^4.0|^5.0",
+            "symfony/service-contracts": "^1.1|^2",
+            "symfony/stopwatch": "^3.4|^4.0|^5.0"
+        },
+        "suggest": {
+            "symfony/dependency-injection": "",
+            "symfony/http-kernel": ""
+        },
+        "time": "2021-11-15 14:42:25",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\EventDispatcher\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Fabien Potencier",
+                "email": "fabien@symfony.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+        "homepage": "https://symfony.com"
+    },
+    {
+        "name": "karsonzhang/fastadmin-addons",
+        "version": "1.3.2",
+        "version_normalized": "1.3.2.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/karsonzhang/fastadmin-addons.git",
+            "reference": "b62f656466811df79586f72bbfbc4636b1c0507d"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/karsonzhang/fastadmin-addons/zipball/b62f656466811df79586f72bbfbc4636b1c0507d",
+            "reference": "b62f656466811df79586f72bbfbc4636b1c0507d",
+            "shasum": "",
+            "mirrors": [
+                {
+                    "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                    "preferred": true
+                }
+            ]
+        },
+        "require": {
+            "nelexa/zip": "^3.3",
+            "php": ">=7.0.0",
+            "symfony/var-exporter": "^4.4.13"
+        },
+        "time": "2022-01-20 10:03:48",
+        "type": "library",
+        "extra": {
+            "think-config": {
+                "addons": "src/config.php"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "think\\": "src/"
+            },
+            "files": [
+                "src/common.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "Karson",
+                "email": "karson@fastadmin.net"
+            },
+            {
+                "name": "xiaobo.sun",
+                "email": "xiaobo.sun@qq.com"
+            }
+        ],
+        "description": "addons package for fastadmin",
+        "homepage": "https://github.com/karsonzhang/fastadmin-addons"
+    }
+]

+ 38 - 0
application/vendor/karsonzhang/fastadmin-addons/composer.json

@@ -0,0 +1,38 @@
+{
+    "name": "karsonzhang/fastadmin-addons",
+    "description": "addons package for fastadmin",
+    "homepage": "https://github.com/karsonzhang/fastadmin-addons",
+    "license": "Apache-2.0",
+    "version": "1.3.2",
+    "authors": [
+        {
+            "name": "Karson",
+            "email": "karson@fastadmin.net"
+        },
+        {
+            "name": "xiaobo.sun",
+            "email": "xiaobo.sun@qq.com"
+        }
+    ],
+    "support": {
+        "issues": "https://github.com/karsonzhang/fastadmin-addons/issues"
+    },
+    "require": {
+        "php": ">=7.0.0",
+        "nelexa/zip": "^3.3",
+        "symfony/var-exporter": "^4.4.13"
+    },
+    "autoload": {
+        "psr-4": {
+            "think\\": "src/"
+        },
+        "files": [
+            "src/common.php"
+        ]
+    },
+    "extra": {
+        "think-config": {
+            "addons": "src/config.php"
+        }
+    }
+}

+ 1150 - 0
application/vendor/karsonzhang/fastadmin-addons/src/addons/Service.php

@@ -0,0 +1,1150 @@
+<?php
+
+namespace think\addons;
+
+use fast\Http;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\TransferException;
+use GuzzleHttp\TransferStats;
+use PhpZip\Exception\ZipException;
+use PhpZip\ZipFile;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use Symfony\Component\VarExporter\VarExporter;
+use think\Cache;
+use think\Db;
+use think\Exception;
+use think\Log;
+
+/**
+ * 插件服务
+ * @package think\addons
+ */
+class Service
+{
+    /**
+     * 插件列表
+     */
+    public static function addons($params = [])
+    {
+        $params['domain'] = request()->host(true);
+        return self::sendRequest('/addon/index', $params, 'GET');
+    }
+
+    /**
+     * 检测插件是否购买授权
+     */
+    public static function isBuy($name, $extend = [])
+    {
+        $params = array_merge(['name' => $name, 'domain' => request()->host(true)], $extend);
+        return self::sendRequest('/addon/isbuy', $params, 'POST');
+    }
+
+    /**
+     * 检测插件是否授权
+     *
+     * @param string $name   插件名称
+     * @param string $domain 验证域名
+     */
+    public static function isAuthorization($name, $domain = '')
+    {
+        $config = self::config($name);
+        $request = request();
+        $domain = self::getRootDomain($domain ? $domain : $request->host(true));
+        if (isset($config['domains']) && isset($config['domains']) && isset($config['validations']) && isset($config['licensecodes'])) {
+            $index = array_search($domain, $config['domains']);
+            if ((in_array($domain, $config['domains']) && in_array(md5(md5($domain) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) || $request->isCli()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 远程下载插件
+     *
+     * @param string $name   插件名称
+     * @param array  $extend 扩展参数
+     * @return  string
+     */
+    public static function download($name, $extend = [])
+    {
+        $addonsTempDir = self::getAddonsBackupDir();
+        $tmpFile = $addonsTempDir . $name . ".zip";
+        try {
+            $client = self::getClient();
+            $response = $client->get('/addon/download', ['query' => array_merge(['name' => $name], $extend)]);
+            $body = $response->getBody();
+            $content = $body->getContents();
+            if (substr($content, 0, 1) === '{') {
+                $json = (array)json_decode($content, true);
+                //如果传回的是一个下载链接,则再次下载
+                if ($json['data'] && isset($json['data']['url'])) {
+                    $response = $client->get($json['data']['url']);
+                    $body = $response->getBody();
+                    $content = $body->getContents();
+                } else {
+                    //下载返回错误,抛出异常
+                    throw new AddonException($json['msg'], $json['code'], $json['data']);
+                }
+            }
+        } catch (TransferException $e) {
+            throw new Exception(config('app_debug') ? $e->getMessage() : "Addon package download failed");
+        }
+
+        if ($write = fopen($tmpFile, 'w')) {
+            fwrite($write, $content);
+            fclose($write);
+            return $tmpFile;
+        }
+        throw new Exception(config('app_debug') && isset($content) ? $content : "No permission to write temporary files");
+    }
+
+    /**
+     * 解压插件
+     *
+     * @param string $name 插件名称
+     * @return  string
+     * @throws  Exception
+     */
+    public static function unzip($name)
+    {
+        if (!$name) {
+            throw new Exception('Invalid parameters');
+        }
+        $addonsBackupDir = self::getAddonsBackupDir();
+        $file = $addonsBackupDir . $name . '.zip';
+
+        // 打开插件压缩包
+        $zip = new ZipFile();
+        try {
+            $zip->openFile($file);
+        } catch (ZipException $e) {
+            $zip->close();
+            throw new Exception(config('app_debug') ? $e->getMessage() : 'Unable to open the zip file');
+        }
+
+        $dir = self::getAddonDir($name);
+        if (!is_dir($dir)) {
+            @mkdir($dir, 0755);
+        }
+
+        // 解压插件压缩包
+        try {
+            $zip->extractTo($dir);
+        } catch (ZipException $e) {
+            throw new Exception(config('app_debug') ? $e->getMessage() : 'Unable to extract the file');
+        } finally {
+            $zip->close();
+        }
+        return $dir;
+    }
+
+    /**
+     * 离线安装
+     * @param string $file 插件压缩包
+     * @param array  $extend
+     */
+    public static function local($file, $extend = [])
+    {
+        $addonsTempDir = self::getAddonsBackupDir();
+        if (!$file || !$file instanceof \think\File) {
+            throw new Exception('No file upload or server upload limit exceeded');
+        }
+        $uploadFile = $file->rule('uniqid')->validate(['size' => 102400000, 'ext' => 'zip,fastaddon'])->move($addonsTempDir);
+        if (!$uploadFile) {
+            // 上传失败获取错误信息
+            throw new Exception(__($file->getError()));
+        }
+        $tmpFile = $addonsTempDir . $uploadFile->getSaveName();
+
+        $info = [];
+        $zip = new ZipFile();
+        try {
+
+            // 打开插件压缩包
+            try {
+                $zip->openFile($tmpFile);
+            } catch (ZipException $e) {
+                @unlink($tmpFile);
+                throw new Exception('Unable to open the zip file');
+            }
+
+            $config = self::getInfoIni($zip);
+
+            // 判断插件标识
+            $name = isset($config['name']) ? $config['name'] : '';
+            if (!$name) {
+                throw new Exception('Addon info file data incorrect');
+            }
+
+            // 判断插件是否存在
+            if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+                throw new Exception('Addon name incorrect');
+            }
+
+            // 判断新插件是否存在
+            $newAddonDir = self::getAddonDir($name);
+            if (is_dir($newAddonDir)) {
+                throw new Exception('Addon already exists');
+            }
+
+            // 追加MD5和Data数据
+            $extend['md5'] = md5_file($tmpFile);
+            $extend['data'] = $zip->getArchiveComment();
+            $extend['unknownsources'] = config('app_debug') && config('fastadmin.unknownsources');
+            $extend['faversion'] = config('fastadmin.version');
+
+            $params = array_merge($config, $extend);
+
+            // 压缩包验证、版本依赖判断
+            Service::valid($params);
+
+            //创建插件目录
+            @mkdir($newAddonDir, 0755, true);
+
+            // 解压到插件目录
+            try {
+                $zip->extractTo($newAddonDir);
+            } catch (ZipException $e) {
+                @unlink($newAddonDir);
+                throw new Exception('Unable to extract the file');
+            }
+
+            Db::startTrans();
+            try {
+                //默认禁用该插件
+                $info = get_addon_info($name);
+                if ($info['state']) {
+                    $info['state'] = 0;
+                    set_addon_info($name, $info);
+                }
+
+                //执行插件的安装方法
+                $class = get_addon_class($name);
+                if (class_exists($class)) {
+                    $addon = new $class();
+                    $addon->install();
+                }
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+                @rmdirs($newAddonDir);
+                throw new Exception(__($e->getMessage()));
+            }
+
+            //导入SQL
+            Service::importsql($name);
+        } catch (AddonException $e) {
+            throw new AddonException($e->getMessage(), $e->getCode(), $e->getData());
+        } catch (Exception $e) {
+            throw new Exception(__($e->getMessage()));
+        } finally {
+            $zip->close();
+            unset($uploadFile);
+            @unlink($tmpFile);
+        }
+
+        $info['config'] = get_addon_config($name) ? 1 : 0;
+        $info['bootstrap'] = is_file(Service::getBootstrapFile($name));
+        $info['testdata'] = is_file(Service::getTestdataFile($name));
+        return $info;
+    }
+
+    /**
+     * 验证压缩包、依赖验证
+     * @param array $params
+     * @return bool
+     * @throws Exception
+     */
+    public static function valid($params = [])
+    {
+        $json = self::sendRequest('/addon/valid', $params, 'POST');
+        if ($json && isset($json['code'])) {
+            if ($json['code']) {
+                return true;
+            } else {
+                throw new Exception($json['msg'] ?? "Invalid addon package");
+            }
+        } else {
+            throw new Exception("Unknown data format");
+        }
+    }
+
+    /**
+     * 备份插件
+     * @param string $name 插件名称
+     * @return bool
+     * @throws Exception
+     */
+    public static function backup($name)
+    {
+        $addonsBackupDir = self::getAddonsBackupDir();
+        $file = $addonsBackupDir . $name . '-backup-' . date("YmdHis") . '.zip';
+        $zipFile = new ZipFile();
+        try {
+            $zipFile
+                ->addDirRecursive(self::getAddonDir($name))
+                ->saveAsFile($file)
+                ->close();
+        } catch (ZipException $e) {
+
+        } finally {
+            $zipFile->close();
+        }
+
+        return true;
+    }
+
+    /**
+     * 检测插件是否完整
+     *
+     * @param string $name 插件名称
+     * @return  boolean
+     * @throws  Exception
+     */
+    public static function check($name)
+    {
+        if (!$name || !is_dir(ADDON_PATH . $name)) {
+            throw new Exception('Addon not exists');
+        }
+        $addonClass = get_addon_class($name);
+        if (!$addonClass) {
+            throw new Exception("The addon file does not exist");
+        }
+        $addon = new $addonClass();
+        if (!$addon->checkInfo()) {
+            throw new Exception("The configuration file content is incorrect");
+        }
+        return true;
+    }
+
+    /**
+     * 是否有冲突
+     *
+     * @param string $name 插件名称
+     * @return  boolean
+     * @throws  AddonException
+     */
+    public static function noconflict($name)
+    {
+        // 检测冲突文件
+        $list = self::getGlobalFiles($name, true);
+        if ($list) {
+            //发现冲突文件,抛出异常
+            throw new AddonException(__("Conflicting file found"), -3, ['conflictlist' => $list]);
+        }
+        return true;
+    }
+
+    /**
+     * 导入SQL
+     *
+     * @param string $name     插件名称
+     * @param string $fileName SQL文件名称
+     * @return  boolean
+     */
+    public static function importsql($name, $fileName = null)
+    {
+        $fileName = is_null($fileName) ? 'install.sql' : $fileName;
+        $sqlFile = self::getAddonDir($name) . $fileName;
+        if (is_file($sqlFile)) {
+            $lines = file($sqlFile);
+            $templine = '';
+            foreach ($lines as $line) {
+                if (substr($line, 0, 2) == '--' || $line == '' || substr($line, 0, 2) == '/*') {
+                    continue;
+                }
+
+                $templine .= $line;
+                if (substr(trim($line), -1, 1) == ';') {
+                    $templine = str_ireplace('__PREFIX__', config('database.prefix'), $templine);
+                    $templine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $templine);
+                    try {
+                        Db::getPdo()->exec($templine);
+                    } catch (\PDOException $e) {
+                        //$e->getMessage();
+                    }
+                    $templine = '';
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 刷新插件缓存文件
+     *
+     * @return  boolean
+     * @throws  Exception
+     */
+    public static function refresh()
+    {
+        //刷新addons.js
+        $addons = get_addon_list();
+        $bootstrapArr = [];
+        foreach ($addons as $name => $addon) {
+            $bootstrapFile = self::getBootstrapFile($name);
+            if ($addon['state'] && is_file($bootstrapFile)) {
+                $bootstrapArr[] = file_get_contents($bootstrapFile);
+            }
+        }
+        $addonsFile = ROOT_PATH . str_replace("/", DS, "public/assets/js/addons.js");
+        if ($handle = fopen($addonsFile, 'w')) {
+            $tpl = <<<EOD
+define([], function () {
+    {__JS__}
+});
+EOD;
+            fwrite($handle, str_replace("{__JS__}", implode("\n", $bootstrapArr), $tpl));
+            fclose($handle);
+        } else {
+            throw new Exception(__("Unable to open file '%s' for writing", "addons.js"));
+        }
+
+        Cache::rm("addons");
+        Cache::rm("hooks");
+
+        $file = self::getExtraAddonsFile();
+
+        $config = get_addon_autoload_config(true);
+        if ($config['autoload']) {
+            return;
+        }
+
+        if (!is_really_writable($file)) {
+            throw new Exception(__("Unable to open file '%s' for writing", "addons.php"));
+        }
+
+        file_put_contents($file, "<?php\n\n" . "return " . VarExporter::export($config) . ";\n", LOCK_EX);
+        return true;
+    }
+
+    /**
+     * 安装插件
+     *
+     * @param string  $name   插件名称
+     * @param boolean $force  是否覆盖
+     * @param array   $extend 扩展参数
+     * @return  boolean
+     * @throws  Exception
+     * @throws  AddonException
+     */
+    public static function install($name, $force = false, $extend = [])
+    {
+        if (!$name || (is_dir(ADDON_PATH . $name) && !$force)) {
+            throw new Exception('Addon already exists');
+        }
+
+        $extend['domain'] = request()->host(true);
+
+        // 远程下载插件
+        $tmpFile = Service::download($name, $extend);
+
+        $addonDir = self::getAddonDir($name);
+
+        try {
+            // 解压插件压缩包到插件目录
+            Service::unzip($name);
+
+            // 检查插件是否完整
+            Service::check($name);
+
+            if (!$force) {
+                Service::noconflict($name);
+            }
+        } catch (AddonException $e) {
+            @rmdirs($addonDir);
+            throw new AddonException($e->getMessage(), $e->getCode(), $e->getData());
+        } catch (Exception $e) {
+            @rmdirs($addonDir);
+            throw new Exception($e->getMessage());
+        } finally {
+            // 移除临时文件
+            @unlink($tmpFile);
+        }
+
+        // 默认启用该插件
+        $info = get_addon_info($name);
+
+        Db::startTrans();
+        try {
+            if (!$info['state']) {
+                $info['state'] = 1;
+                set_addon_info($name, $info);
+            }
+
+            // 执行安装脚本
+            $class = get_addon_class($name);
+            if (class_exists($class)) {
+                $addon = new $class();
+                $addon->install();
+            }
+            Db::commit();
+        } catch (Exception $e) {
+            @rmdirs($addonDir);
+            Db::rollback();
+            throw new Exception($e->getMessage());
+        }
+
+        // 导入
+        Service::importsql($name);
+
+        // 启用插件
+        Service::enable($name, true);
+
+        $info['config'] = get_addon_config($name) ? 1 : 0;
+        $info['bootstrap'] = is_file(Service::getBootstrapFile($name));
+        $info['testdata'] = is_file(Service::getTestdataFile($name));
+        return $info;
+    }
+
+    /**
+     * 卸载插件
+     *
+     * @param string  $name
+     * @param boolean $force 是否强制卸载
+     * @return  boolean
+     * @throws  Exception
+     */
+    public static function uninstall($name, $force = false)
+    {
+        if (!$name || !is_dir(ADDON_PATH . $name)) {
+            throw new Exception('Addon not exists');
+        }
+
+        if (!$force) {
+            Service::noconflict($name);
+        }
+
+        // 移除插件全局资源文件
+        if ($force) {
+            $list = Service::getGlobalFiles($name);
+            foreach ($list as $k => $v) {
+                @unlink(ROOT_PATH . $v);
+            }
+        }
+
+        // 执行卸载脚本
+        try {
+            $class = get_addon_class($name);
+            if (class_exists($class)) {
+                $addon = new $class();
+                $addon->uninstall();
+            }
+        } catch (Exception $e) {
+            throw new Exception($e->getMessage());
+        }
+
+        // 移除插件目录
+        rmdirs(ADDON_PATH . $name);
+
+        // 刷新
+        Service::refresh();
+        return true;
+    }
+
+    /**
+     * 启用
+     * @param string  $name  插件名称
+     * @param boolean $force 是否强制覆盖
+     * @return  boolean
+     */
+    public static function enable($name, $force = false)
+    {
+        if (!$name || !is_dir(ADDON_PATH . $name)) {
+            throw new Exception('Addon not exists');
+        }
+
+        if (!$force) {
+            Service::noconflict($name);
+        }
+
+        //备份冲突文件
+        if (config('fastadmin.backup_global_files')) {
+            $conflictFiles = self::getGlobalFiles($name, true);
+            if ($conflictFiles) {
+                $zip = new ZipFile();
+                try {
+                    foreach ($conflictFiles as $k => $v) {
+                        $zip->addFile(ROOT_PATH . $v, $v);
+                    }
+                    $addonsBackupDir = self::getAddonsBackupDir();
+                    $zip->saveAsFile($addonsBackupDir . $name . "-conflict-enable-" . date("YmdHis") . ".zip");
+                } catch (Exception $e) {
+
+                } finally {
+                    $zip->close();
+                }
+            }
+        }
+
+        $addonDir = self::getAddonDir($name);
+        $sourceAssetsDir = self::getSourceAssetsDir($name);
+        $destAssetsDir = self::getDestAssetsDir($name);
+
+        $files = self::getGlobalFiles($name);
+        if ($files) {
+            //刷新插件配置缓存
+            Service::config($name, ['files' => $files]);
+        }
+
+        // 复制文件
+        if (is_dir($sourceAssetsDir)) {
+            copydirs($sourceAssetsDir, $destAssetsDir);
+        }
+
+        // 复制application和public到全局
+        foreach (self::getCheckDirs() as $k => $dir) {
+            if (is_dir($addonDir . $dir)) {
+                copydirs($addonDir . $dir, ROOT_PATH . $dir);
+            }
+        }
+
+        //插件纯净模式时将插件目录下的application、public和assets删除
+        if (config('fastadmin.addon_pure_mode')) {
+            // 删除插件目录已复制到全局的文件
+            @rmdirs($sourceAssetsDir);
+            foreach (self::getCheckDirs() as $k => $dir) {
+                @rmdirs($addonDir . $dir);
+            }
+        }
+
+        //执行启用脚本
+        try {
+            $class = get_addon_class($name);
+            if (class_exists($class)) {
+                $addon = new $class();
+                if (method_exists($class, "enable")) {
+                    $addon->enable();
+                }
+            }
+        } catch (Exception $e) {
+            throw new Exception($e->getMessage());
+        }
+
+        $info = get_addon_info($name);
+        $info['state'] = 1;
+        unset($info['url']);
+
+        set_addon_info($name, $info);
+
+        // 刷新
+        Service::refresh();
+        return true;
+    }
+
+    /**
+     * 禁用
+     *
+     * @param string  $name  插件名称
+     * @param boolean $force 是否强制禁用
+     * @return  boolean
+     * @throws  Exception
+     */
+    public static function disable($name, $force = false)
+    {
+        if (!$name || !is_dir(ADDON_PATH . $name)) {
+            throw new Exception('Addon not exists');
+        }
+
+        $file = self::getExtraAddonsFile();
+        if (!is_really_writable($file)) {
+            throw new Exception(__("Unable to open file '%s' for writing", "addons.php"));
+        }
+
+        if (!$force) {
+            Service::noconflict($name);
+        }
+
+        if (config('fastadmin.backup_global_files')) {
+            //仅备份修改过的文件
+            $conflictFiles = Service::getGlobalFiles($name, true);
+            if ($conflictFiles) {
+                $zip = new ZipFile();
+                try {
+                    foreach ($conflictFiles as $k => $v) {
+                        $zip->addFile(ROOT_PATH . $v, $v);
+                    }
+                    $addonsBackupDir = self::getAddonsBackupDir();
+                    $zip->saveAsFile($addonsBackupDir . $name . "-conflict-disable-" . date("YmdHis") . ".zip");
+                } catch (Exception $e) {
+
+                } finally {
+                    $zip->close();
+                }
+            }
+        }
+
+        $config = Service::config($name);
+
+        $addonDir = self::getAddonDir($name);
+        //插件资源目录
+        $destAssetsDir = self::getDestAssetsDir($name);
+
+        // 移除插件全局文件
+        $list = Service::getGlobalFiles($name);
+
+        //插件纯净模式时将原有的文件复制回插件目录
+        //当无法获取全局文件列表时也将列表复制回插件目录
+        if (config('fastadmin.addon_pure_mode') || !$list) {
+            if ($config && isset($config['files']) && is_array($config['files'])) {
+                foreach ($config['files'] as $index => $item) {
+                    //避免切换不同服务器后导致路径不一致
+                    $item = str_replace(['/', '\\'], DS, $item);
+                    //插件资源目录,无需重复复制
+                    if (stripos($item, str_replace(ROOT_PATH, '', $destAssetsDir)) === 0) {
+                        continue;
+                    }
+                    //检查目录是否存在,不存在则创建
+                    $itemBaseDir = dirname($addonDir . $item);
+                    if (!is_dir($itemBaseDir)) {
+                        @mkdir($itemBaseDir, 0755, true);
+                    }
+                    if (is_file(ROOT_PATH . $item)) {
+                        @copy(ROOT_PATH . $item, $addonDir . $item);
+                    }
+                }
+                $list = $config['files'];
+            }
+            //复制插件目录资源
+            if (is_dir($destAssetsDir)) {
+                @copydirs($destAssetsDir, $addonDir . 'assets' . DS);
+            }
+        }
+
+        $dirs = [];
+        foreach ($list as $k => $v) {
+            $file = ROOT_PATH . $v;
+            $dirs[] = dirname($file);
+            @unlink($file);
+        }
+
+        // 移除插件空目录
+        $dirs = array_filter(array_unique($dirs));
+        foreach ($dirs as $k => $v) {
+            remove_empty_folder($v);
+        }
+
+        $info = get_addon_info($name);
+        $info['state'] = 0;
+        unset($info['url']);
+
+        set_addon_info($name, $info);
+
+        // 执行禁用脚本
+        try {
+            $class = get_addon_class($name);
+            if (class_exists($class)) {
+                $addon = new $class();
+
+                if (method_exists($class, "disable")) {
+                    $addon->disable();
+                }
+            }
+        } catch (Exception $e) {
+            throw new Exception($e->getMessage());
+        }
+
+        // 刷新
+        Service::refresh();
+        return true;
+    }
+
+    /**
+     * 升级插件
+     *
+     * @param string $name   插件名称
+     * @param array  $extend 扩展参数
+     */
+    public static function upgrade($name, $extend = [])
+    {
+        $info = get_addon_info($name);
+        if ($info['state']) {
+            throw new Exception(__('Please disable addon first'));
+        }
+        $config = get_addon_config($name);
+        if ($config) {
+            //备份配置
+        }
+
+        // 远程下载插件
+        $tmpFile = Service::download($name, $extend);
+
+        // 备份插件文件
+        Service::backup($name);
+
+        $addonDir = self::getAddonDir($name);
+
+        // 删除插件目录下的application和public
+        $files = self::getCheckDirs();
+        foreach ($files as $index => $file) {
+            @rmdirs($addonDir . $file);
+        }
+
+        try {
+            // 解压插件
+            Service::unzip($name);
+        } catch (Exception $e) {
+            throw new Exception($e->getMessage());
+        } finally {
+            // 移除临时文件
+            @unlink($tmpFile);
+        }
+
+        if ($config) {
+            // 还原配置
+            set_addon_config($name, $config);
+        }
+
+        // 导入
+        Service::importsql($name);
+
+        // 执行升级脚本
+        try {
+            $addonName = ucfirst($name);
+            //创建临时类用于调用升级的方法
+            $sourceFile = $addonDir . $addonName . ".php";
+            $destFile = $addonDir . $addonName . "Upgrade.php";
+
+            $classContent = str_replace("class {$addonName} extends", "class {$addonName}Upgrade extends", file_get_contents($sourceFile));
+
+            //创建临时的类文件
+            file_put_contents($destFile, $classContent);
+
+            $className = "\\addons\\" . $name . "\\" . $addonName . "Upgrade";
+            $addon = new $className($name);
+
+            //调用升级的方法
+            if (method_exists($addon, "upgrade")) {
+                $addon->upgrade();
+            }
+
+            //移除临时文件
+            @unlink($destFile);
+        } catch (Exception $e) {
+            throw new Exception($e->getMessage());
+        }
+
+        // 刷新
+        Service::refresh();
+
+        //必须变更版本号
+        $info['version'] = isset($extend['version']) ? $extend['version'] : $info['version'];
+
+        $info['config'] = get_addon_config($name) ? 1 : 0;
+        $info['bootstrap'] = is_file(Service::getBootstrapFile($name));
+        return $info;
+    }
+
+    /**
+     * 读取或修改插件配置
+     * @param string $name
+     * @param array  $changed
+     * @return array
+     */
+    public static function config($name, $changed = [])
+    {
+        $addonDir = self::getAddonDir($name);
+        $addonConfigFile = $addonDir . '.addonrc';
+        $config = [];
+        if (is_file($addonConfigFile)) {
+            $config = (array)json_decode(file_get_contents($addonConfigFile), true);
+        }
+        $config = array_merge($config, $changed);
+        if ($changed) {
+            file_put_contents($addonConfigFile, json_encode($config, JSON_UNESCAPED_UNICODE));
+        }
+        return $config;
+    }
+
+    /**
+     * 获取插件在全局的文件
+     *
+     * @param string  $name         插件名称
+     * @param boolean $onlyconflict 是否只返回冲突文件
+     * @return  array
+     */
+    public static function getGlobalFiles($name, $onlyconflict = false)
+    {
+        $list = [];
+        $addonDir = self::getAddonDir($name);
+        $checkDirList = self::getCheckDirs();
+        $checkDirList = array_merge($checkDirList, ['assets']);
+
+        $assetDir = self::getDestAssetsDir($name);
+
+        // 扫描插件目录是否有覆盖的文件
+        foreach ($checkDirList as $k => $dirName) {
+            //检测目录是否存在
+            if (!is_dir($addonDir . $dirName)) {
+                continue;
+            }
+            //匹配出所有的文件
+            $files = new RecursiveIteratorIterator(
+                new RecursiveDirectoryIterator($addonDir . $dirName, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST
+            );
+
+            foreach ($files as $fileinfo) {
+                if ($fileinfo->isFile()) {
+                    $filePath = $fileinfo->getPathName();
+                    //如果名称为assets需要做特殊处理
+                    if ($dirName === 'assets') {
+                        $path = str_replace(ROOT_PATH, '', $assetDir) . str_replace($addonDir . $dirName . DS, '', $filePath);
+                    } else {
+                        $path = str_replace($addonDir, '', $filePath);
+                    }
+                    if ($onlyconflict) {
+                        $destPath = ROOT_PATH . $path;
+                        if (is_file($destPath)) {
+                            if (filesize($filePath) != filesize($destPath) || md5_file($filePath) != md5_file($destPath)) {
+                                $list[] = $path;
+                            }
+                        }
+                    } else {
+                        $list[] = $path;
+                    }
+                }
+            }
+        }
+        $list = array_filter(array_unique($list));
+        return $list;
+    }
+
+    /**
+     * 更新本地应用插件授权
+     */
+    public static function authorization($params = [])
+    {
+        $addonList = get_addon_list();
+        $result = [];
+        $domain = request()->host(true);
+        $addons = [];
+        foreach ($addonList as $name => $item) {
+            $config = self::config($name);
+            $addons[] = ['name' => $name, 'domains' => $config['domains'] ?? [], 'licensecodes' => $config['licensecodes'] ?? [], 'validations' => $config['validations'] ?? []];
+        }
+        $params = array_merge($params, [
+            'faversion' => config('fastadmin.version'),
+            'domain'    => $domain,
+            'addons'    => $addons
+        ]);
+        $result = self::sendRequest('/addon/authorization', $params, 'POST');
+        if (isset($result['code']) && $result['code'] == 1) {
+            $json = $result['data']['addons'] ?? [];
+            foreach ($addonList as $name => $item) {
+                self::config($name, ['domains' => $json[$name]['domains'] ?? [], 'licensecodes' => $json[$name]['licensecodes'] ?? [], 'validations' => $json[$name]['validations'] ?? []]);
+            }
+            return true;
+        } else {
+            throw new Exception($result['msg'] ?? __('Network error'));
+        }
+    }
+
+    /**
+     * 验证插件授权,应用插件需要授权使用,移除或绕过授权验证,保留追究法律责任的权利
+     * @param $name
+     * @return bool
+     */
+    public static function checkAddonAuthorization($name)
+    {
+        $request = request();
+        $config = self::config($name);
+        $domain = self::getRootDomain($request->host(true));
+        //应用插件需要授权使用,移除或绕过授权验证,保留追究法律责任的权利
+        if (isset($config['domains']) && isset($config['domains']) && isset($config['validations']) && isset($config['licensecodes'])) {
+            $index = array_search($domain, $config['domains']);
+            if ((in_array($domain, $config['domains']) && in_array(md5(md5($domain) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) || $request->isCli()) {
+                $request->bind('authorized', $domain ?: 'cli');
+                return true;
+            } elseif ($config['domains']) {
+                foreach ($config['domains'] as $index => $item) {
+                    if (substr_compare($domain, "." . $item, -strlen("." . $item)) === 0 && in_array(md5(md5($item) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) {
+                        $request->bind('authorized', $domain);
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 获取顶级域名
+     * @param $domain
+     * @return string
+     */
+    public static function getRootDomain($domain)
+    {
+        $host = strtolower(trim($domain));
+        $hostArr = explode('.', $host);
+        $hostCount = count($hostArr);
+        $cnRegex = '/\w+\.(gov|org|ac|mil|net|edu|com|bj|tj|sh|cq|he|sx|nm|ln|jl|hl|js|zj|ah|fj|jx|sd|ha|hb|hn|gd|gx|hi|sc|gz|yn|xz|sn|gs|qh|nx|xj|tw|hk|mo)\.cn$/i';
+        $countryRegex = '/\w+\.(\w{2}|com|net)\.\w{2}$/i';
+        if ($hostCount > 2 && (preg_match($cnRegex, $host) || preg_match($countryRegex, $host))) {
+            $host = implode('.', array_slice($hostArr, -3, 3, true));
+        } else {
+            $host = implode('.', array_slice($hostArr, -2, 2, true));
+        }
+        return $host;
+    }
+
+    /**
+     * 获取插件行为、路由配置文件
+     * @return string
+     */
+    public static function getExtraAddonsFile()
+    {
+        return CONF_PATH . 'extra' . DS . 'addons.php';
+    }
+
+    /**
+     * 获取bootstrap.js路径
+     * @return string
+     */
+    public static function getBootstrapFile($name)
+    {
+        return ADDON_PATH . $name . DS . 'bootstrap.js';
+    }
+
+    /**
+     * 获取testdata.sql路径
+     * @return string
+     */
+    public static function getTestdataFile($name)
+    {
+        return ADDON_PATH . $name . DS . 'testdata.sql';
+    }
+
+    /**
+     * 获取指定插件的目录
+     */
+    public static function getAddonDir($name)
+    {
+        $dir = ADDON_PATH . $name . DS;
+        return $dir;
+    }
+
+    /**
+     * 获取插件备份目录
+     */
+    public static function getAddonsBackupDir()
+    {
+        $dir = RUNTIME_PATH . 'addons' . DS;
+        if (!is_dir($dir)) {
+            @mkdir($dir, 0755, true);
+        }
+        return $dir;
+    }
+
+    /**
+     * 获取插件源资源文件夹
+     * @param string $name 插件名称
+     * @return  string
+     */
+    protected static function getSourceAssetsDir($name)
+    {
+        return ADDON_PATH . $name . DS . 'assets' . DS;
+    }
+
+    /**
+     * 获取插件目标资源文件夹
+     * @param string $name 插件名称
+     * @return  string
+     */
+    protected static function getDestAssetsDir($name)
+    {
+        $assetsDir = ROOT_PATH . str_replace("/", DS, "public/assets/addons/{$name}/");
+        return $assetsDir;
+    }
+
+    /**
+     * 获取远程服务器
+     * @return  string
+     */
+    protected static function getServerUrl()
+    {
+        return config('fastadmin.api_url');
+    }
+
+    /**
+     * 获取检测的全局文件夹目录
+     * @return  array
+     */
+    protected static function getCheckDirs()
+    {
+        return [
+            'application',
+            'public'
+        ];
+    }
+
+    /**
+     * 获取请求对象
+     * @return Client
+     */
+    public static function getClient()
+    {
+        $options = [
+            'base_uri'        => self::getServerUrl(),
+            'timeout'         => 30,
+            'connect_timeout' => 30,
+            'verify'          => false,
+            'http_errors'     => false,
+            'headers'         => [
+                'X-REQUESTED-WITH' => 'XMLHttpRequest',
+                'Referer'          => dirname(request()->root(true)),
+                'User-Agent'       => 'FastAddon',
+            ]
+        ];
+        static $client;
+        if (empty($client)) {
+            $client = new Client($options);
+        }
+        return $client;
+    }
+
+    /**
+     * 发送请求
+     * @return array
+     * @throws Exception
+     * @throws \GuzzleHttp\Exception\GuzzleException
+     */
+    public static function sendRequest($url, $params = [], $method = 'POST')
+    {
+        $json = [];
+        try {
+            $client = self::getClient();
+            $options = strtoupper($method) == 'POST' ? ['form_params' => $params] : ['query' => $params];
+            $response = $client->request($method, $url, $options);
+            $body = $response->getBody();
+            $content = $body->getContents();
+            $json = (array)json_decode($content, true);
+        } catch (TransferException $e) {
+            throw new Exception(config('app_debug') ? $e->getMessage() : __('Network error'));
+        } catch (\Exception $e) {
+            throw new Exception(config('app_debug') ? $e->getMessage() : __('Unknown data format'));
+        }
+        return $json;
+    }
+
+    /**
+     * 匹配配置文件中info信息
+     * @param ZipFile $zip
+     * @return array|false
+     * @throws Exception
+     */
+    protected static function getInfoIni($zip)
+    {
+        $config = [];
+        // 读取插件信息
+        try {
+            $info = $zip->getEntryContents('info.ini');
+            $config = parse_ini_string($info);
+        } catch (ZipException $e) {
+            throw new Exception('Unable to extract the file');
+        }
+        return $config;
+    }
+
+}

+ 1 - 1
composer.json

@@ -21,7 +21,7 @@
         "topthink/think-installer": "^1.0.14",
         "topthink/think-queue": "1.1.6",
         "topthink/think-helper": "^1.0.7",
-        "karsonzhang/fastadmin-addons": "~1.3.0",
+        "karsonzhang/fastadmin-addons": "~1.3.1",
         "overtrue/pinyin": "^3.0",
         "phpoffice/phpspreadsheet": "1.12",
         "overtrue/wechat": "4.2.11",

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

@@ -614,7 +614,6 @@ form.form-horizontal .control-label {
 /*顶栏addtabs*/
 .nav-addtabs {
   height: 100%;
-  overflow-y: hidden;
   border: none;
 }
 .nav-addtabs.disable-top-badge > li > a > .pull-right-container {

File diff ditekan karena terlalu besar
+ 0 - 0
public/assets/css/backend.min.css


+ 26 - 0
public/assets/css/frontend.css

@@ -431,6 +431,32 @@ a:focus {
 .layui-layer-content {
   clear: both;
 }
+.layui-layer-fast .layui-layer-content > table.table {
+  margin-bottom: 0;
+}
+.layui-layer-fast .layui-layer-confirm {
+  display: none;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  bottom: 0;
+  border: 1px solid transparent;
+  background: transparent;
+  color: transparent;
+}
+.layui-layer-fast .layui-layer-confirm:focus {
+  border: 1px solid #444c69;
+  -webkit-border-radius: 2px;
+  -webkit-background-clip: padding-box;
+  -moz-border-radius: 2px;
+  -moz-background-clip: padding;
+  border-radius: 2px;
+  background-clip: padding-box;
+}
+.layui-layer-fast .layui-layer-confirm:focus-visible {
+  outline: 0;
+}
 .layui-layer-fast-msg {
   min-width: 100px;
   border-radius: 2px;

File diff ditekan karena terlalu besar
+ 0 - 0
public/assets/css/frontend.min.css


TEMPAT SAMPAH
public/assets/img/login.jpg


TEMPAT SAMPAH
public/assets/img/login2.jpg


TEMPAT SAMPAH
public/assets/img/login3.jpg


+ 8 - 2
public/assets/js/backend/addon.js

@@ -4,7 +4,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
             // 初始化表格参数配置
             Table.api.init({
                 extend: {
-                    index_url: Config.api_url ? 'addon/index' : "addon/downloaded",
+                    index_url: Config.api_url ? Config.api_url + '/addon/index' : "addon/downloaded",
                     add_url: '',
                     edit_url: '',
                     del_url: '',
@@ -45,6 +45,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 }
             });
             table.on('load-error.bs.table', function (e, status, res) {
+                console.log(e, status, res);
                 switch_local();
             });
             table.on('post-body.bs.table', function (e, settings, json, xhr) {
@@ -94,6 +95,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     $.extend(params, {
                         uid: userinfo ? userinfo.id : '',
                         token: userinfo ? userinfo.token : '',
+                        domain: Config.domain,
                         version: Config.faversion
                     });
                     return params;
@@ -168,6 +170,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     });
                     return res;
                 },
+                dataType: 'jsonp',
                 templateView: false,
                 clickToSelect: false,
                 search: true,
@@ -350,7 +353,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                             area: area,
                             title: __('Userinfo'),
                             resize: false,
-                            btn: [__('Logout'), __('Cancel')],
+                            btn: [__('Logout'), __('Close')],
                             yes: function () {
                                 Fast.api.ajax({
                                     url: Config.api_url + '/user/logout',
@@ -680,6 +683,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
         api: {
             formatter: {
                 title: function (value, row, index) {
+                    if ($(".btn-switch.active").data("type") == "local") {
+                        // return value;
+                    }
                     var title = '<a class="title" href="' + row.url + '" data-toggle="tooltip" title="' + __('View addon home page') + '" target="_blank">' + value + '</a>';
                     if (row.screenshots && row.screenshots.length > 0) {
                         title += ' <a href="javascript:;" data-index="' + index + '" class="view-screenshots text-success" title="' + __('View addon screenshots') + '" data-toggle="tooltip"><i class="fa fa-image"></i></a>';

File diff ditekan karena terlalu besar
+ 0 - 0
public/assets/js/require-backend.min.js


+ 4 - 0
public/assets/js/require-form.js

@@ -9,6 +9,10 @@ define(['jquery', 'bootstrap', 'upload', 'validator', 'validator-lang'], functio
                     return;
                 //绑定表单事件
                 form.validator($.extend({
+                    rules: {
+                        username: [/^\w{3,30}$/, __('Username must be 3 to 30 characters')],
+                        password: [/^[\S]{6,30}$/, __('Password must be 6 to 30 characters')]
+                    },
                     validClass: 'has-success',
                     invalidClass: 'has-error',
                     bindClassTo: '.form-group',

File diff ditekan karena terlalu besar
+ 0 - 0
public/assets/js/require-frontend.min.js


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

@@ -616,7 +616,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                             url = Fast.api.cdnurl(value);
                             data.push({
                                 src: url,
-                                thumb: url + Config.upload.thumbstyle
+                                thumb: url.match(/^(\/|data:image\\)/) ? url : url + Config.upload.thumbstyle
                             });
                         });
                         Layer.photos({
@@ -641,16 +641,45 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     value = value == null || value.length === 0 ? '' : value.toString();
                     value = value ? value : '/assets/img/blank.gif';
                     var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
-                    return '<a href="javascript:"><img class="' + classname + '" src="' + Fast.api.cdnurl(value, true) + Config.upload.thumbstyle + '" /></a>';
+                    var url = Fast.api.cdnurl(value, true);
+                    url = url.match(/^(\/|data:image\\)/) ? url : url + Config.upload.thumbstyle;
+                    return '<a href="javascript:"><img class="' + classname + '" src="' + url + '" /></a>';
                 },
                 images: function (value, row, index) {
                     value = value == null || value.length === 0 ? '' : value.toString();
                     var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
                     var arr = value != '' ? value.split(',') : [];
                     var html = [];
+                    var url;
                     $.each(arr, function (i, value) {
                         value = value ? value : '/assets/img/blank.gif';
-                        html.push('<a href="javascript:"><img class="' + classname + '" src="' + Fast.api.cdnurl(value, true) + Config.upload.thumbstyle + '" /></a>');
+                        url = Fast.api.cdnurl(value, true);
+                        url = url.match(/^(\/|data:image\\)/) ? url : url + Config.upload.thumbstyle;
+                        html.push('<a href="javascript:"><img class="' + classname + '" src="' + url + '" /></a>');
+                    });
+                    return html.join(' ');
+                },
+                file: function (value, row, index) {
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    value = Fast.api.cdnurl(value, true);
+                    var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
+                    var suffix = /[\.]?([a-zA-Z0-9]+)$/.exec(value);
+                    suffix = suffix ? suffix[1] : 'file';
+                    var url = Fast.api.fixurl("ajax/icon?suffix=" + suffix);
+                    return '<a href="' + value + '" target="_blank"><img src="' + url + '" class="' + classname + '"></a>';
+                },
+                files: function (value, row, index) {
+                    value = value == null || value.length === 0 ? '' : value.toString();
+                    var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
+                    var arr = value != '' ? value.split(',') : [];
+                    var html = [];
+                    var suffix, url;
+                    $.each(arr, function (i, value) {
+                        value = Fast.api.cdnurl(value, true);
+                        suffix = /[\.]?([a-zA-Z0-9]+)$/.exec(value);
+                        suffix = suffix ? suffix[1] : 'file';
+                        url = Fast.api.fixurl("ajax/icon?suffix=" + suffix);
+                        html.push('<a href="' + value + '" target="_blank"><img src="' + url + '" class="' + classname + '"></a>');
                     });
                     return html.join(' ');
                 },

+ 1 - 1
public/assets/less/backend.less

@@ -454,7 +454,7 @@ form.form-horizontal .control-label {
 /*顶栏addtabs*/
 .nav-addtabs {
     height: 100%;
-    overflow-y: hidden;
+    //overflow-y: hidden;
 
     &.disable-top-badge {
         > li > a > .pull-right-container {

+ 29 - 0
public/assets/less/frontend.less

@@ -225,6 +225,35 @@ a {
     clear: both;
 }
 
+.layui-layer-fast {
+    .layui-layer-content {
+        > table.table {
+            margin-bottom: 0;
+        }
+    }
+
+    .layui-layer-confirm {
+        display: none;
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        left: 0;
+        bottom: 0;
+        border: 1px solid transparent;
+        background: transparent;
+        color: transparent;
+
+        &:focus {
+            border: 1px solid #444c69;
+            .border-radius(2px);
+        }
+
+        &:focus-visible {
+            outline: 0;
+        }
+    }
+}
+
 .layui-layer-fast-msg {
     min-width: 100px;
     border-radius: 2px;

+ 6 - 6
public/assets/libs/bootstrap/.bower.json

@@ -11,7 +11,7 @@
     "framework",
     "web"
   ],
-  "homepage": "http://getbootstrap.com",
+  "homepage": "https://getbootstrap.com/",
   "license": "MIT",
   "moduleType": "globals",
   "main": [
@@ -31,14 +31,14 @@
   "dependencies": {
     "jquery": "1.9.1 - 3"
   },
-  "version": "3.3.7",
-  "_release": "3.3.7",
+  "version": "3.4.1",
+  "_release": "3.4.1",
   "_resolution": {
     "type": "version",
-    "tag": "v3.3.7",
-    "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86"
+    "tag": "v3.4.1",
+    "commit": "68b0d231a13201eb14acd3dc84e51543d16e5f7e"
   },
   "_source": "https://github.com/twbs/bootstrap.git",
-  "_target": "~3.3.7",
+  "_target": "^3.3.7",
   "_originalSource": "bootstrap"
 }

+ 2 - 2
public/assets/libs/bootstrap/CHANGELOG.md

@@ -1,5 +1,5 @@
-Bootstrap uses [GitHub's Releases feature](https://github.com/blog/1547-release-your-software) for its changelogs.
+Bootstrap uses [GitHub's Releases feature](https://blog.github.com/2013-07-02-release-your-software/) for its changelogs.
 
 See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap.
 
-Release announcement posts on [the official Bootstrap blog](http://blog.getbootstrap.com) contain summaries of the most noteworthy changes made in each release.
+Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release.

+ 4 - 2
public/assets/libs/bootstrap/Gemfile

@@ -1,6 +1,8 @@
 source 'https://rubygems.org'
 
 group :development, :test do
-  gem 'jekyll', '~> 3.1.2'
-  gem 'jekyll-sitemap', '~> 0.11.0'
+  gem 'jekyll', '~> 3.8.5'
+  gem 'jekyll-redirect-from', '~> 0.14.0'
+  gem 'jekyll-sitemap', '~> 1.2.0'
+  gem 'wdm', '~> 0.1.1', :install_if => Gem.win_platform?
 end

+ 56 - 25
public/assets/libs/bootstrap/Gemfile.lock

@@ -1,43 +1,74 @@
 GEM
   remote: https://rubygems.org/
   specs:
-    addressable (2.4.0)
-    colorator (0.1)
-    ffi (1.9.14-x64-mingw32)
-    jekyll (3.1.6)
-      colorator (~> 0.1)
+    addressable (2.6.0)
+      public_suffix (>= 2.0.2, < 4.0)
+    colorator (1.1.0)
+    concurrent-ruby (1.1.4)
+    em-websocket (0.5.1)
+      eventmachine (>= 0.12.9)
+      http_parser.rb (~> 0.6.0)
+    eventmachine (1.2.7)
+    eventmachine (1.2.7-x64-mingw32)
+    ffi (1.10.0)
+    ffi (1.10.0-x64-mingw32)
+    forwardable-extended (2.6.0)
+    http_parser.rb (0.6.0)
+    i18n (0.9.5)
+      concurrent-ruby (~> 1.0)
+    jekyll (3.8.5)
+      addressable (~> 2.4)
+      colorator (~> 1.0)
+      em-websocket (~> 0.5)
+      i18n (~> 0.7)
       jekyll-sass-converter (~> 1.0)
-      jekyll-watch (~> 1.1)
-      kramdown (~> 1.3)
-      liquid (~> 3.0)
+      jekyll-watch (~> 2.0)
+      kramdown (~> 1.14)
+      liquid (~> 4.0)
       mercenary (~> 0.3.3)
-      rouge (~> 1.7)
+      pathutil (~> 0.9)
+      rouge (>= 1.7, < 4)
       safe_yaml (~> 1.0)
-    jekyll-sass-converter (1.4.0)
+    jekyll-redirect-from (0.14.0)
+      jekyll (~> 3.3)
+    jekyll-sass-converter (1.5.2)
       sass (~> 3.4)
-    jekyll-sitemap (0.11.0)
-      addressable (~> 2.4.0)
-    jekyll-watch (1.4.0)
-      listen (~> 3.0, < 3.1)
-    kramdown (1.11.1)
-    liquid (3.0.6)
-    listen (3.0.8)
+    jekyll-sitemap (1.2.0)
+      jekyll (~> 3.3)
+    jekyll-watch (2.1.2)
+      listen (~> 3.0)
+    kramdown (1.17.0)
+    liquid (4.0.1)
+    listen (3.1.5)
       rb-fsevent (~> 0.9, >= 0.9.4)
       rb-inotify (~> 0.9, >= 0.9.7)
+      ruby_dep (~> 1.2)
     mercenary (0.3.6)
-    rb-fsevent (0.9.7)
-    rb-inotify (0.9.7)
-      ffi (>= 0.5.0)
-    rouge (1.11.1)
+    pathutil (0.16.2)
+      forwardable-extended (~> 2.6)
+    public_suffix (3.0.3)
+    rb-fsevent (0.10.3)
+    rb-inotify (0.10.0)
+      ffi (~> 1.0)
+    rouge (3.3.0)
+    ruby_dep (1.5.0)
     safe_yaml (1.0.4)
-    sass (3.4.22)
+    sass (3.7.3)
+      sass-listen (~> 4.0.0)
+    sass-listen (4.0.0)
+      rb-fsevent (~> 0.9, >= 0.9.4)
+      rb-inotify (~> 0.9, >= 0.9.7)
+    wdm (0.1.1)
 
 PLATFORMS
+  ruby
   x64-mingw32
 
 DEPENDENCIES
-  jekyll (~> 3.1.2)
-  jekyll-sitemap (~> 0.11.0)
+  jekyll (~> 3.8.5)
+  jekyll-redirect-from (~> 0.14.0)
+  jekyll-sitemap (~> 1.2.0)
+  wdm (~> 0.1.1)
 
 BUNDLED WITH
-   1.12.5
+   1.17.3

+ 96 - 177
public/assets/libs/bootstrap/Gruntfile.js

@@ -1,7 +1,7 @@
 /*!
  * Bootstrap's Gruntfile
- * http://getbootstrap.com
- * Copyright 2013-2016 Twitter, Inc.
+ * https://getbootstrap.com/
+ * Copyright 2013-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  */
 
@@ -104,7 +104,7 @@ module.exports = function (grunt) {
         banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
         stripBanners: false
       },
-      bootstrap: {
+      core: {
         src: [
           'js/transition.js',
           'js/alert.js',
@@ -125,78 +125,90 @@ module.exports = function (grunt) {
 
     uglify: {
       options: {
-        compress: {
-          warnings: false
-        },
+        compress: true,
         mangle: true,
-        preserveComments: /^!|@preserve|@license|@cc_on/i
+        ie8: true,
+        output: {
+          comments: /^!|@preserve|@license|@cc_on/i
+        }
       },
       core: {
-        src: '<%= concat.bootstrap.dest %>',
+        src: '<%= concat.core.dest %>',
         dest: 'dist/js/<%= pkg.name %>.min.js'
       },
       customize: {
         src: configBridge.paths.customizerJs,
         dest: 'docs/assets/js/customize.min.js'
       },
-      docsJs: {
+      docs: {
         src: configBridge.paths.docsJs,
         dest: 'docs/assets/js/docs.min.js'
       }
     },
 
-    qunit: {
+    less: {
       options: {
-        inject: 'js/tests/unit/phantom.js'
+        ieCompat: true,
+        strictMath: true,
+        sourceMap: true,
+        outputSourceFiles: true
       },
-      files: 'js/tests/index.html'
-    },
-
-    less: {
-      compileCore: {
+      core: {
         options: {
-          strictMath: true,
-          sourceMap: true,
-          outputSourceFiles: true,
           sourceMapURL: '<%= pkg.name %>.css.map',
           sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
         },
         src: 'less/bootstrap.less',
         dest: 'dist/css/<%= pkg.name %>.css'
       },
-      compileTheme: {
+      theme: {
         options: {
-          strictMath: true,
-          sourceMap: true,
-          outputSourceFiles: true,
           sourceMapURL: '<%= pkg.name %>-theme.css.map',
           sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
         },
         src: 'less/theme.less',
         dest: 'dist/css/<%= pkg.name %>-theme.css'
+      },
+      docs: {
+        options: {
+          sourceMapURL: 'docs.css.map',
+          sourceMapFilename: 'docs/assets/css/docs.css.map'
+        },
+        src: 'docs/assets/less/docs.less',
+        dest: 'docs/assets/css/docs.css'
+      },
+      docsIe: {
+        options: {
+          sourceMap: false
+        },
+        src: 'docs/assets/less/ie10-viewport-bug-workaround.less',
+        dest: 'docs/assets/css/ie10-viewport-bug-workaround.css'
       }
     },
 
-    autoprefixer: {
+    postcss: {
       options: {
-        browsers: configBridge.config.autoprefixerBrowsers
+        map: {
+          inline: false,
+          sourcesContent: true
+        },
+        processors: [
+          require('autoprefixer')(configBridge.config.autoprefixer)
+        ]
       },
       core: {
-        options: {
-          map: true
-        },
         src: 'dist/css/<%= pkg.name %>.css'
       },
       theme: {
-        options: {
-          map: true
-        },
         src: 'dist/css/<%= pkg.name %>-theme.css'
       },
       docs: {
-        src: ['docs/assets/css/src/docs.css']
+        src: 'docs/assets/css/docs.css'
       },
       examples: {
+        options: {
+          map: false
+        },
         expand: true,
         cwd: 'docs/examples/',
         src: ['**/*.css'],
@@ -204,76 +216,47 @@ module.exports = function (grunt) {
       }
     },
 
-    csslint: {
+    stylelint: {
       options: {
-        csslintrc: 'less/.csslintrc'
+        configFile: 'grunt/.stylelintrc',
+        reportNeedlessDisables: false
       },
       dist: [
-        'dist/css/bootstrap.css',
-        'dist/css/bootstrap-theme.css'
+        'less/**/*.less'
+      ],
+      docs: [
+        'docs/assets/less/**/*.less'
       ],
       examples: [
         'docs/examples/**/*.css'
-      ],
-      docs: {
-        options: {
-          ids: false,
-          'overqualified-elements': false
-        },
-        src: 'docs/assets/css/src/docs.css'
-      }
+      ]
     },
 
     cssmin: {
       options: {
-        // TODO: disable `zeroUnits` optimization once clean-css 3.2 is released
-        //    and then simplify the fix for https://github.com/twbs/bootstrap/issues/14837 accordingly
         compatibility: 'ie8',
-        keepSpecialComments: '*',
         sourceMap: true,
         sourceMapInlineSources: true,
-        advanced: false
+        level: {
+          1: {
+            specialComments: 'all'
+          }
+        }
       },
-      minifyCore: {
+      core: {
         src: 'dist/css/<%= pkg.name %>.css',
         dest: 'dist/css/<%= pkg.name %>.min.css'
       },
-      minifyTheme: {
+      theme: {
         src: 'dist/css/<%= pkg.name %>-theme.css',
         dest: 'dist/css/<%= pkg.name %>-theme.min.css'
       },
       docs: {
-        src: [
-          'docs/assets/css/ie10-viewport-bug-workaround.css',
-          'docs/assets/css/src/pygments-manni.css',
-          'docs/assets/css/src/docs.css'
-        ],
+        src: 'docs/assets/css/docs.css',
         dest: 'docs/assets/css/docs.min.css'
       }
     },
 
-    csscomb: {
-      options: {
-        config: 'less/.csscomb.json'
-      },
-      dist: {
-        expand: true,
-        cwd: 'dist/css/',
-        src: ['*.css', '!*.min.css'],
-        dest: 'dist/css/'
-      },
-      examples: {
-        expand: true,
-        cwd: 'docs/examples/',
-        src: '**/*.css',
-        dest: 'docs/examples/'
-      },
-      docs: {
-        src: 'docs/assets/css/src/docs.css',
-        dest: 'docs/assets/css/src/docs.css'
-      }
-    },
-
     copy: {
       fonts: {
         expand: true,
@@ -313,41 +296,6 @@ module.exports = function (grunt) {
       }
     },
 
-    htmlmin: {
-      dist: {
-        options: {
-          collapseBooleanAttributes: true,
-          collapseWhitespace: true,
-          conservativeCollapse: true,
-          decodeEntities: false,
-          minifyCSS: {
-            compatibility: 'ie8',
-            keepSpecialComments: 0
-          },
-          minifyJS: true,
-          minifyURLs: false,
-          processConditionalComments: true,
-          removeAttributeQuotes: true,
-          removeComments: true,
-          removeOptionalAttributes: true,
-          removeOptionalTags: true,
-          removeRedundantAttributes: true,
-          removeScriptTypeAttributes: true,
-          removeStyleLinkTypeAttributes: true,
-          removeTagWhitespace: false,
-          sortAttributes: true,
-          sortClassName: true
-        },
-        expand: true,
-        cwd: '_gh_pages',
-        dest: '_gh_pages',
-        src: [
-          '**/*.html',
-          '!examples/**/*.html'
-        ]
-      }
-    },
-
     pug: {
       options: {
         pretty: true,
@@ -366,67 +314,40 @@ module.exports = function (grunt) {
     htmllint: {
       options: {
         ignore: [
-          'Attribute "autocomplete" not allowed on element "button" at this point.',
-          'Attribute "autocomplete" is only allowed when the input type is "color", "date", "datetime", "datetime-local", "email", "hidden", "month", "number", "password", "range", "search", "tel", "text", "time", "url", or "week".',
           'Element "img" is missing required attribute "src".'
-        ]
+        ],
+        noLangDetect: true
       },
-      src: '_gh_pages/**/*.html'
+      src: ['_gh_pages/**/*.html', 'js/tests/**/*.html']
     },
 
     watch: {
       src: {
         files: '<%= jshint.core.src %>',
-        tasks: ['jshint:core', 'qunit', 'concat']
+        tasks: ['jshint:core', 'exec:karma', 'concat']
       },
       test: {
         files: '<%= jshint.test.src %>',
-        tasks: ['jshint:test', 'qunit']
+        tasks: ['jshint:test', 'exec:karma']
       },
       less: {
         files: 'less/**/*.less',
-        tasks: 'less'
-      }
-    },
-
-    'saucelabs-qunit': {
-      all: {
-        options: {
-          build: process.env.TRAVIS_JOB_ID,
-          throttled: 10,
-          maxRetries: 3,
-          maxPollRetries: 4,
-          urls: ['http://127.0.0.1:3000/js/tests/index.html?hidepassed'],
-          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
-        }
+        tasks: ['less', 'copy']
+      },
+      docs: {
+        files: 'docs/assets/less/**/*.less',
+        tasks: ['less']
       }
     },
 
     exec: {
-      npmUpdate: {
-        command: 'npm update'
-      }
-    },
-
-    compress: {
-      main: {
-        options: {
-          archive: 'bootstrap-<%= pkg.version %>-dist.zip',
-          mode: 'zip',
-          level: 9,
-          pretty: true
-        },
-        files: [
-          {
-            expand: true,
-            cwd: 'dist/',
-            src: ['**'],
-            dest: 'bootstrap-<%= pkg.version %>-dist'
-          }
-        ]
+      browserstack: {
+        command: 'cross-env BROWSER=true karma start grunt/karma.conf.js'
+      },
+      karma: {
+        command: 'karma start grunt/karma.conf.js'
       }
     }
-
   });
 
 
@@ -441,16 +362,14 @@ module.exports = function (grunt) {
     return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
   };
   var isUndefOrNonZero = function (val) {
-    return val === undefined || val !== '0';
+    return typeof val === 'undefined' || val !== '0';
   };
 
   // Test task.
   var testSubtasks = [];
   // Skip core tests if running a different subset of the test suite
-  if (runSubset('core') &&
-      // Skip core tests if this is a Savage build
-      process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
-    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'csslint:dist', 'test-js', 'docs']);
+  if (runSubset('core')) {
+    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'stylelint:dist', 'test-js', 'docs']);
   }
   // Skip HTML validation if running a different subset of the test suite
   if (runSubset('validate-html') &&
@@ -458,24 +377,23 @@ module.exports = function (grunt) {
       isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
     testSubtasks.push('validate-html');
   }
-  // Only run Sauce Labs tests if there's a Sauce access key
-  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
-      // Skip Sauce if running a different subset of the test suite
-      runSubset('sauce-js-unit') &&
-      // Skip Sauce on Travis when [skip sauce] is in the commit message
-      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
-    testSubtasks.push('connect');
-    testSubtasks.push('saucelabs-qunit');
+  // Only run BrowserStack tests if there's a BrowserStack access key
+  if (typeof process.env.BROWSER_STACK_USERNAME !== 'undefined' &&
+      // Skip BrowserStack if running a different subset of the test suite
+      runSubset('browserstack') &&
+      // Skip BrowserStack on Travis when [skip browserstack] is in the commit message
+      isUndefOrNonZero(process.env.TWBS_DO_BROWSERSTACK)) {
+    testSubtasks.push('exec:browserstack');
   }
+
   grunt.registerTask('test', testSubtasks);
-  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
+  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'exec:karma']);
 
   // JS distribution task.
   grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
 
   // CSS distribution task.
-  grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
-  grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']);
+  grunt.registerTask('dist-css', ['less:core', 'less:theme', 'postcss:core', 'postcss:theme', 'cssmin:core', 'cssmin:theme']);
 
   // Full distribution task.
   grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
@@ -483,7 +401,9 @@ module.exports = function (grunt) {
   // Default task.
   grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
 
-  grunt.registerTask('build-glyphicons-data', function () { generateGlyphiconsData.call(this, grunt); });
+  grunt.registerTask('build-glyphicons-data', function () {
+    generateGlyphiconsData.call(this, grunt);
+  });
 
   // task for building customizer
   grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
@@ -494,18 +414,17 @@ module.exports = function (grunt) {
   });
 
   grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
-    var srcFiles = grunt.config.get('concat.bootstrap.src');
+    var srcFiles = grunt.config.get('concat.core.src');
     var destFilepath = 'dist/js/npm.js';
     generateCommonJSModule(grunt, srcFiles, destFilepath);
   });
 
   // Docs task.
-  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
-  grunt.registerTask('lint-docs-css', ['csslint:docs', 'csslint:examples']);
-  grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']);
+  grunt.registerTask('docs-css', ['less:docs', 'less:docsIe', 'postcss:docs', 'postcss:examples', 'cssmin:docs']);
+  grunt.registerTask('lint-docs-css', ['stylelint:docs', 'stylelint:examples']);
+  grunt.registerTask('docs-js', ['uglify:docs', 'uglify:customize']);
   grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
   grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-glyphicons-data', 'build-customizer']);
-  grunt.registerTask('docs-github', ['jekyll:github', 'htmlmin']);
 
-  grunt.registerTask('prep-release', ['dist', 'docs', 'docs-github', 'compress']);
+  grunt.registerTask('prep-release', ['dist', 'docs', 'jekyll:github']);
 };

+ 2 - 2
public/assets/libs/bootstrap/ISSUE_TEMPLATE.md

@@ -1,7 +1,7 @@
 Before opening an issue:
 
 - [Search for duplicate or closed issues](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue)
-- [Validate](http://validator.w3.org/nu/) and [lint](https://github.com/twbs/bootlint#in-the-browser) any HTML to avoid common problems
+- [Validate](https://validator.w3.org/nu/) and [lint](https://github.com/twbs/bootlint#in-the-browser) any HTML to avoid common problems
 - Prepare a [reduced test case](https://css-tricks.com/reduced-test-cases/) for any bugs
 - Read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/master/CONTRIBUTING.md)
 
@@ -14,7 +14,7 @@ When reporting a bug, include:
 
 - Operating system and version (Windows, Mac OS X, Android, iOS, Win10 Mobile)
 - Browser and version (Chrome, Firefox, Safari, IE, MS Edge, Opera 15+, Android Browser)
-- Reduced test cases and potential fixes using [JS Bin](https://jsbin.com)
+- Reduced test cases and potential fixes using [JS Bin](https://jsbin.com/)
 
 When suggesting a feature, include:
 

+ 1 - 1
public/assets/libs/bootstrap/LICENSE

@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2011-2016 Twitter, Inc.
+Copyright (c) 2011-2019 Twitter, Inc.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

+ 30 - 23
public/assets/libs/bootstrap/README.md

@@ -1,16 +1,15 @@
-# [Bootstrap](http://getbootstrap.com)
+# [Bootstrap](https://getbootstrap.com/)
 
-[![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com)
+[![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com/)
 ![Bower version](https://img.shields.io/bower/v/bootstrap.svg)
 [![npm version](https://img.shields.io/npm/v/bootstrap.svg)](https://www.npmjs.com/package/bootstrap)
 [![Build Status](https://img.shields.io/travis/twbs/bootstrap/master.svg)](https://travis-ci.org/twbs/bootstrap)
 [![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap.svg)](https://david-dm.org/twbs/bootstrap#info=devDependencies)
 [![NuGet](https://img.shields.io/nuget/v/bootstrap.svg)](https://www.nuget.org/packages/Bootstrap)
-[![Selenium Test Status](https://saucelabs.com/browser-matrix/bootstrap.svg)](https://saucelabs.com/u/bootstrap)
 
 Bootstrap is a sleek, intuitive, and powerful front-end framework for faster and easier web development, created by [Mark Otto](https://twitter.com/mdo) and [Jacob Thornton](https://twitter.com/fat), and maintained by the [core team](https://github.com/orgs/twbs/people) with the massive support and involvement of the community.
 
-To get started, check out <http://getbootstrap.com>!
+To get started, check out <https://getbootstrap.com/>!
 
 
 ## Table of contents
@@ -22,6 +21,7 @@ To get started, check out <http://getbootstrap.com>!
 * [Community](#community)
 * [Versioning](#versioning)
 * [Creators](#creators)
+* [Thanks](#thanks)
 * [Copyright and license](#copyright-and-license)
 
 
@@ -29,14 +29,14 @@ To get started, check out <http://getbootstrap.com>!
 
 Several quick start options are available:
 
-* [Download the latest release](https://github.com/twbs/bootstrap/archive/v3.3.7.zip).
+* [Download the latest release](https://github.com/twbs/bootstrap/archive/v3.4.1.zip).
 * Clone the repo: `git clone https://github.com/twbs/bootstrap.git`.
-* Install with [Bower](http://bower.io): `bower install bootstrap`.
-* Install with [npm](https://www.npmjs.com): `npm install bootstrap@3`.
-* Install with [Meteor](https://www.meteor.com): `meteor add twbs:bootstrap`.
-* Install with [Composer](https://getcomposer.org): `composer require twbs/bootstrap`.
+* Install with [Bower](https://bower.io/): `bower install bootstrap`.
+* Install with [npm](https://www.npmjs.com/): `npm install bootstrap@3`.
+* Install with [Meteor](https://www.meteor.com/): `meteor add twbs:bootstrap`.
+* Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap`.
 
-Read the [Getting started page](http://getbootstrap.com/getting-started/) for information on the framework contents, templates and examples, and more.
+Read the [Getting started page](https://getbootstrap.com/docs/3.4/getting-started/) for information on the framework contents, templates and examples, and more.
 
 ### What's included
 
@@ -64,7 +64,7 @@ bootstrap/
     └── glyphicons-halflings-regular.woff2
 ```
 
-We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). CSS [source maps](https://developer.chrome.com/devtools/docs/css-preprocessors) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Fonts from Glyphicons are included, as is the optional Bootstrap theme.
+We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). CSS [source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Fonts from Glyphicons are included, as is the optional Bootstrap theme.
 
 
 ## Bugs and feature requests
@@ -76,20 +76,20 @@ Note that **feature requests must target [Bootstrap v4](https://github.com/twbs/
 
 ## Documentation
 
-Bootstrap's documentation, included in this repo in the root directory, is built with [Jekyll](http://jekyllrb.com) and publicly hosted on GitHub Pages at <http://getbootstrap.com>. The docs may also be run locally.
+Bootstrap's documentation, included in this repo in the root directory, is built with [Jekyll](https://jekyllrb.com/) and publicly hosted on GitHub Pages at <https://getbootstrap.com/>. The docs may also be run locally.
 
 ### Running documentation locally
 
-1. If necessary, [install Jekyll](http://jekyllrb.com/docs/installation) and other Ruby dependencies with `bundle install`.
-   **Note for Windows users:** Read [this unofficial guide](http://jekyll-windows.juthilo.com/) to get Jekyll up and running without problems.
+1. If necessary, [install Jekyll](https://jekyllrb.com/docs/installation/) and other Ruby dependencies with `bundle install`.
+   **Note for Windows users:** Read [this guide](https://jekyllrb.com/docs/installation/windows/) to get Jekyll up and running without problems.
 2. From the root `/bootstrap` directory, run `bundle exec jekyll serve` in the command line.
 4. Open `http://localhost:9001` in your browser, and voilà.
 
-Learn more about using Jekyll by reading its [documentation](http://jekyllrb.com/docs/home/).
+Learn more about using Jekyll by reading its [documentation](https://jekyllrb.com/docs/).
 
 ### Documentation for previous releases
 
-Documentation for v2.3.2 has been made available for the time being at <http://getbootstrap.com/2.3.2/> while folks transition to Bootstrap 3.
+Documentation for v2.3.2 has been made available for the time being at <https://getbootstrap.com/2.3.2/> while folks transition to Bootstrap 3.
 
 [Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download.
 
@@ -102,7 +102,7 @@ Moreover, if your pull request contains JavaScript patches or features, you must
 
 **Bootstrap v3 is now closed off to new features.** It has gone into maintenance mode so that we can focus our efforts on [Bootstrap v4](https://github.com/twbs/bootstrap/tree/v4-dev), the future of the framework. Pull requests which add new features (rather than fix bugs) should target [Bootstrap v4 (the `v4-dev` git branch)](https://github.com/twbs/bootstrap/tree/v4-dev) instead.
 
-Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/master/.editorconfig) for easy use in common text editors. Read more and download plugins at <http://editorconfig.org>.
+Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/master/.editorconfig) for easy use in common text editors. Read more and download plugins at <https://editorconfig.org/>.
 
 
 ## Community
@@ -110,18 +110,25 @@ Editor preferences are available in the [editor config](https://github.com/twbs/
 Get updates on Bootstrap's development and chat with the project maintainers and community members.
 
 * Follow [@getbootstrap on Twitter](https://twitter.com/getbootstrap).
-* Read and subscribe to [The Official Bootstrap Blog](http://blog.getbootstrap.com).
-* Join [the official Slack room](https://bootstrap-slack.herokuapp.com).
+* Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/).
+* Join [the official Slack room](https://bootstrap-slack.herokuapp.com/).
 * Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel.
 * Implementation help may be found at Stack Overflow (tagged [`twitter-bootstrap-3`](https://stackoverflow.com/questions/tagged/twitter-bootstrap-3)).
-* Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability.
+* Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/search?q=keywords:bootstrap) or similar delivery mechanisms for maximum discoverability.
 
 
 ## Versioning
 
-For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](http://semver.org/). Sometimes we screw up, but we'll adhere to those rules whenever possible.
+For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we'll adhere to those rules whenever possible.
 
-See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](http://blog.getbootstrap.com) contain summaries of the most noteworthy changes made in each release.
+See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release.
+
+
+## Thanks
+
+<img src="https://live.browserstack.com/images/opensource/browserstack-logo.svg" alt="BrowserStack Logo" width="490" height="106">
+
+Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers!
 
 
 ## Creators
@@ -139,4 +146,4 @@ See [the Releases section of our GitHub project](https://github.com/twbs/bootstr
 
 ## Copyright and license
 
-Code and documentation copyright 2011-2016 Twitter, Inc. Code released under [the MIT license](https://github.com/twbs/bootstrap/blob/master/LICENSE). Docs released under [Creative Commons](https://github.com/twbs/bootstrap/blob/master/docs/LICENSE).
+Code and documentation copyright 2011-2019 Twitter, Inc. Code released under [the MIT license](https://github.com/twbs/bootstrap/blob/master/LICENSE). Docs released under [Creative Commons](https://github.com/twbs/bootstrap/blob/master/docs/LICENSE).

+ 1 - 1
public/assets/libs/bootstrap/bower.json

@@ -11,7 +11,7 @@
     "framework",
     "web"
   ],
-  "homepage": "http://getbootstrap.com",
+  "homepage": "https://getbootstrap.com/",
   "license": "MIT",
   "moduleType": "globals",
   "main": [

+ 100 - 100
public/assets/libs/bootstrap/dist/css/bootstrap-theme.css

@@ -1,6 +1,6 @@
 /*!
- * Bootstrap v3.3.7 (http://getbootstrap.com)
- * Copyright 2011-2016 Twitter, Inc.
+ * Bootstrap v3.4.1 (https://getbootstrap.com/)
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  */
 .btn-default,
@@ -9,9 +9,9 @@
 .btn-info,
 .btn-warning,
 .btn-danger {
-  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
-  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
-          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
 }
 .btn-default:active,
 .btn-primary:active,
@@ -25,8 +25,8 @@
 .btn-info.active,
 .btn-warning.active,
 .btn-danger.active {
-  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
-          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
 }
 .btn-default.disabled,
 .btn-primary.disabled,
@@ -47,7 +47,7 @@ fieldset[disabled] .btn-info,
 fieldset[disabled] .btn-warning,
 fieldset[disabled] .btn-danger {
   -webkit-box-shadow: none;
-          box-shadow: none;
+  box-shadow: none;
 }
 .btn-default .badge,
 .btn-primary .badge,
@@ -62,15 +62,15 @@ fieldset[disabled] .btn-danger {
   background-image: none;
 }
 .btn-default {
-  text-shadow: 0 1px 0 #fff;
   background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
-  background-image:      -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
-  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
+  background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
   filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   background-repeat: repeat-x;
   border-color: #dbdbdb;
+  text-shadow: 0 1px 0 #fff;
   border-color: #ccc;
 }
 .btn-default:hover,
@@ -106,9 +106,9 @@ fieldset[disabled] .btn-default.active {
 }
 .btn-primary {
   background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
-  background-image:      -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
-  background-image:         linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
+  background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
   filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   background-repeat: repeat-x;
@@ -147,9 +147,9 @@ fieldset[disabled] .btn-primary.active {
 }
 .btn-success {
   background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
-  background-image:      -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
-  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
+  background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
   filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   background-repeat: repeat-x;
@@ -188,9 +188,9 @@ fieldset[disabled] .btn-success.active {
 }
 .btn-info {
   background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
-  background-image:      -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
-  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
+  background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
   filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   background-repeat: repeat-x;
@@ -229,9 +229,9 @@ fieldset[disabled] .btn-info.active {
 }
 .btn-warning {
   background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
-  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
-  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
+  background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
   filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   background-repeat: repeat-x;
@@ -270,9 +270,9 @@ fieldset[disabled] .btn-warning.active {
 }
 .btn-danger {
   background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
-  background-image:      -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
-  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
+  background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
   filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   background-repeat: repeat-x;
@@ -311,81 +311,81 @@ fieldset[disabled] .btn-danger.active {
 }
 .thumbnail,
 .img-thumbnail {
-  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
-          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
 }
 .dropdown-menu > li > a:hover,
 .dropdown-menu > li > a:focus {
-  background-color: #e8e8e8;
   background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
-  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
-  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
   background-repeat: repeat-x;
+  background-color: #e8e8e8;
 }
 .dropdown-menu > .active > a,
 .dropdown-menu > .active > a:hover,
 .dropdown-menu > .active > a:focus {
-  background-color: #2e6da4;
   background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
-  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
-  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+  background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
   background-repeat: repeat-x;
+  background-color: #2e6da4;
 }
 .navbar-default {
-  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
-  background-image:      -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
-  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
-  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
+  background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
+  background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#f8f8f8));
+  background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
-  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   border-radius: 4px;
-  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
-          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
 }
 .navbar-default .navbar-nav > .open > a,
 .navbar-default .navbar-nav > .active > a {
   background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
-  background-image:      -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
+  background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
-  background-image:         linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
+  background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
   background-repeat: repeat-x;
-  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
-          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
 }
 .navbar-brand,
 .navbar-nav > li > a {
-  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
 }
 .navbar-inverse {
   background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
-  background-image:      -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
-  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
+  background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
-  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
   border-radius: 4px;
 }
 .navbar-inverse .navbar-nav > .open > a,
 .navbar-inverse .navbar-nav > .active > a {
   background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
-  background-image:      -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
+  background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
-  background-image:         linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
+  background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
   background-repeat: repeat-x;
-  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
-          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
+  box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
 }
 .navbar-inverse .navbar-brand,
 .navbar-inverse .navbar-nav > li > a {
-  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
 }
 .navbar-static-top,
 .navbar-fixed-top,
@@ -398,120 +398,120 @@ fieldset[disabled] .btn-danger.active {
   .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
     color: #fff;
     background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
-    background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+    background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
     background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
-    background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+    background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
     filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
     background-repeat: repeat-x;
   }
 }
 .alert {
-  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
-  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
-          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
 }
 .alert-success {
   background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
-  background-image:      -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
-  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
+  background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
   background-repeat: repeat-x;
   border-color: #b2dba1;
 }
 .alert-info {
   background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
-  background-image:      -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
-  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
+  background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
   background-repeat: repeat-x;
   border-color: #9acfea;
 }
 .alert-warning {
   background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
-  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
-  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
+  background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
   background-repeat: repeat-x;
   border-color: #f5e79e;
 }
 .alert-danger {
   background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
-  background-image:      -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
-  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
+  background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
   background-repeat: repeat-x;
   border-color: #dca7a7;
 }
 .progress {
   background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
-  background-image:      -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
-  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
+  background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
   background-repeat: repeat-x;
 }
 .progress-bar {
   background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
-  background-image:      -o-linear-gradient(top, #337ab7 0%, #286090 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
-  background-image:         linear-gradient(to bottom, #337ab7 0%, #286090 100%);
+  background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
   background-repeat: repeat-x;
 }
 .progress-bar-success {
   background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
-  background-image:      -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
-  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
+  background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
   background-repeat: repeat-x;
 }
 .progress-bar-info {
   background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
-  background-image:      -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
-  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
+  background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
   background-repeat: repeat-x;
 }
 .progress-bar-warning {
   background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
-  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
-  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
+  background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
   background-repeat: repeat-x;
 }
 .progress-bar-danger {
   background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
-  background-image:      -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
-  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
+  background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
   background-repeat: repeat-x;
 }
 .progress-bar-striped {
-  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
-  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
-  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
 }
 .list-group {
   border-radius: 4px;
-  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
-          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
 }
 .list-group-item.active,
 .list-group-item.active:hover,
 .list-group-item.active:focus {
   text-shadow: 0 -1px 0 #286090;
   background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
-  background-image:      -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
-  background-image:         linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
+  background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
   background-repeat: repeat-x;
   border-color: #2b669a;
@@ -522,66 +522,66 @@ fieldset[disabled] .btn-danger.active {
   text-shadow: none;
 }
 .panel {
-  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
-          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
 }
 .panel-default > .panel-heading {
   background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
-  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
-  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
   background-repeat: repeat-x;
 }
 .panel-primary > .panel-heading {
   background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
-  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
-  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+  background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
   background-repeat: repeat-x;
 }
 .panel-success > .panel-heading {
   background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
-  background-image:      -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
-  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
+  background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
   background-repeat: repeat-x;
 }
 .panel-info > .panel-heading {
   background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
-  background-image:      -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
-  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
+  background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
   background-repeat: repeat-x;
 }
 .panel-warning > .panel-heading {
   background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
-  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
-  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
+  background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
   background-repeat: repeat-x;
 }
 .panel-danger > .panel-heading {
   background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
-  background-image:      -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
-  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
+  background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
   background-repeat: repeat-x;
 }
 .well {
   background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
-  background-image:      -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
   background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
-  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
+  background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
   filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
   background-repeat: repeat-x;
   border-color: #dcdcdc;
-  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
-          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
 }
-/*# sourceMappingURL=bootstrap-theme.css.map */
+/*# sourceMappingURL=bootstrap-theme.css.map */

File diff ditekan karena terlalu besar
+ 0 - 0
public/assets/libs/bootstrap/dist/css/bootstrap-theme.css.map


File diff ditekan karena terlalu besar
+ 2 - 2
public/assets/libs/bootstrap/dist/css/bootstrap-theme.min.css


File diff ditekan karena terlalu besar
+ 0 - 0
public/assets/libs/bootstrap/dist/css/bootstrap-theme.min.css.map


File diff ditekan karena terlalu besar
+ 255 - 173
public/assets/libs/bootstrap/dist/css/bootstrap.css


File diff ditekan karena terlalu besar
+ 0 - 0
public/assets/libs/bootstrap/dist/css/bootstrap.css.map


File diff ditekan karena terlalu besar
+ 2 - 2
public/assets/libs/bootstrap/dist/css/bootstrap.min.css


File diff ditekan karena terlalu besar
+ 0 - 0
public/assets/libs/bootstrap/dist/css/bootstrap.min.css.map


+ 300 - 97
public/assets/libs/bootstrap/dist/js/bootstrap.js

@@ -1,6 +1,6 @@
 /*!
- * Bootstrap v3.3.7 (http://getbootstrap.com)
- * Copyright 2011-2016 Twitter, Inc.
+ * Bootstrap v3.4.1 (https://getbootstrap.com/)
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under the MIT license
  */
 
@@ -17,10 +17,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: transition.js v3.3.7
- * http://getbootstrap.com/javascript/#transitions
+ * Bootstrap: transition.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#transitions
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -28,7 +28,7 @@ if (typeof jQuery === 'undefined') {
 +function ($) {
   'use strict';
 
-  // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
+  // CSS TRANSITION SUPPORT (Shoutout: https://modernizr.com/)
   // ============================================================
 
   function transitionEnd() {
@@ -50,7 +50,7 @@ if (typeof jQuery === 'undefined') {
     return false // explicit for ie8 (  ._.)
   }
 
-  // http://blog.alexmaccaw.com/css-transitions
+  // https://blog.alexmaccaw.com/css-transitions
   $.fn.emulateTransitionEnd = function (duration) {
     var called = false
     var $el = this
@@ -77,10 +77,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: alert.js v3.3.7
- * http://getbootstrap.com/javascript/#alerts
+ * Bootstrap: alert.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#alerts
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -96,7 +96,7 @@ if (typeof jQuery === 'undefined') {
     $(el).on('click', dismiss, this.close)
   }
 
-  Alert.VERSION = '3.3.7'
+  Alert.VERSION = '3.4.1'
 
   Alert.TRANSITION_DURATION = 150
 
@@ -109,7 +109,8 @@ if (typeof jQuery === 'undefined') {
       selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
     }
 
-    var $parent = $(selector === '#' ? [] : selector)
+    selector    = selector === '#' ? [] : selector
+    var $parent = $(document).find(selector)
 
     if (e) e.preventDefault()
 
@@ -172,10 +173,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: button.js v3.3.7
- * http://getbootstrap.com/javascript/#buttons
+ * Bootstrap: button.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#buttons
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -192,7 +193,7 @@ if (typeof jQuery === 'undefined') {
     this.isLoading = false
   }
 
-  Button.VERSION  = '3.3.7'
+  Button.VERSION  = '3.4.1'
 
   Button.DEFAULTS = {
     loadingText: 'loading...'
@@ -298,10 +299,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: carousel.js v3.3.7
- * http://getbootstrap.com/javascript/#carousel
+ * Bootstrap: carousel.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#carousel
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -329,7 +330,7 @@ if (typeof jQuery === 'undefined') {
       .on('mouseleave.bs.carousel', $.proxy(this.cycle, this))
   }
 
-  Carousel.VERSION  = '3.3.7'
+  Carousel.VERSION  = '3.4.1'
 
   Carousel.TRANSITION_DURATION = 600
 
@@ -443,7 +444,9 @@ if (typeof jQuery === 'undefined') {
     var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid"
     if ($.support.transition && this.$element.hasClass('slide')) {
       $next.addClass(type)
-      $next[0].offsetWidth // force reflow
+      if (typeof $next === 'object' && $next.length) {
+        $next[0].offsetWidth // force reflow
+      }
       $active.addClass(direction)
       $next.addClass(direction)
       $active
@@ -505,10 +508,17 @@ if (typeof jQuery === 'undefined') {
   // =================
 
   var clickHandler = function (e) {
-    var href
     var $this   = $(this)
-    var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
+    var href    = $this.attr('href')
+    if (href) {
+      href = href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
+    }
+
+    var target  = $this.attr('data-target') || href
+    var $target = $(document).find(target)
+
     if (!$target.hasClass('carousel')) return
+
     var options = $.extend({}, $target.data(), $this.data())
     var slideIndex = $this.attr('data-slide-to')
     if (slideIndex) options.interval = false
@@ -536,10 +546,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: collapse.js v3.3.7
- * http://getbootstrap.com/javascript/#collapse
+ * Bootstrap: collapse.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#collapse
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -567,7 +577,7 @@ if (typeof jQuery === 'undefined') {
     if (this.options.toggle) this.toggle()
   }
 
-  Collapse.VERSION  = '3.3.7'
+  Collapse.VERSION  = '3.4.1'
 
   Collapse.TRANSITION_DURATION = 350
 
@@ -674,7 +684,7 @@ if (typeof jQuery === 'undefined') {
   }
 
   Collapse.prototype.getParent = function () {
-    return $(this.options.parent)
+    return $(document).find(this.options.parent)
       .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
       .each($.proxy(function (i, element) {
         var $element = $(element)
@@ -697,7 +707,7 @@ if (typeof jQuery === 'undefined') {
     var target = $trigger.attr('data-target')
       || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
 
-    return $(target)
+    return $(document).find(target)
   }
 
 
@@ -749,10 +759,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: dropdown.js v3.3.7
- * http://getbootstrap.com/javascript/#dropdowns
+ * Bootstrap: dropdown.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#dropdowns
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -769,7 +779,7 @@ if (typeof jQuery === 'undefined') {
     $(element).on('click.bs.dropdown', this.toggle)
   }
 
-  Dropdown.VERSION = '3.3.7'
+  Dropdown.VERSION = '3.4.1'
 
   function getParent($this) {
     var selector = $this.attr('data-target')
@@ -779,7 +789,7 @@ if (typeof jQuery === 'undefined') {
       selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
     }
 
-    var $parent = selector && $(selector)
+    var $parent = selector !== '#' ? $(document).find(selector) : null
 
     return $parent && $parent.length ? $parent : $this.parent()
   }
@@ -915,10 +925,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: modal.js v3.3.7
- * http://getbootstrap.com/javascript/#modals
+ * Bootstrap: modal.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#modals
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -930,15 +940,16 @@ if (typeof jQuery === 'undefined') {
   // ======================
 
   var Modal = function (element, options) {
-    this.options             = options
-    this.$body               = $(document.body)
-    this.$element            = $(element)
-    this.$dialog             = this.$element.find('.modal-dialog')
-    this.$backdrop           = null
-    this.isShown             = null
-    this.originalBodyPad     = null
-    this.scrollbarWidth      = 0
+    this.options = options
+    this.$body = $(document.body)
+    this.$element = $(element)
+    this.$dialog = this.$element.find('.modal-dialog')
+    this.$backdrop = null
+    this.isShown = null
+    this.originalBodyPad = null
+    this.scrollbarWidth = 0
     this.ignoreBackdropClick = false
+    this.fixedContent = '.navbar-fixed-top, .navbar-fixed-bottom'
 
     if (this.options.remote) {
       this.$element
@@ -949,7 +960,7 @@ if (typeof jQuery === 'undefined') {
     }
   }
 
-  Modal.VERSION  = '3.3.7'
+  Modal.VERSION = '3.4.1'
 
   Modal.TRANSITION_DURATION = 300
   Modal.BACKDROP_TRANSITION_DURATION = 150
@@ -966,7 +977,7 @@ if (typeof jQuery === 'undefined') {
 
   Modal.prototype.show = function (_relatedTarget) {
     var that = this
-    var e    = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
+    var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
 
     this.$element.trigger(e)
 
@@ -1057,8 +1068,8 @@ if (typeof jQuery === 'undefined') {
       .off('focusin.bs.modal') // guard against infinite focus loop
       .on('focusin.bs.modal', $.proxy(function (e) {
         if (document !== e.target &&
-            this.$element[0] !== e.target &&
-            !this.$element.has(e.target).length) {
+          this.$element[0] !== e.target &&
+          !this.$element.has(e.target).length) {
           this.$element.trigger('focus')
         }
       }, this))
@@ -1160,7 +1171,7 @@ if (typeof jQuery === 'undefined') {
     var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
 
     this.$element.css({
-      paddingLeft:  !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
+      paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
       paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''
     })
   }
@@ -1185,11 +1196,26 @@ if (typeof jQuery === 'undefined') {
   Modal.prototype.setScrollbar = function () {
     var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)
     this.originalBodyPad = document.body.style.paddingRight || ''
-    if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)
+    var scrollbarWidth = this.scrollbarWidth
+    if (this.bodyIsOverflowing) {
+      this.$body.css('padding-right', bodyPad + scrollbarWidth)
+      $(this.fixedContent).each(function (index, element) {
+        var actualPadding = element.style.paddingRight
+        var calculatedPadding = $(element).css('padding-right')
+        $(element)
+          .data('padding-right', actualPadding)
+          .css('padding-right', parseFloat(calculatedPadding) + scrollbarWidth + 'px')
+      })
+    }
   }
 
   Modal.prototype.resetScrollbar = function () {
     this.$body.css('padding-right', this.originalBodyPad)
+    $(this.fixedContent).each(function (index, element) {
+      var padding = $(element).data('padding-right')
+      $(element).removeData('padding-right')
+      element.style.paddingRight = padding ? padding : ''
+    })
   }
 
   Modal.prototype.measureScrollbar = function () { // thx walsh
@@ -1207,8 +1233,8 @@ if (typeof jQuery === 'undefined') {
 
   function Plugin(option, _relatedTarget) {
     return this.each(function () {
-      var $this   = $(this)
-      var data    = $this.data('bs.modal')
+      var $this = $(this)
+      var data = $this.data('bs.modal')
       var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
 
       if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
@@ -1219,7 +1245,7 @@ if (typeof jQuery === 'undefined') {
 
   var old = $.fn.modal
 
-  $.fn.modal             = Plugin
+  $.fn.modal = Plugin
   $.fn.modal.Constructor = Modal
 
 
@@ -1236,10 +1262,13 @@ if (typeof jQuery === 'undefined') {
   // ==============
 
   $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
-    var $this   = $(this)
-    var href    = $this.attr('href')
-    var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7
-    var option  = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
+    var $this = $(this)
+    var href = $this.attr('href')
+    var target = $this.attr('data-target') ||
+      (href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
+
+    var $target = $(document).find(target)
+    var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
 
     if ($this.is('a')) e.preventDefault()
 
@@ -1255,18 +1284,148 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: tooltip.js v3.3.7
- * http://getbootstrap.com/javascript/#tooltip
+ * Bootstrap: tooltip.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#tooltip
  * Inspired by the original jQuery.tipsy by Jason Frame
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
-
 +function ($) {
   'use strict';
 
+  var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
+
+  var uriAttrs = [
+    'background',
+    'cite',
+    'href',
+    'itemtype',
+    'longdesc',
+    'poster',
+    'src',
+    'xlink:href'
+  ]
+
+  var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
+
+  var DefaultWhitelist = {
+    // Global attributes allowed on any supplied element below.
+    '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
+    a: ['target', 'href', 'title', 'rel'],
+    area: [],
+    b: [],
+    br: [],
+    col: [],
+    code: [],
+    div: [],
+    em: [],
+    hr: [],
+    h1: [],
+    h2: [],
+    h3: [],
+    h4: [],
+    h5: [],
+    h6: [],
+    i: [],
+    img: ['src', 'alt', 'title', 'width', 'height'],
+    li: [],
+    ol: [],
+    p: [],
+    pre: [],
+    s: [],
+    small: [],
+    span: [],
+    sub: [],
+    sup: [],
+    strong: [],
+    u: [],
+    ul: []
+  }
+
+  /**
+   * A pattern that recognizes a commonly useful subset of URLs that are safe.
+   *
+   * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+   */
+  var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi
+
+  /**
+   * A pattern that matches safe data URLs. Only matches image, video and audio types.
+   *
+   * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+   */
+  var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i
+
+  function allowedAttribute(attr, allowedAttributeList) {
+    var attrName = attr.nodeName.toLowerCase()
+
+    if ($.inArray(attrName, allowedAttributeList) !== -1) {
+      if ($.inArray(attrName, uriAttrs) !== -1) {
+        return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
+      }
+
+      return true
+    }
+
+    var regExp = $(allowedAttributeList).filter(function (index, value) {
+      return value instanceof RegExp
+    })
+
+    // Check if a regular expression validates the attribute.
+    for (var i = 0, l = regExp.length; i < l; i++) {
+      if (attrName.match(regExp[i])) {
+        return true
+      }
+    }
+
+    return false
+  }
+
+  function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
+    if (unsafeHtml.length === 0) {
+      return unsafeHtml
+    }
+
+    if (sanitizeFn && typeof sanitizeFn === 'function') {
+      return sanitizeFn(unsafeHtml)
+    }
+
+    // IE 8 and below don't support createHTMLDocument
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      return unsafeHtml
+    }
+
+    var createdDocument = document.implementation.createHTMLDocument('sanitization')
+    createdDocument.body.innerHTML = unsafeHtml
+
+    var whitelistKeys = $.map(whiteList, function (el, i) { return i })
+    var elements = $(createdDocument.body).find('*')
+
+    for (var i = 0, len = elements.length; i < len; i++) {
+      var el = elements[i]
+      var elName = el.nodeName.toLowerCase()
+
+      if ($.inArray(elName, whitelistKeys) === -1) {
+        el.parentNode.removeChild(el)
+
+        continue
+      }
+
+      var attributeList = $.map(el.attributes, function (el) { return el })
+      var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])
+
+      for (var j = 0, len2 = attributeList.length; j < len2; j++) {
+        if (!allowedAttribute(attributeList[j], whitelistedAttributes)) {
+          el.removeAttribute(attributeList[j].nodeName)
+        }
+      }
+    }
+
+    return createdDocument.body.innerHTML
+  }
+
   // TOOLTIP PUBLIC CLASS DEFINITION
   // ===============================
 
@@ -1282,7 +1441,7 @@ if (typeof jQuery === 'undefined') {
     this.init('tooltip', element, options)
   }
 
-  Tooltip.VERSION  = '3.3.7'
+  Tooltip.VERSION  = '3.4.1'
 
   Tooltip.TRANSITION_DURATION = 150
 
@@ -1299,7 +1458,10 @@ if (typeof jQuery === 'undefined') {
     viewport: {
       selector: 'body',
       padding: 0
-    }
+    },
+    sanitize : true,
+    sanitizeFn : null,
+    whiteList : DefaultWhitelist
   }
 
   Tooltip.prototype.init = function (type, element, options) {
@@ -1307,7 +1469,7 @@ if (typeof jQuery === 'undefined') {
     this.type      = type
     this.$element  = $(element)
     this.options   = this.getOptions(options)
-    this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
+    this.$viewport = this.options.viewport && $(document).find($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
     this.inState   = { click: false, hover: false, focus: false }
 
     if (this.$element[0] instanceof document.constructor && !this.options.selector) {
@@ -1340,7 +1502,15 @@ if (typeof jQuery === 'undefined') {
   }
 
   Tooltip.prototype.getOptions = function (options) {
-    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
+    var dataAttributes = this.$element.data()
+
+    for (var dataAttr in dataAttributes) {
+      if (dataAttributes.hasOwnProperty(dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) {
+        delete dataAttributes[dataAttr]
+      }
+    }
+
+    options = $.extend({}, this.getDefaults(), dataAttributes, options)
 
     if (options.delay && typeof options.delay == 'number') {
       options.delay = {
@@ -1349,6 +1519,10 @@ if (typeof jQuery === 'undefined') {
       }
     }
 
+    if (options.sanitize) {
+      options.template = sanitizeHtml(options.template, options.whiteList, options.sanitizeFn)
+    }
+
     return options
   }
 
@@ -1460,7 +1634,7 @@ if (typeof jQuery === 'undefined') {
         .addClass(placement)
         .data('bs.' + this.type, this)
 
-      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
+      this.options.container ? $tip.appendTo($(document).find(this.options.container)) : $tip.insertAfter(this.$element)
       this.$element.trigger('inserted.bs.' + this.type)
 
       var pos          = this.getPosition()
@@ -1562,7 +1736,16 @@ if (typeof jQuery === 'undefined') {
     var $tip  = this.tip()
     var title = this.getTitle()
 
-    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
+    if (this.options.html) {
+      if (this.options.sanitize) {
+        title = sanitizeHtml(title, this.options.whiteList, this.options.sanitizeFn)
+      }
+
+      $tip.find('.tooltip-inner').html(title)
+    } else {
+      $tip.find('.tooltip-inner').text(title)
+    }
+
     $tip.removeClass('fade in top bottom left right')
   }
 
@@ -1743,6 +1926,9 @@ if (typeof jQuery === 'undefined') {
     })
   }
 
+  Tooltip.prototype.sanitizeHtml = function (unsafeHtml) {
+    return sanitizeHtml(unsafeHtml, this.options.whiteList, this.options.sanitizeFn)
+  }
 
   // TOOLTIP PLUGIN DEFINITION
   // =========================
@@ -1776,10 +1962,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: popover.js v3.3.7
- * http://getbootstrap.com/javascript/#popovers
+ * Bootstrap: popover.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#popovers
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -1796,7 +1982,7 @@ if (typeof jQuery === 'undefined') {
 
   if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
 
-  Popover.VERSION  = '3.3.7'
+  Popover.VERSION  = '3.4.1'
 
   Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
     placement: 'right',
@@ -1822,10 +2008,25 @@ if (typeof jQuery === 'undefined') {
     var title   = this.getTitle()
     var content = this.getContent()
 
-    $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
-    $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events
-      this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
-    ](content)
+    if (this.options.html) {
+      var typeContent = typeof content
+
+      if (this.options.sanitize) {
+        title = this.sanitizeHtml(title)
+
+        if (typeContent === 'string') {
+          content = this.sanitizeHtml(content)
+        }
+      }
+
+      $tip.find('.popover-title').html(title)
+      $tip.find('.popover-content').children().detach().end()[
+        typeContent === 'string' ? 'html' : 'append'
+      ](content)
+    } else {
+      $tip.find('.popover-title').text(title)
+      $tip.find('.popover-content').children().detach().end().text(content)
+    }
 
     $tip.removeClass('fade top bottom left right in')
 
@@ -1844,8 +2045,8 @@ if (typeof jQuery === 'undefined') {
 
     return $e.attr('data-content')
       || (typeof o.content == 'function' ?
-            o.content.call($e[0]) :
-            o.content)
+        o.content.call($e[0]) :
+        o.content)
   }
 
   Popover.prototype.arrow = function () {
@@ -1885,10 +2086,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: scrollspy.js v3.3.7
- * http://getbootstrap.com/javascript/#scrollspy
+ * Bootstrap: scrollspy.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#scrollspy
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -1914,7 +2115,7 @@ if (typeof jQuery === 'undefined') {
     this.process()
   }
 
-  ScrollSpy.VERSION  = '3.3.7'
+  ScrollSpy.VERSION  = '3.4.1'
 
   ScrollSpy.DEFAULTS = {
     offset: 10
@@ -2058,10 +2259,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: tab.js v3.3.7
- * http://getbootstrap.com/javascript/#tabs
+ * Bootstrap: tab.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#tabs
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -2078,7 +2279,7 @@ if (typeof jQuery === 'undefined') {
     // jscs:enable requireDollarBeforejQueryAssignment
   }
 
-  Tab.VERSION = '3.3.7'
+  Tab.VERSION = '3.4.1'
 
   Tab.TRANSITION_DURATION = 150
 
@@ -2107,7 +2308,7 @@ if (typeof jQuery === 'undefined') {
 
     if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return
 
-    var $target = $(selector)
+    var $target = $(document).find(selector)
 
     this.activate($this.closest('li'), $ul)
     this.activate($target, $target.parent(), function () {
@@ -2132,15 +2333,15 @@ if (typeof jQuery === 'undefined') {
       $active
         .removeClass('active')
         .find('> .dropdown-menu > .active')
-          .removeClass('active')
+        .removeClass('active')
         .end()
         .find('[data-toggle="tab"]')
-          .attr('aria-expanded', false)
+        .attr('aria-expanded', false)
 
       element
         .addClass('active')
         .find('[data-toggle="tab"]')
-          .attr('aria-expanded', true)
+        .attr('aria-expanded', true)
 
       if (transition) {
         element[0].offsetWidth // reflow for transition
@@ -2152,10 +2353,10 @@ if (typeof jQuery === 'undefined') {
       if (element.parent('.dropdown-menu').length) {
         element
           .closest('li.dropdown')
-            .addClass('active')
+          .addClass('active')
           .end()
           .find('[data-toggle="tab"]')
-            .attr('aria-expanded', true)
+          .attr('aria-expanded', true)
       }
 
       callback && callback()
@@ -2214,10 +2415,10 @@ if (typeof jQuery === 'undefined') {
 }(jQuery);
 
 /* ========================================================================
- * Bootstrap: affix.js v3.3.7
- * http://getbootstrap.com/javascript/#affix
+ * Bootstrap: affix.js v3.4.1
+ * https://getbootstrap.com/docs/3.4/javascript/#affix
  * ========================================================================
- * Copyright 2011-2016 Twitter, Inc.
+ * Copyright 2011-2019 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
@@ -2231,7 +2432,9 @@ if (typeof jQuery === 'undefined') {
   var Affix = function (element, options) {
     this.options = $.extend({}, Affix.DEFAULTS, options)
 
-    this.$target = $(this.options.target)
+    var target = this.options.target === Affix.DEFAULTS.target ? $(this.options.target) : $(document).find(this.options.target)
+
+    this.$target = target
       .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
       .on('click.bs.affix.data-api',  $.proxy(this.checkPositionWithEventLoop, this))
 
@@ -2243,7 +2446,7 @@ if (typeof jQuery === 'undefined') {
     this.checkPosition()
   }
 
-  Affix.VERSION  = '3.3.7'
+  Affix.VERSION  = '3.4.1'
 
   Affix.RESET    = 'affix affix-top affix-bottom'
 

File diff ditekan karena terlalu besar
+ 2 - 2
public/assets/libs/bootstrap/dist/js/bootstrap.min.js


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini