Переглянути джерело

新增命令行一键生成API文档功能
新增插件绑定二级域名功能
新增加载JS公用模块
新增命令行创建插件自动生成菜单功能
新增后台菜单Fast.api.refreshmenu
新增后台菜单在数据变更后自动刷新的功能
新增require.min.js压缩版
新增从Headers中读取授权token的功能
新增Form.events.daterangepicker时间区别事件
新增Form表单提示成功和失败的回调事件
新增Fast.api.getrowbyid和Fast.api.getrowbyindex方法
新增commonsearch的find_in_set类型搜索
新增Menu::export的方法
新增php think api一键生成API文档功能
新增php think min的压缩参数和调试功能

优化API模块生产环境下错误信息的显示
优化移动端显示移除顶部Logo一行
优化bower.json和composer.json的版本依赖
优化插件管理列表显示
优化后台控制区多作的选项卡数据
优化CRUD生成的复选框样式及文字
优化规则管理的列表显示
优化第三方前端资源,移除冗余资源

修复在启用域名部署下的BUG
修复API初始化接口的BUG
修复会员积分日志模型BUG
修复多语言切换不存在的BUG
修复Backend.php中multi操作不触发模型事件的BUG

Karson 7 роки тому
батько
коміт
d863f93d10
78 змінених файлів з 6603 додано та 4719 видалено
  1. 7 1
      .bowerrc
  2. 3 1
      README.md
  3. 66 3
      application/admin/command/Addon.php
  4. 5 0
      application/admin/command/Addon/stubs/addon.stub
  5. 15 0
      application/admin/command/Addon/stubs/controller.stub
  6. 1 1
      application/admin/command/Addon/stubs/info.stub
  7. 178 0
      application/admin/command/Api.php
  8. 21 0
      application/admin/command/Api/lang/zh-cn.php
  9. 216 0
      application/admin/command/Api/library/Builder.php
  10. 549 0
      application/admin/command/Api/library/Extractor.php
  11. 460 0
      application/admin/command/Api/template/index.html
  12. 6 3
      application/admin/command/Crud.php
  13. 2 0
      application/admin/command/Install.php
  14. 51 1
      application/admin/command/Install/fastadmin.sql
  15. 0 1
      application/admin/command/Menu.php
  16. 13 2
      application/admin/command/Min.php
  17. 2 1
      application/admin/command/Min/stubs/css.stub
  18. 2 1
      application/admin/command/Min/stubs/js.stub
  19. 7 1
      application/admin/controller/Addon.php
  20. 10 2
      application/admin/controller/Index.php
  21. 1 1
      application/admin/lang/zh-cn/auth/rule.php
  22. 2 0
      application/admin/lang/zh-cn/user/user.php
  23. 4 4
      application/admin/library/Auth.php
  24. 2 1
      application/admin/library/traits/Backend.php
  25. 9 0
      application/admin/model/AuthRule.php
  26. 10 4
      application/admin/view/addon/index.html
  27. 1 1
      application/admin/view/auth/rule/add.html
  28. 14 1
      application/admin/view/auth/rule/index.html
  29. 1 1
      application/admin/view/category/index.html
  30. 4 136
      application/admin/view/common/control.html
  31. 1 1
      application/admin/view/common/header.html
  32. 4 4
      application/admin/view/common/menu.html
  33. 1 1
      application/admin/view/common/script.html
  34. 1 1
      application/admin/view/user/rule/index.html
  35. 2 1
      application/api/config.php
  36. 2 2
      application/api/controller/Common.php
  37. 40 0
      application/api/library/ExceptionHandle.php
  38. 1 0
      application/command.php
  39. 36 3
      application/common.php
  40. 5 0
      application/common/behavior/Common.php
  41. 21 18
      application/common/controller/Api.php
  42. 13 8
      application/common/controller/Backend.php
  43. 1 2
      application/common/controller/Frontend.php
  44. 98 5
      application/common/lang/zh-cn/addon.php
  45. 25 3
      application/common/library/Menu.php
  46. 1 1
      application/api/model/Area.php
  47. 1 1
      application/common/model/ScoreLog.php
  48. 54 0
      application/common/model/Version.php
  49. 1 1
      application/config.php
  50. 1 1
      application/index/lang/zh-cn.php
  51. 1 1
      application/index/view/common/script.html
  52. 0 2
      application/index/view/index/index.html
  53. 9 15
      bower.json
  54. 2 2
      composer.json
  55. 2878 0
      public/api.html
  56. 20 8
      public/assets/css/backend.css
  57. 1 1
      public/assets/css/backend.min.css
  58. 2 8
      public/assets/css/frontend.css
  59. 1 1
      public/assets/css/frontend.min.css
  60. 3 0
      public/assets/js/backend-init.js
  61. 3 0
      public/assets/js/backend.js
  62. 177 160
      public/assets/js/backend/addon.js
  63. 19 9
      public/assets/js/backend/auth/rule.js
  64. 20 58
      public/assets/js/backend/index.js
  65. 1 1
      public/assets/js/backend/user/rule.js
  66. 1 1
      public/assets/js/bootstrap-table-commonsearch.js
  67. 1 0
      public/assets/js/bootstrap-table-template.js
  68. 1 2
      public/assets/js/fast.js
  69. 3 0
      public/assets/js/frontend-init.js
  70. 8 11
      public/assets/js/require-backend.js
  71. 1321 2858
      public/assets/js/require-backend.min.js
  72. 64 2
      public/assets/js/require-form.js
  73. 7 10
      public/assets/js/require-frontend.js
  74. 13 1317
      public/assets/js/require-frontend.min.js
  75. 54 16
      public/assets/js/require-table.js
  76. 2 0
      public/assets/js/require.min.js
  77. 19 6
      public/assets/less/backend.less
  78. 1 10
      public/assets/less/frontend.less

+ 7 - 1
.bowerrc

@@ -1,3 +1,9 @@
 {
-  "directory" : "public/assets/libs"
+  "directory" : "public/assets/libs",
+  "ignoredDependencies": [
+    "file-saver",
+    "html2canvas",
+    "jspdf",
+    "jspdf-autotable"
+  ]
 }

+ 3 - 1
README.md

@@ -16,10 +16,12 @@ FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架。
     * 基于`Bower`进行前端组件包管理
 * 数据库表一键生成`CRUD`,包括控制器、模型、视图、JS、语言包、菜单等
 * 一键压缩打包JS和CSS文件,一键CDN静态资源部署
+* 一键生成API接口文档
 * 强大的插件扩展功能,在线安装卸载升级插件
 * 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
+* 二级域名部署支持,同时域名支持绑定到插件
 * 多语言支持,服务端及客户端支持
-* 强大的第三方插件支持(CMS、博客、文档生成)
+* 强大的第三方模块支持(CMS、博客、文档生成)
 * 整合第三方短信接口(阿里云、创蓝短信)
 * 无缝整合第三方云存储(七牛、阿里云OSS、又拍云)功能
 * 第三方登录(QQ、微信、微博)整合

+ 66 - 3
application/admin/command/Addon.php

@@ -2,14 +2,17 @@
 
 namespace app\admin\command;
 
+use app\common\library\Menu;
 use think\addons\AddonException;
 use think\addons\Service;
+use think\Config;
 use think\console\Command;
 use think\console\Input;
 use think\console\input\Option;
 use think\console\Output;
 use think\Db;
 use think\Exception;
+use think\exception\PDOException;
 
 class Addon extends Command
 {
@@ -63,14 +66,43 @@ class Addon extends Command
                     rmdirs($addonDir);
                 }
                 mkdir($addonDir);
+                mkdir($addonDir . DS . 'controller');
+                $menuList = Menu::export($name);
+                $createMenu = $this->getCreateMenu($menuList);
+                $prefix = Config::get('database.prefix');
+                $createTableSql = '';
+                try
+                {
+                    $result = Db::query("SHOW CREATE TABLE `" . $prefix . $name . "`;");
+                    if (isset($result[0]) && isset($result[0]['Create Table']))
+                    {
+                        $createTableSql = $result[0]['Create Table'];
+                    }
+                }
+                catch (PDOException $e)
+                {
+                    
+                }
+
                 $data = [
-                    'name'           => $name,
-                    'addon'          => $name,
-                    'addonClassName' => ucfirst($name)
+                    'name'               => $name,
+                    'addon'              => $name,
+                    'addonClassName'     => ucfirst($name),
+                    'addonInstallMenu'   => $createMenu ? "\$menu = " . var_export_short($createMenu, "\t") . ";\n\tMenu::create(\$menu);" : '',
+                    'addonUninstallMenu' => $menuList ? 'Menu::delete("' . $name . '");' : '',
+                    'addonEnableMenu'    => $menuList ? 'Menu::enable("' . $name . '");' : '',
+                    'addonDisableMenu'   => $menuList ? 'Menu::disable("' . $name . '");' : '',
                 ];
                 $this->writeToFile("addon", $data, $addonDir . ucfirst($name) . '.php');
                 $this->writeToFile("config", $data, $addonDir . 'config.php');
                 $this->writeToFile("info", $data, $addonDir . 'info.ini');
+                $this->writeToFile("controller", $data, $addonDir . 'controller' . DS . 'Index.php');
+                if ($createTableSql)
+                {
+                    $createTableSql = str_replace("`" . $prefix, '`__PREFIX__', $createTableSql);
+                    file_put_contents($addonDir . 'install.sql', $createTableSql);
+                }
+
                 $output->info("Create Successed!");
                 break;
             case 'disable':
@@ -257,6 +289,37 @@ class Addon extends Command
     }
 
     /**
+     * 获取创建菜单的数组
+     * @param array $menu
+     * @return array
+     */
+    protected function getCreateMenu($menu)
+    {
+        $result = [];
+        foreach ($menu as $k => & $v)
+        {
+            $arr = [
+                'name'  => $v['name'],
+                'title' => $v['title'],
+            ];
+            if ($v['icon'] != 'fa fa-circle-o')
+            {
+                $arr['icon'] = $v['icon'];
+            }
+            if ($v['ismenu'])
+            {
+                $arr['ismenu'] = $v['ismenu'];
+            }
+            if (isset($v['childlist']) && $v['childlist'])
+            {
+                $arr['sublist'] = $this->getCreateMenu($v['childlist']);
+            }
+            $result[] = $arr;
+        }
+        return $result;
+    }
+
+    /**
      * 写入到文件
      * @param string $name
      * @param array $data

+ 5 - 0
application/admin/command/Addon/stubs/addon.stub

@@ -2,6 +2,7 @@
 
 namespace addons\{%name%};
 
+use app\common\library\Menu;
 use think\Addons;
 
 /**
@@ -16,6 +17,7 @@ class {%addonClassName%} extends Addons
      */
     public function install()
     {
+        {%addonInstallMenu%}
         return true;
     }
 
@@ -25,6 +27,7 @@ class {%addonClassName%} extends Addons
      */
     public function uninstall()
     {
+        {%addonUninstallMenu%}
         return true;
     }
 
@@ -34,6 +37,7 @@ class {%addonClassName%} extends Addons
      */
     public function enable()
     {
+        {%addonEnableMenu%}
         return true;
     }
 
@@ -43,6 +47,7 @@ class {%addonClassName%} extends Addons
      */
     public function disable()
     {
+        {%addonDisableMenu%}
         return true;
     }
 

+ 15 - 0
application/admin/command/Addon/stubs/controller.stub

@@ -0,0 +1,15 @@
+<?php
+
+namespace addons\{%addon%}\controller;
+
+use think\addons\Controller;
+
+class Index extends Controller
+{
+
+    public function index()
+    {
+        $this->error("当前插件暂无前台页面");
+    }
+
+}

+ 1 - 1
application/admin/command/Addon/stubs/info.stub

@@ -1,5 +1,5 @@
 name = {%name%}
-title = 插件名称
+title = 插件名称({%name%})
 intro = FastAdmin插件
 author = yourname
 website = http://www.fastadmin.net

+ 178 - 0
application/admin/command/Api.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace app\admin\command;
+
+use app\admin\command\Api\library\Builder;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Exception;
+
+class Api extends Command
+{
+
+    protected function configure()
+    {
+        $site = Config::get('site');
+        $this
+                ->setName('api')
+                ->addOption('url', 'u', Option::VALUE_OPTIONAL, 'default api url', '')
+                ->addOption('module', 'm', Option::VALUE_OPTIONAL, 'module name(admin/index/api)', 'api')
+                ->addOption('output', 'o', Option::VALUE_OPTIONAL, 'output index file name', 'api.html')
+                ->addOption('template', 'e', Option::VALUE_OPTIONAL, '', 'index.html')
+                ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override general file', false)
+                ->addOption('title', 't', Option::VALUE_OPTIONAL, 'document title', $site['name'])
+                ->addOption('author', 'a', Option::VALUE_OPTIONAL, 'document author', $site['name'])
+                ->addOption('class', 'c', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'extend class', null)
+                ->addOption('language', 'l', Option::VALUE_OPTIONAL, 'language', 'zh-cn')
+                ->setDescription('Compress js and css file');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $apiDir = __DIR__ . DS . 'Api' . DS;
+
+        $force = $input->getOption('force');
+        $url = $input->getOption('url');
+        $language = $input->getOption('language');
+        $langFile = $apiDir . 'lang' . DS . $language . '.php';
+        if (!is_file($langFile))
+        {
+            throw new Exception('language file not found');
+        }
+        $lang = include $langFile;
+        // 目标目录
+        $output_dir = ROOT_PATH . 'public' . DS;
+        $output_file = $output_dir . $input->getOption('output');
+        if (is_file($output_file) && !$force)
+        {
+            throw new Exception("api index file already exists!\nIf you need to rebuild again, use the parameter --force=true ");
+        }
+        // 模板文件
+        $template_dir = $apiDir . 'template' . DS;
+        $template_file = $template_dir . $input->getOption('template');
+        if (!is_file($template_file))
+        {
+            throw new Exception('template file not found');
+        }
+        // 额外的类
+        $classes = $input->getOption('class');
+        // 标题
+        $title = $input->getOption('title');
+        // 作者
+        $author = $input->getOption('author');
+        // 模块
+        $module = $input->getOption('module');
+
+        $moduleDir = APP_PATH . $module . DS;
+        if (!is_dir($moduleDir))
+        {
+            throw new Exception('module not found');
+        }
+        $controllerDir = $moduleDir . Config::get('url_controller_layer') . DS;
+        $files = new \RecursiveIteratorIterator(
+                new \RecursiveDirectoryIterator($controllerDir), \RecursiveIteratorIterator::LEAVES_ONLY
+        );
+
+        foreach ($files as $name => $file)
+        {
+            if (!$file->isDir())
+            {
+                $filePath = $file->getRealPath();
+                $classes[] = $this->get_class_from_file($filePath);
+            }
+        }
+
+        $config = [
+            'title'       => $title,
+            'author'      => $author,
+            'description' => '',
+            'apiurl'      => $url,
+        ];
+        $builder = new Builder($classes);
+        $content = $builder->render($template_file, ['config' => $config, 'lang' => $lang]);
+
+        if (!file_put_contents($output_file, $content))
+        {
+            throw new Exception('Cannot save the content to ' . $output_file);
+        }
+        $output->info("Build Successed!");
+    }
+
+    /**
+     * get full qualified class name
+     * 
+     * @param string $path_to_file
+     * @author JBYRNE http://jarretbyrne.com/2015/06/197/
+     * @return string
+     */
+    protected function get_class_from_file($path_to_file)
+    {
+        //Grab the contents of the file
+        $contents = file_get_contents($path_to_file);
+
+        //Start with a blank namespace and class
+        $namespace = $class = "";
+
+        //Set helper values to know that we have found the namespace/class token and need to collect the string values after them
+        $getting_namespace = $getting_class = false;
+
+        //Go through each token and evaluate it as necessary
+        foreach (token_get_all($contents) as $token)
+        {
+
+            //If this token is the namespace declaring, then flag that the next tokens will be the namespace name
+            if (is_array($token) && $token[0] == T_NAMESPACE)
+            {
+                $getting_namespace = true;
+            }
+
+            //If this token is the class declaring, then flag that the next tokens will be the class name
+            if (is_array($token) && $token[0] == T_CLASS)
+            {
+                $getting_class = true;
+            }
+
+            //While we're grabbing the namespace name...
+            if ($getting_namespace === true)
+            {
+
+                //If the token is a string or the namespace separator...
+                if (is_array($token) && in_array($token[0], [T_STRING, T_NS_SEPARATOR]))
+                {
+
+                    //Append the token's value to the name of the namespace
+                    $namespace .= $token[1];
+                }
+                else if ($token === ';')
+                {
+
+                    //If the token is the semicolon, then we're done with the namespace declaration
+                    $getting_namespace = false;
+                }
+            }
+
+            //While we're grabbing the class name...
+            if ($getting_class === true)
+            {
+
+                //If the token is a string, it's the name of the class
+                if (is_array($token) && $token[0] == T_STRING)
+                {
+
+                    //Store the token's value as the class name
+                    $class = $token[1];
+
+                    //Got what we need, stope here
+                    break;
+                }
+            }
+        }
+
+        //Build the fully-qualified class name and return it
+        return $namespace ? $namespace . '\\' . $class : $class;
+    }
+
+}

+ 21 - 0
application/admin/command/Api/lang/zh-cn.php

@@ -0,0 +1,21 @@
+<?php
+
+return [
+    'Info'             => '基础信息',
+    'Sandbox'          => '在线测试',
+    'Sampleoutput'     => '返回示例',
+    'Headers'          => 'Headers',
+    'Parameters'       => '参数',
+    'Body'             => '正文',
+    'Name'             => '名称',
+    'Type'             => '类型',
+    'Required'         => '必选',
+    'Description'      => '描述',
+    'Send'             => '提交',
+    'Tokentips'        => 'Token在会员注册或登录后都会返回,WEB端同时存在于Cookie中',
+    'Apiurltips'       => 'API接口URL',
+    'Savetips'         => '点击保存后Token和Api url都将保存在本地Localstorage中',
+    'ReturnHeaders'    => '响应头',
+    'ReturnParameters' => '返回参数',
+    'Response'         => '响应输出',
+];

+ 216 - 0
application/admin/command/Api/library/Builder.php

@@ -0,0 +1,216 @@
+<?php
+
+namespace app\admin\command\Api\library;
+
+use think\Config;
+
+/**
+ * @website https://github.com/calinrada/php-apidoc
+ * @author  Calin Rada <rada.calin@gmail.com>
+ * @author  Karson <karsonzhang@163.com>
+ */
+class Builder
+{
+
+    /**
+     *
+     * @var \think\View 
+     */
+    public $view = null;
+
+    /**
+     * parse classes
+     * @var array 
+     */
+    protected $classes = [];
+
+    /**
+     * 
+     * @param array $classes
+     */
+    public function __construct($classes = [])
+    {
+        $this->classes = array_merge($this->classes, $classes);
+        $this->view = \think\View::instance(Config::get('template'), Config::get('view_replace_str'));
+    }
+
+    protected function extractAnnotations()
+    {
+        $st_output = [];
+        foreach ($this->classes as $class)
+        {
+            $st_output[] = Extractor::getAllClassAnnotations($class);
+        }
+        return end($st_output);
+    }
+
+    protected function generateHeadersTemplate($docs)
+    {
+        if (!isset($docs['ApiHeaders']))
+        {
+            return [];
+        }
+
+        $headerslist = array();
+        foreach ($docs['ApiHeaders'] as $params)
+        {
+            $tr = array(
+                'name'        => $params['name'],
+                'type'        => $params['type'],
+                'sample'      => isset($params['sample']) ? $params['sample'] : '',
+                'required'    => isset($params['required']) ? $params['required'] : false,
+                'description' => isset($params['description']) ? $params['description'] : '',
+            );
+            $headerslist[] = $tr;
+        }
+
+        return $headerslist;
+    }
+
+    protected function generateParamsTemplate($docs)
+    {
+        if (!isset($docs['ApiParams']))
+        {
+            return [];
+        }
+
+        $paramslist = array();
+        foreach ($docs['ApiParams'] as $params)
+        {
+            $tr = array(
+                'name'        => $params['name'],
+                'type'        => isset($params['type']) ? $params['type'] : 'string',
+                'sample'      => isset($params['sample']) ? $params['sample'] : '',
+                'required'    => isset($params['required']) ? $params['required'] : true,
+                'description' => isset($params['description']) ? $params['description'] : '',
+            );
+            $paramslist[] = $tr;
+        }
+
+        return $paramslist;
+    }
+
+    protected function generateReturnHeadersTemplate($docs)
+    {
+        if (!isset($docs['ApiReturnHeaders']))
+        {
+            return [];
+        }
+
+        $headerslist = array();
+        foreach ($docs['ApiReturnHeaders'] as $params)
+        {
+            $tr = array(
+                'name'        => $params['name'],
+                'type'        => 'string',
+                'sample'      => isset($params['sample']) ? $params['sample'] : '',
+                'required'    => isset($params['required']) && $params['required'] ? 'Yes' : 'No',
+                'description' => isset($params['description']) ? $params['description'] : '',
+            );
+            $headerslist[] = $tr;
+        }
+
+        return $headerslist;
+    }
+
+    protected function generateReturnParamsTemplate($st_params)
+    {
+        if (!isset($st_params['ApiReturnParams']))
+        {
+            return [];
+        }
+
+        $paramslist = array();
+        foreach ($st_params['ApiReturnParams'] as $params)
+        {
+            $tr = array(
+                'name'        => $params['name'],
+                'type'        => isset($params['type']) ? $params['type'] : 'string',
+                'sample'      => isset($params['sample']) ? $params['sample'] : '',
+                'description' => isset($params['description']) ? $params['description'] : '',
+            );
+            $paramslist[] = $tr;
+        }
+
+        return $paramslist;
+    }
+
+    protected function generateBadgeForMethod($data)
+    {
+        $method = strtoupper(is_array($data['ApiMethod'][0]) ? $data['ApiMethod'][0]['data'] : $data['ApiMethod'][0]);
+        $labes = array(
+            'POST'    => 'label-primary',
+            'GET'     => 'label-success',
+            'PUT'     => 'label-warning',
+            'DELETE'  => 'label-danger',
+            'PATCH'   => 'label-default',
+            'OPTIONS' => 'label-info'
+        );
+
+        return isset($labes[$method]) ? $labes[$method] : $labes['GET'];
+    }
+
+    public function parse()
+    {
+        $annotations = $this->extractAnnotations();
+
+        $counter = 0;
+        $section = null;
+        $docslist = [];
+        foreach ($annotations as $class => $methods)
+        {
+            foreach ($methods as $name => $docs)
+            {
+                if (isset($docs['ApiSector'][0]))
+                {
+                    $section = is_array($docs['ApiSector'][0]) ? $docs['ApiSector'][0]['data'] : $docs['ApiSector'][0];
+                }
+                else
+                {
+                    $section = $class;
+                }
+                if (0 === count($docs))
+                {
+                    continue;
+                }
+
+                $docslist[$section][] = [
+                    'id'                => $counter,
+                    'method'            => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0],
+                    'method_label'      => $this->generateBadgeForMethod($docs),
+                    'section'           => $section,
+                    'route'             => is_array($docs['ApiRoute'][0]) ? $docs['ApiRoute'][0]['data'] : $docs['ApiRoute'][0],
+                    'summary'           => is_array($docs['ApiSummary'][0]) ? $docs['ApiSummary'][0]['data'] : $docs['ApiSummary'][0],
+                    'body'              => isset($docs['ApiBody'][0]) ? is_array($docs['ApiBody'][0]) ? $docs['ApiBody'][0]['data'] : $docs['ApiBody'][0] : '',
+                    'headerslist'       => $this->generateHeadersTemplate($docs),
+                    'paramslist'        => $this->generateParamsTemplate($docs),
+                    'returnheaderslist' => $this->generateReturnHeadersTemplate($docs),
+                    'returnparamslist'  => $this->generateReturnParamsTemplate($docs),
+                    'return'            => isset($docs['ApiReturn']) ? is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0] : '',
+                ];
+                $counter++;
+            }
+        }
+
+        return $docslist;
+    }
+
+    public function getView()
+    {
+        return $this->view;
+    }
+
+    /**
+     * 渲染
+     * @param string $template
+     * @param array $vars
+     * @return string
+     */
+    public function render($template, $vars = [])
+    {
+        $docslist = $this->parse();
+
+        return $this->view->display(file_get_contents($template), array_merge($vars, ['docslist' => $docslist]));
+    }
+
+}

+ 549 - 0
application/admin/command/Api/library/Extractor.php

@@ -0,0 +1,549 @@
+<?php
+
+namespace app\admin\command\Api\library;
+
+/**
+ * Class imported from https://github.com/eriknyk/Annotations
+ * @author  Erik Amaru Ortiz https://github.com/eriknyk‎
+ *
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ * @author  Calin Rada <rada.calin@gmail.com>
+ */
+class Extractor
+{
+
+    /**
+     * Static array to store already parsed annotations
+     * @var array
+     */
+    private static $annotationCache;
+
+    /**
+     * Indicates that annotations should has strict behavior, 'false' by default
+     * @var boolean
+     */
+    private $strict = false;
+
+    /**
+     * Stores the default namespace for Objects instance, usually used on methods like getMethodAnnotationsObjects()
+     * @var string
+     */
+    public $defaultNamespace = '';
+
+    /**
+     * Sets strict variable to true/false
+     * @param bool $value boolean value to indicate that annotations to has strict behavior
+     */
+    public function setStrict($value)
+    {
+        $this->strict = (bool) $value;
+    }
+
+    /**
+     * Sets default namespace to use in object instantiation
+     * @param string $namespace default namespace
+     */
+    public function setDefaultNamespace($namespace)
+    {
+        $this->defaultNamespace = $namespace;
+    }
+
+    /**
+     * Gets default namespace used in object instantiation
+     * @return string $namespace default namespace
+     */
+    public function getDefaultAnnotationNamespace()
+    {
+        return $this->defaultNamespace;
+    }
+
+    /**
+     * Gets all anotations with pattern @SomeAnnotation() from a given class
+     *
+     * @param  string $className class name to get annotations
+     * @return array  self::$annotationCache all annotated elements
+     */
+    public static function getClassAnnotations($className)
+    {
+        if (!isset(self::$annotationCache[$className]))
+        {
+            $class = new \ReflectionClass($className);
+            self::$annotationCache[$className] = self::parseAnnotations($class->getDocComment());
+        }
+
+        return self::$annotationCache[$className];
+    }
+
+    public static function getAllClassAnnotations($className)
+    {
+        $class = new \ReflectionClass($className);
+
+        foreach ($class->getMethods() as $object)
+        {
+            self::$annotationCache['annotations'][$className][$object->name] = self::getMethodAnnotations($className, $object->name);
+        }
+
+        return self::$annotationCache['annotations'];
+    }
+
+    /**
+     * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
+     *
+     * @param  string $className  class name
+     * @param  string $methodName method name to get annotations
+     * @return array  self::$annotationCache all annotated elements of a method given
+     */
+    public static function getMethodAnnotations($className, $methodName)
+    {
+        if (!isset(self::$annotationCache[$className . '::' . $methodName]))
+        {
+            try
+            {
+                $method = new \ReflectionMethod($className, $methodName);
+                $class = new \ReflectionClass($className);
+                if (!$method->isPublic() || $method->isConstructor())
+                {
+                    $annotations = array();
+                }
+                else
+                {
+                    $annotations = self::consolidateAnnotations($method, $class);
+                }
+            }
+            catch (\ReflectionException $e)
+            {
+                $annotations = array();
+            }
+
+            self::$annotationCache[$className . '::' . $methodName] = $annotations;
+        }
+
+        return self::$annotationCache[$className . '::' . $methodName];
+    }
+
+    /**
+     * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
+     * and instance its abcAnnotation class
+     *
+     * @param  string $className  class name
+     * @param  string $methodName method name to get annotations
+     * @return array  self::$annotationCache all annotated objects of a method given
+     */
+    public function getMethodAnnotationsObjects($className, $methodName)
+    {
+        $annotations = $this->getMethodAnnotations($className, $methodName);
+        $objects = array();
+
+        $i = 0;
+
+        foreach ($annotations as $annotationClass => $listParams)
+        {
+            $annotationClass = ucfirst($annotationClass);
+            $class = $this->defaultNamespace . $annotationClass . 'Annotation';
+
+            // verify is the annotation class exists, depending if Annotations::strict is true
+            // if not, just skip the annotation instance creation.
+            if (!class_exists($class))
+            {
+                if ($this->strict)
+                {
+                    throw new Exception(sprintf('Runtime Error: Annotation Class Not Found: %s', $class));
+                }
+                else
+                {
+                    // silent skip & continue
+                    continue;
+                }
+            }
+
+            if (empty($objects[$annotationClass]))
+            {
+                $objects[$annotationClass] = new $class();
+            }
+
+            foreach ($listParams as $params)
+            {
+                if (is_array($params))
+                {
+                    foreach ($params as $key => $value)
+                    {
+                        $objects[$annotationClass]->set($key, $value);
+                    }
+                }
+                else
+                {
+                    $objects[$annotationClass]->set($i++, $params);
+                }
+            }
+        }
+
+        return $objects;
+    }
+
+    private static function consolidateAnnotations($method, $class)
+    {
+        $dockblockClass = $class->getDocComment();
+        $docblockMethod = $method->getDocComment();
+        $methodName = $method->getName();
+
+        $methodAnnotations = self::parseAnnotations($docblockMethod);
+        $classAnnotations = self::parseAnnotations($dockblockClass);
+        if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty')
+        {
+            return [];
+        }
+
+        $properties = $class->getDefaultProperties();
+        $noNeedLogin = isset($properties['noNeedLogin']) ? is_array($properties['noNeedLogin']) ? $properties['noNeedLogin'] : [$properties['noNeedLogin']] : [];
+        $noNeedRight = isset($properties['noNeedRight']) ? is_array($properties['noNeedRight']) ? $properties['noNeedRight'] : [$properties['noNeedRight']] : [];
+
+        preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $docblockMethod), $methodArr);
+        preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $dockblockClass), $classArr);
+
+        $methodTitle = isset($methodArr[1]) && isset($methodArr[1][0]) ? $methodArr[1][0] : '';
+        $classTitle = isset($classArr[1]) && isset($classArr[1][0]) ? $classArr[1][0] : '';
+
+        if (!isset($methodAnnotations['ApiMethod']))
+        {
+            $methodAnnotations['ApiMethod'] = ['get'];
+        }
+        if (!isset($methodAnnotations['ApiSummary']))
+        {
+            $methodAnnotations['ApiSummary'] = [$methodTitle];
+        }
+
+        if ($methodAnnotations)
+        {
+            foreach ($classAnnotations as $name => $valueClass)
+            {
+                if (count($valueClass) !== 1)
+                {
+                    continue;
+                }
+
+                if ($name === 'ApiRoute')
+                {
+                    if (isset($methodAnnotations[$name]))
+                    {
+                        $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . $methodAnnotations[$name][0]];
+                    }
+                    else
+                    {
+                        $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . '/' . $method->getName()];
+                    }
+                }
+
+                if ($name === 'ApiSector')
+                {
+                    $methodAnnotations[$name] = $valueClass;
+                }
+            }
+        }
+        if (!isset($methodAnnotations['ApiTitle']))
+        {
+            $methodAnnotations['ApiTitle'] = [$methodTitle];
+        }
+        if (!isset($methodAnnotations['ApiRoute']))
+        {
+            $urlArr = [];
+            $className = $class->getName();
+
+            list($prefix, $suffix) = explode('\\' . \think\Config::get('url_controller_layer') . '\\', $className);
+            $prefixArr = explode('\\', $prefix);
+            $suffixArr = explode('\\', $suffix);
+            if ($prefixArr[0] == \think\Config::get('app_namespace'))
+            {
+                $prefixArr[0] = '';
+            }
+            $urlArr = array_merge($urlArr, $prefixArr);
+            $urlArr[] = implode('.', array_map(function($item) {
+                        return \think\Loader::parseName($item);
+                    }, $suffixArr));
+            $urlArr[] = $method->getName();
+            $methodAnnotations['ApiRoute'] = [implode('/', $urlArr)];
+        }
+        if (!isset($methodAnnotations['ApiSector']))
+        {
+            $methodAnnotations['ApiSector'] = isset($classAnnotations['ApiSector']) ? $classAnnotations['ApiSector'] : [$classTitle];
+        }
+        if (!isset($methodAnnotations['ApiParams']))
+        {
+            $params = self::parseCustomAnnotations($docblockMethod, 'param');
+            foreach ($params as $k => $v)
+            {
+                $arr = explode(' ', preg_replace("/[\s]+/", " ", $v));
+                $methodAnnotations['ApiParams'][] = [
+                    'name'        => isset($arr[1]) ? str_replace('$', '', $arr[1]) : '',
+                    'nullable'    => false,
+                    'type'        => isset($arr[0]) ? $arr[0] : 'string',
+                    'description' => isset($arr[2]) ? $arr[2] : ''
+                ];
+            }
+        }
+        $methodAnnotations['ApiPermissionLogin'] = [!in_array('*', $noNeedLogin) && !in_array($methodName, $noNeedLogin)];
+        $methodAnnotations['ApiPermissionRight'] = [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
+        return $methodAnnotations;
+    }
+
+    /**
+     * Parse annotations
+     *
+     * @param  string $docblock
+     * @param  string $name
+     * @return array  parsed annotations params
+     */
+    private static function parseCustomAnnotations($docblock, $name = 'param')
+    {
+        $annotations = array();
+
+        $docblock = substr($docblock, 3, -2);
+        if (preg_match_all('/@' . $name . '(?:\s*(?:\(\s*)?(.*?)(?:\s*\))?)??\s*(?:\n|\*\/)/', $docblock, $matches))
+        {
+            foreach ($matches[1] as $k => $v)
+            {
+                $annotations[] = $v;
+            }
+        }
+        return $annotations;
+    }
+
+    /**
+     * Parse annotations
+     *
+     * @param  string $docblock
+     * @return array  parsed annotations params
+     */
+    private static function parseAnnotations($docblock)
+    {
+        $annotations = array();
+
+        // Strip away the docblock header and footer to ease parsing of one line annotations
+        $docblock = substr($docblock, 3, -2);
+        if (preg_match_all('/@(?<name>[A-Za-z_-]+)[\s\t]*\((?<args>(?:(?!\)).)*)\)\r?/s', $docblock, $matches))
+        {
+            $numMatches = count($matches[0]);
+
+            for ($i = 0; $i < $numMatches; ++$i)
+            {
+                // annotations has arguments
+                if (isset($matches['args'][$i]))
+                {
+                    $argsParts = trim($matches['args'][$i]);
+                    $name = $matches['name'][$i];
+                    $value = self::parseArgs($argsParts);
+                }
+                else
+                {
+                    $value = array();
+                }
+
+                $annotations[$name][] = $value;
+            }
+        }
+
+        return $annotations;
+    }
+
+    /**
+     * Parse individual annotation arguments
+     *
+     * @param  string $content arguments string
+     * @return array  annotated arguments
+     */
+    private static function parseArgs($content)
+    {
+        // Replace initial stars
+        $content = preg_replace('/^\s*\*/m', '', $content);
+
+        $data = array();
+        $len = strlen($content);
+        $i = 0;
+        $var = '';
+        $val = '';
+        $level = 1;
+
+        $prevDelimiter = '';
+        $nextDelimiter = '';
+        $nextToken = '';
+        $composing = false;
+        $type = 'plain';
+        $delimiter = null;
+        $quoted = false;
+        $tokens = array('"', '"', '{', '}', ',', '=');
+
+        while ($i <= $len)
+        {
+            $prev_c = substr($content, $i - 1, 1);
+            $c = substr($content, $i++, 1);
+
+            if ($c === '"' && $prev_c !== "\\")
+            {
+                $delimiter = $c;
+                //open delimiter
+                if (!$composing && empty($prevDelimiter) && empty($nextDelimiter))
+                {
+                    $prevDelimiter = $nextDelimiter = $delimiter;
+                    $val = '';
+                    $composing = true;
+                    $quoted = true;
+                }
+                else
+                {
+                    // close delimiter
+                    if ($c !== $nextDelimiter)
+                    {
+                        throw new Exception(sprintf(
+                                "Parse Error: enclosing error -> expected: [%s], given: [%s]", $nextDelimiter, $c
+                        ));
+                    }
+
+                    // validating syntax
+                    if ($i < $len)
+                    {
+                        if (',' !== substr($content, $i, 1) && '\\' !== $prev_c)
+                        {
+                            throw new Exception(sprintf(
+                                    "Parse Error: missing comma separator near: ...%s<--", substr($content, ($i - 10), $i)
+                            ));
+                        }
+                    }
+
+                    $prevDelimiter = $nextDelimiter = '';
+                    $composing = false;
+                    $delimiter = null;
+                }
+            }
+            elseif (!$composing && in_array($c, $tokens))
+            {
+                switch ($c)
+                {
+                    case '=':
+                        $prevDelimiter = $nextDelimiter = '';
+                        $level = 2;
+                        $composing = false;
+                        $type = 'assoc';
+                        $quoted = false;
+                        break;
+                    case ',':
+                        $level = 3;
+
+                        // If composing flag is true yet,
+                        // it means that the string was not enclosed, so it is parsing error.
+                        if ($composing === true && !empty($prevDelimiter) && !empty($nextDelimiter))
+                        {
+                            throw new Exception(sprintf(
+                                    "Parse Error: enclosing error -> expected: [%s], given: [%s]", $nextDelimiter, $c
+                            ));
+                        }
+
+                        $prevDelimiter = $nextDelimiter = '';
+                        break;
+                    case '{':
+                        $subc = '';
+                        $subComposing = true;
+
+                        while ($i <= $len)
+                        {
+                            $c = substr($content, $i++, 1);
+
+                            if (isset($delimiter) && $c === $delimiter)
+                            {
+                                throw new Exception(sprintf(
+                                        "Parse Error: Composite variable is not enclosed correctly."
+                                ));
+                            }
+
+                            if ($c === '}')
+                            {
+                                $subComposing = false;
+                                break;
+                            }
+                            $subc .= $c;
+                        }
+
+                        // if the string is composing yet means that the structure of var. never was enclosed with '}'
+                        if ($subComposing)
+                        {
+                            throw new Exception(sprintf(
+                                    "Parse Error: Composite variable is not enclosed correctly. near: ...%s'", $subc
+                            ));
+                        }
+
+                        $val = self::parseArgs($subc);
+                        break;
+                }
+            }
+            else
+            {
+                if ($level == 1)
+                {
+                    $var .= $c;
+                }
+                elseif ($level == 2)
+                {
+                    $val .= $c;
+                }
+            }
+
+            if ($level === 3 || $i === $len)
+            {
+                if ($type == 'plain' && $i === $len)
+                {
+                    $data = self::castValue($var);
+                }
+                else
+                {
+                    $data[trim($var)] = self::castValue($val, !$quoted);
+                }
+
+                $level = 1;
+                $var = $val = '';
+                $composing = false;
+                $quoted = false;
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Try determinate the original type variable of a string
+     *
+     * @param  string  $val  string containing possibles variables that can be cast to bool or int
+     * @param  boolean $trim indicate if the value passed should be trimmed after to try cast
+     * @return mixed   returns the value converted to original type if was possible
+     */
+    private static function castValue($val, $trim = false)
+    {
+        if (is_array($val))
+        {
+            foreach ($val as $key => $value)
+            {
+                $val[$key] = self::castValue($value);
+            }
+        }
+        elseif (is_string($val))
+        {
+            if ($trim)
+            {
+                $val = trim($val);
+            }
+            $val = stripslashes($val);
+            $tmp = strtolower($val);
+
+            if ($tmp === 'false' || $tmp === 'true')
+            {
+                $val = $tmp === 'true';
+            }
+            elseif (is_numeric($val))
+            {
+                return $val + 0;
+            }
+
+            unset($tmp);
+        }
+
+        return $val;
+    }
+
+}

+ 460 - 0
application/admin/command/Api/template/index.html

@@ -0,0 +1,460 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=edge">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="description" content="">
+        <meta name="author" content="{$config.author}">
+        <title>{$config.title}</title>
+        <link href="https://cdn.bootcss.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
+        <style type="text/css">
+            body      { padding-top: 70px; margin-bottom: 15px; }
+            .tab-pane { padding-top: 10px; }
+            .mt0      { margin-top: 0px; }
+            .footer   { font-size: 12px; color: #666; }
+            .label    { display: inline-block; min-width: 65px; padding: 0.3em 0.6em 0.3em; }
+            .string   { color: green; }
+            .number   { color: darkorange; }
+            .boolean  { color: blue; }
+            .null     { color: magenta; }
+            .key      { color: red; }
+            .popover  { max-width: 400px; max-height: 400px; overflow-y: auto;}
+        </style>
+    </head>
+    <body>
+        <!-- Fixed navbar -->
+        <div class="navbar navbar-default navbar-fixed-top" role="navigation">
+            <div class="container">
+                <div class="navbar-header">
+                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+                        <span class="sr-only">Toggle navigation</span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </button>
+                    <a class="navbar-brand" href="http://www.fastadmin.net" target="_blank">{$config.title}</a>
+                </div>
+                <div class="navbar-collapse collapse">
+                    <form class="navbar-form navbar-right">
+                        <div class="form-group">
+                            Token:
+                        </div>
+                        <div class="form-group">
+                            <input type="text" class="form-control input-sm" data-toggle="tooltip" title="{$lang.Tokentips}" placeholder="token" id="token" />
+                        </div>
+                        <div class="form-group">
+                            Apiurl:
+                        </div>
+                        <div class="form-group">
+                            <input id="apiUrl" type="text" class="form-control input-sm" data-toggle="tooltip" title="{$lang.Apiurltips}" placeholder="https://api.mydomain.com" value="{$config.apiurl}" />
+                        </div>
+                        <div class="form-group">
+                            <button type="button" class="btn btn-success btn-sm" data-toggle="tooltip" title="{$lang.Savetips}" id="save_data">
+                                <span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>
+                            </button>
+                        </div>
+                    </form>
+                </div><!--/.nav-collapse -->
+            </div>
+        </div>
+
+        <div class="container">
+            <div class="panel-group" id="accordion">
+                {foreach name="docslist" id="docs"}
+                <h2>{$key}</h2>
+                <hr>
+                {foreach name="docs" id="api" }
+                <div class="panel panel-default">
+                    <div class="panel-heading">
+                        <h4 class="panel-title">
+                            <span class="label {$api.method_label}">{$api.method|strtoupper}</span> <a data-toggle="collapse" data-parent="#accordion{$api.id}" href="#collapseOne{$api.id}"> {$api.route}</a>
+                        </h4>
+                    </div>
+                    <div id="collapseOne{$api.id}" class="panel-collapse collapse">
+                        <div class="panel-body">
+
+                            <!-- Nav tabs -->
+                            <ul class="nav nav-tabs" id="doctab{$api.id}">
+                                <li class="active"><a href="#info{$api.id}" data-toggle="tab">{$lang.Info}</a></li>
+                                <li><a href="#sandbox{$api.id}" data-toggle="tab">{$lang.Sandbox}</a></li>
+                                <li><a href="#sample{$api.id}" data-toggle="tab">{$lang.Sampleoutput}</a></li>
+                            </ul>
+
+                            <!-- Tab panes -->
+                            <div class="tab-content">
+
+                                <div class="tab-pane active" id="info{$api.id}">
+                                    <div class="well">
+                                        {$api.summary}
+                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>{$lang.Headers}</strong></div>
+                                        <div class="panel-body">
+                                            {if $api.headerslist}
+                                            <table class="table table-hover">
+                                                <thead>
+                                                    <tr>
+                                                        <th>{$lang.Name}</th>
+                                                        <th>{$lang.Type}</th>
+                                                        <th>{$lang.Required}</th>
+                                                        <th>{$lang.Description}</th>
+                                                    </tr>
+                                                </thead>
+                                                <tbody>
+                                                    {foreach name="api['headerslist']" id="header"}
+                                                    <tr>
+                                                        <td>{$header.name}</td>
+                                                        <td>{$header.type}</td>
+                                                        <td>{$header.required?'是':'否'}</td>
+                                                        <td>{$header.description}</td>
+                                                    </tr>
+                                                    {/foreach}
+                                                </tbody>
+                                            </table>
+                                            {else /}
+                                            无
+                                            {/if}
+                                        </div>
+                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
+                                        <div class="panel-body">
+                                            {if $api.paramslist}
+                                            <table class="table table-hover">
+                                                <thead>
+                                                    <tr>
+                                                        <th>{$lang.Name}</th>
+                                                        <th>{$lang.Type}</th>
+                                                        <th>{$lang.Required}</th>
+                                                        <th>{$lang.Description}</th>
+                                                    </tr>
+                                                </thead>
+                                                <tbody>
+                                                    {foreach name="api['paramslist']" id="param"}
+                                                    <tr>
+                                                        <td>{$param.name}</td>
+                                                        <td>{$param.type}</td>
+                                                        <td>{:$param.required?'是':'否'}</td>
+                                                        <td>{$param.description}</td>
+                                                    </tr>
+                                                    {/foreach}
+                                                </tbody>
+                                            </table>
+                                            {else /}
+                                            无
+                                            {/if}
+                                        </div>
+                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>{$lang.Body}</strong></div>
+                                        <div class="panel-body">
+                                            {$api.body|default='无'}
+                                        </div>
+                                    </div>
+                                </div><!-- #info -->
+
+                                <div class="tab-pane" id="sandbox{$api.id}">
+                                    <div class="row">
+                                        <div class="col-md-12">
+                                            {if $api.headerslist}
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>{$lang.Headers}</strong></div>
+                                                <div class="panel-body">
+                                                    <div class="headers">
+                                                        {foreach name="api['headerslist']" id="param"}
+                                                        <div class="form-group">
+                                                            <label class="control-label" for="{$param.name}">{$param.name}</label>
+                                                            <input type="{$param.type}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description} - Ex: {$param.sample}" name="{$param.name}">
+                                                        </div>
+                                                        {/foreach}
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            {/if}
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
+                                                <div class="panel-body">
+                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="{$api.route}" method="{$api.method}" name="form{$api.id}" id="form{$api.id}">
+                                                        {if $api.paramslist}
+                                                        {foreach name="api['paramslist']" id="param"}
+                                                        <div class="form-group">
+                                                            <label class="control-label" for="{$param.name}">{$param.name}</label>
+                                                            <input type="{$param.type}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description}{if $param.sample} - 例: {$param.sample}{/if}" name="{$param.name}">
+                                                        </div>
+                                                        {/foreach}
+                                                        {else /}
+                                                        <div class="form-group">
+                                                            无
+                                                        </div>
+                                                        {/if}
+                                                        <div class="form-group">
+                                                            <button type="submit" class="btn btn-success send" rel="{$api.id}">{$lang.Send}</button>
+                                                        </div>
+                                                    </form>
+                                                </div>
+                                            </div>
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>{$lang.Response}</strong></div>
+                                                <div class="panel-body">
+                                                    <div class="row">
+                                                        <div class="col-md-12" style="overflow-x:auto">
+                                                            <pre id="response_headers{$api.id}"></pre>
+                                                            <pre id="response{$api.id}"></pre>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>{$lang.ReturnParameters}</strong></div>
+                                                <div class="panel-body">
+                                                    {if $api.returnparamslist}
+                                                    <table class="table table-hover">
+                                                        <thead>
+                                                            <tr>
+                                                                <th>{$lang.Name}</th>
+                                                                <th>{$lang.Type}</th>
+                                                                <th>{$lang.Description}</th>
+                                                            </tr>
+                                                        </thead>
+                                                        <tbody>
+                                                            {foreach name="api['returnparamslist']" id="param"}
+                                                            <tr>
+                                                                <td>{$param.name}</td>
+                                                                <td>{$param.type}</td>
+                                                                <td>{$param.description}</td>
+                                                            </tr>
+                                                            {/foreach}
+                                                        </tbody>
+                                                    </table>
+                                                    {else /}
+                                                    无
+                                                    {/if}
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div><!-- #sandbox -->
+
+                                <div class="tab-pane" id="sample{$api.id}">
+                                    <div class="row">
+                                        <div class="col-md-12">
+                                            <pre id="sample_response{$api.id}">{$api.return|default='无'}</pre>
+                                        </div>
+                                    </div>
+                                </div><!-- #sample -->
+
+                            </div><!-- .tab-content -->
+                        </div>
+                    </div>
+                </div>
+                {/foreach}
+                {/foreach}
+            </div>
+
+            <hr>
+
+            <div class="row mt0 footer">
+                <div class="col-md-6" align="left">
+                    Generated on {:date('Y-m-d H:i:s')}
+                </div>
+                <div class="col-md-6" align="right">
+                    <a href="http://www.fastadmin.net" target="_blank">FastAdmin</a>
+                </div>
+            </div>
+
+        </div> <!-- /container -->
+
+        <script src="https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js"></script>
+        <script src="https://cdn.bootcss.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
+        <script type="text/javascript">
+            function syntaxHighlight(json) {
+                if (typeof json != 'string') {
+                    json = JSON.stringify(json, undefined, 2);
+                }
+                json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+                return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
+                    var cls = 'number';
+                    if (/^"/.test(match)) {
+                        if (/:$/.test(match)) {
+                            cls = 'key';
+                        } else {
+                            cls = 'string';
+                        }
+                    } else if (/true|false/.test(match)) {
+                        cls = 'boolean';
+                    } else if (/null/.test(match)) {
+                        cls = 'null';
+                    }
+                    return '<span class="' + cls + '">' + match + '</span>';
+                });
+            }
+
+            function prepareStr(str) {
+                try {
+                    return syntaxHighlight(JSON.stringify(JSON.parse(str.replace(/'/g, '"')), null, 2));
+                } catch (e) {
+                    return str;
+                }
+            }
+            var storage = (function () {
+                var uid = new Date;
+                var storage;
+                var result;
+                try {
+                    (storage = window.localStorage).setItem(uid, uid);
+                    result = storage.getItem(uid) == uid;
+                    storage.removeItem(uid);
+                    return result && storage;
+                } catch (exception) {
+                }
+            }());
+
+            $.fn.serializeObject = function ()
+            {
+                var o = {};
+                var a = this.serializeArray();
+                $.each(a, function () {
+                    if (!this.value) {
+                        return;
+                    }
+                    if (o[this.name] !== undefined) {
+                        if (!o[this.name].push) {
+                            o[this.name] = [o[this.name]];
+                        }
+                        o[this.name].push(this.value || '');
+                    } else {
+                        o[this.name] = this.value || '';
+                    }
+                });
+                return o;
+            };
+
+            $(document).ready(function () {
+
+                if (storage) {
+                    $('#token').val(storage.getItem('token'));
+                    $('#apiUrl').val(storage.getItem('apiUrl'));
+                }
+
+                $('[data-toggle="tooltip"]').tooltip({
+                    placement: 'bottom'
+                });
+
+                $('code[id^=response]').hide();
+
+                $.each($('pre[id^=sample_response],pre[id^=sample_post_body]'), function () {
+                    if ($(this).html() == 'NA') {
+                        return;
+                    }
+                    var str = prepareStr($(this).html());
+                    $(this).html(str);
+                });
+
+                $("[data-toggle=popover]").popover({placement: 'right'});
+
+                $('[data-toggle=popover]').on('shown.bs.popover', function () {
+                    var $sample = $(this).parent().find(".popover-content"),
+                            str = $(this).data('content');
+                    if (typeof str == "undefined" || str === "") {
+                        return;
+                    }
+                    var str = prepareStr(str);
+                    $sample.html('<pre>' + str + '</pre>');
+                });
+
+                $('body').on('click', '#save_data', function (e) {
+                    if (storage) {
+                        storage.setItem('token', $('#token').val());
+                        storage.setItem('apiUrl', $('#apiUrl').val());
+                    } else {
+                        alert('Your browser does not support local storage');
+                    }
+                });
+
+                $('body').on('click', '.send', function (e) {
+                    e.preventDefault();
+                    var form = $(this).closest('form');
+                    //added /g to get all the matched params instead of only first
+                    var matchedParamsInRoute = $(form).attr('action').match(/[^{]+(?=\})/g);
+                    var theId = $(this).attr('rel');
+                    //keep a copy of action attribute in order to modify the copy
+                    //instead of the initial attribute
+                    var url = $(form).attr('action');
+
+                    var serializedData = new FormData();
+
+                    $(form).find('input').each(function (i, input) {
+                        if ($(input).attr('type') == 'file') {
+                            serializedData.append($(input).attr('name'), $(input)[0].files[0]);
+                        } else {
+                            serializedData.append($(input).attr('name'), $(input).val())
+                        }
+                    });
+
+                    var index, key, value;
+
+                    if (matchedParamsInRoute) {
+                        for (index = 0; index < matchedParamsInRoute.length; ++index) {
+                            try {
+                                key = matchedParamsInRoute[index];
+                                value = serializedData[key];
+                                if (typeof value == "undefined")
+                                    value = "";
+                                url = url.replace("{" + key + "}", value);
+                                delete serializedData[key];
+                            } catch (err) {
+                                console.log(err);
+                            }
+                        }
+                    }
+
+                    var headers = {};
+
+                    var token = $('#token').val();
+                    if (token.length > 0) {
+                        headers[token] = token;
+                    }
+
+                    $("#sandbox" + theId + " .headers input[type=text]").each(function () {
+                        val = $(this).val();
+                        if (val.length > 0) {
+                            headers[$(this).prop('name')] = val;
+                        }
+                    });
+
+                    $.ajax({
+                        url: $('#apiUrl').val() + url,
+                        data: $(form).attr('method') == 'get' ? $(form).serialize() : serializedData,
+                        type: $(form).attr('method') + '',
+                        dataType: 'json',
+                        contentType: false,
+                        processData: false,
+                        headers: headers,
+                        success: function (data, textStatus, xhr) {
+                            if (typeof data === 'object') {
+                                var str = JSON.stringify(data, null, 2);
+                                $('#response' + theId).html(syntaxHighlight(str));
+                            } else {
+                                $('#response' + theId).html(data || '');
+                            }
+                            $('#response_headers' + theId).html('HTTP ' + xhr.status + ' ' + xhr.statusText + '<br/><br/>' + xhr.getAllResponseHeaders());
+                            $('#response' + theId).show();
+                        },
+                        error: function (xhr, textStatus, error) {
+                            try {
+                                var str = JSON.stringify($.parseJSON(xhr.responseText), null, 2);
+                            } catch (e) {
+                                var str = xhr.responseText;
+                            }
+                            $('#response_headers' + theId).html('HTTP ' + xhr.status + ' ' + xhr.statusText + '<br/><br/>' + xhr.getAllResponseHeaders());
+                            $('#response' + theId).html(syntaxHighlight(str));
+                            $('#response' + theId).show();
+                        }
+                    });
+                    return false;
+                });
+            });
+        </script>
+    </body>
+</html>

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

@@ -580,8 +580,8 @@ class Crud extends Command
                         }
                         $formAddElement = $formEditElement = Form::hidden($fieldName, $no, array_merge(['checked' => ''], $attrArr));
                         $attrArr['id'] = $fieldName . "-switch";
-                        $formAddElement .= sprintf(Form::label("{$attrArr['id']}", "%s abcdefg"), Form::checkbox($fieldName, $yes, $defaultValue === $yes, $attrArr));
-                        $formEditElement .= sprintf(Form::label("{$attrArr['id']}", "%s abcdefg"), Form::checkbox($fieldName, $yes, 0, $attrArr));
+                        $formAddElement .= sprintf(Form::label("{$attrArr['id']}", "%s {:__('Yes')}", ['class'=>'control-label']), Form::checkbox($fieldName, $yes, $defaultValue === $yes, $attrArr));
+                        $formEditElement .= sprintf(Form::label("{$attrArr['id']}", "%s {:__('Yes')}", ['class'=>'control-label']), Form::checkbox($fieldName, $yes, 0, $attrArr));
                         $formEditElement = str_replace('type="checkbox"', 'type="checkbox" {in name="' . "\$row.{$field}" . '" value="' . $yes . '"}checked{/in}', $formEditElement);
                     }
                     else if ($inputType == 'citypicker')
@@ -963,6 +963,7 @@ EOD;
         if ($content || !Lang::has($field))
         {
             $itemArr = [];
+            $content = str_replace(',', ',', $content);
             if (stripos($content, ':') !== false && stripos($content, ',') && stripos($content, '=') !== false)
             {
                 list($fieldLang, $item) = explode(':', $content);
@@ -997,6 +998,7 @@ EOD;
     /**
      * 读取数据和语言数组列表
      * @param array $arr
+     * @param boolean $withTpl
      * @return array
      */
     protected function getLangArray($arr, $withTpl = TRUE)
@@ -1035,6 +1037,7 @@ EOD;
     protected function getItemArray($item, $field, $comment)
     {
         $itemArr = [];
+        $comment = str_replace(',', ',', $comment);
         if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false)
         {
             list($fieldLang, $item) = explode(':', $comment);
@@ -1255,7 +1258,7 @@ EOD;
         {
             $html .= ", operate:'RANGE', addclass:'datetimerange'";
         }
-        else if (in_array($datatype,['float', 'double', 'decimal']))
+        else if (in_array($datatype, ['float', 'double', 'decimal']))
         {
             $html .= ", operate:'BETWEEN'";
         }

+ 2 - 0
application/admin/command/Install.php

@@ -80,6 +80,8 @@ class Install extends Command
         $config = preg_replace_callback("/'(hostname|database|username|password|hostport|prefix)'(\s+)=>(\s+)Env::get\((.*)\)\,/", $callback, $config);
         // 写入数据库配置
         file_put_contents($dbConfigFile, $config);
+        
+        \think\Cache::rm('__menu__');
 
         $output->info("Install Successed!");
     }

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

@@ -4,7 +4,7 @@
  官网: http://www.fastadmin.net
  演示: http://demo.fastadmin.net
 
- Date: 2017年09月15
+ Date: 2018年03月07
 */
 
 SET FOREIGN_KEY_CHECKS = 0;
@@ -396,6 +396,9 @@ 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', '关键字', '描述', '广西壮族自治区/百色市/平果县', 0.00, 0, '2017-07-10', '2017-07-10 18:24:45', 2017, '18:24:45', 1499682285, 1499682526, 1499682526, 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',
@@ -431,10 +434,16 @@ CREATE TABLE `fa_user` (
   KEY `mobile` (`mobile`)
 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='会员表';
 
+-- ----------------------------
+-- Records of fa_user
+-- ----------------------------
 BEGIN;
 INSERT INTO `fa_user` VALUES (1, 1, 'admin', 'admin', 'c13f62012fd6a8fdf06b3452a94430e5', 'rpR6Bv', 'admin@163.com', '13888888888', '/assets/img/avatar.png', 0, 0, '2017-04-15', '', 0, 1, 1, 1516170492, 1516171614, '127.0.0.1', 0, '127.0.0.1', 1491461418, 0, 1516171614, '', '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,
@@ -446,10 +455,16 @@ CREATE TABLE `fa_user_group` (
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 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', 1515386468, 1516168298, 'normal');
 COMMIT;
 
+-- ----------------------------
+-- 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,
@@ -465,6 +480,9 @@ CREATE TABLE `fa_user_rule` (
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 COMMENT='会员规则表';
 
+-- ----------------------------
+-- Records of fa_user_rule
+-- ----------------------------
 BEGIN;
 INSERT INTO `fa_user_rule` VALUES (1, 0, 'index', '前台', '', 1, 1516168079, 1516168079, 1, 'normal');
 INSERT INTO `fa_user_rule` VALUES (2, 0, 'api', 'API接口', '', 1, 1516168062, 1516168062, 2, 'normal');
@@ -480,6 +498,9 @@ INSERT INTO `fa_user_rule` VALUES (11, 4, 'api/user/index', '会员中心', '',
 INSERT INTO `fa_user_rule` VALUES (12, 4, 'api/user/profile', '个人资料', '', 0, 1516015012, 1516015012, 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,
@@ -492,6 +513,9 @@ CREATE TABLE `fa_user_score_log` (
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 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',
@@ -501,4 +525,30 @@ CREATE TABLE `fa_user_token` (
   PRIMARY KEY (`token`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 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) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '旧版本号',
+  `newversion` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '新版本号',
+  `packagesize` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '包大小',
+  `content` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '升级内容',
+  `downloadurl` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '下载地址',
+  `enforce` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '强制更新',
+  `createtime` int(10) NOT NULL DEFAULT 0 COMMENT '创建时间',
+  `updatetime` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间',
+  `weigh` int(10) NOT NULL DEFAULT 0 COMMENT '权重',
+  `status` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '版本表' ROW_FORMAT = Compact;
+
+-- ----------------------------
+-- Table structure for fa_version
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_version` (`id`, `oldversion`, `newversion`, `packagesize`, `content`, `downloadurl`, `enforce`, `createtime`, `updatetime`, `weigh`, `status`) VALUES
+(1, '1.1.1,2', '1.2.1', '20M', '更新内容', 'http://www.fastadmin.net/download.html', 1, 1520425318, 0, 0, 'normal');
+COMMIT;
 SET FOREIGN_KEY_CHECKS = 1;

+ 0 - 1
application/admin/command/Menu.php

@@ -56,7 +56,6 @@ class Menu extends Command
             {
                 throw new Exception("There is no menu to delete");
             }
-            $readyMenu = [];
             $output->info("Are you sure you want to delete all those menu?  Type 'yes' to continue: ");
             $line = fgets(STDIN);
             if (trim($line) != 'yes')

+ 13 - 2
application/admin/command/Min.php

@@ -27,6 +27,7 @@ class Min extends Command
                 ->setName('min')
                 ->addOption('module', 'm', Option::VALUE_REQUIRED, 'module name(frontend or backend),use \'all\' when build all modules', null)
                 ->addOption('resource', 'r', Option::VALUE_REQUIRED, 'resource name(js or css),use \'all\' when build all resources', null)
+                ->addOption('optimize', 'o', Option::VALUE_OPTIONAL, 'optimize type(uglify|closure|none)', 'none')
                 ->setDescription('Compress js and css file');
     }
 
@@ -34,6 +35,7 @@ class Min extends Command
     {
         $module = $input->getOption('module') ?: '';
         $resource = $input->getOption('resource') ?: '';
+        $optimize = $input->getOption('optimize') ?: 'none';
 
         if (!$module || !in_array($module, ['frontend', 'backend', 'all']))
         {
@@ -89,6 +91,7 @@ class Min extends Command
                     'cssBaseUrl'  => $this->options['cssBaseUrl'],
                     'jsBasePath'  => str_replace(DS, '/', ROOT_PATH . $this->options['jsBaseUrl']),
                     'cssBasePath' => str_replace(DS, '/', ROOT_PATH . $this->options['cssBaseUrl']),
+                    'optimize'    => $optimize,
                     'ds'          => DS,
                 ];
 
@@ -117,11 +120,19 @@ class Min extends Command
                 $output->info("Compress " . $data["{$res}BaseName"] . ".{$res}");
 
                 // 执行压缩
-                echo exec("{$nodeExec} \"{$minPath}r.js\" -o \"{$tempFile}\" >> \"{$minPath}node.log\"");
+                $command = "{$nodeExec} \"{$minPath}r.js\" -o \"{$tempFile}\" >> \"{$minPath}node.log\"";
+                if ($output->isDebug())
+                {
+                    $output->warning($command);
+                }
+                echo exec($command);
             }
         }
 
-        @unlink($tempFile);
+        if (!$output->isDebug())
+        {
+            @unlink($tempFile);
+        }
 
         $output->info("Build Successed!");
     }

+ 2 - 1
application/admin/command/Min/stubs/css.stub

@@ -1,5 +1,6 @@
 ({
   cssIn: "{%cssBasePath%}{%cssBaseName%}.css",
   out: "{%cssBasePath%}{%cssBaseName%}.min.css",
-  optimizeCss: "default"
+  optimizeCss: "default",
+  optimize: "{%optimize%}"
 })

+ 2 - 1
application/admin/command/Min/stubs/js.stub

@@ -2,7 +2,8 @@
     {%config%}
     ,
     optimizeCss: "standard",
-    optimize: "none",   //可使用uglify|closure|none
+    optimize: "{%optimize%}",   //可使用uglify|closure|none
+    preserveLicenseComments: false,
     removeCombined: false,
     baseUrl: "{%jsBasePath%}",    //JS文件所在的基础目录
     name: "{%jsBaseName%}", //来源文件,不包含后缀

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

@@ -5,6 +5,7 @@ namespace app\admin\controller;
 use app\common\controller\Backend;
 use think\addons\AddonException;
 use think\addons\Service;
+use think\Cache;
 use think\Config;
 use think\Exception;
 
@@ -190,6 +191,7 @@ class Addon extends Backend
             $action = $action == 'enable' ? $action : 'disable';
             //调用启用、禁用的方法
             Service::$action($name, $force);
+            Cache::rm('__menu__');
             $this->success(__('Operate successful'));
         }
         catch (AddonException $e)
@@ -314,6 +316,7 @@ class Addon extends Backend
             ];
             //调用更新的方法
             Service::upgrade($name, $extend);
+            Cache::rm('__menu__');
             $this->success(__('Operate successful'));
         }
         catch (AddonException $e)
@@ -370,7 +373,10 @@ class Addon extends Backend
             $list[] = $v;
         }
         $total = count($list);
-        $list = array_slice($list, $offset, $limit);
+        if ($limit)
+        {
+            $list = array_slice($list, $offset, $limit);
+        }
         $result = array("total" => $total, "rows" => $list);
 
         $callback = $this->request->get('callback') ? "jsonp" : "json";

+ 10 - 2
application/admin/controller/Index.php

@@ -29,13 +29,21 @@ class Index extends Backend
      */
     public function index()
     {
-        //
+        //左侧菜单
         $menulist = $this->auth->getSidebar([
             'dashboard' => 'hot',
             'addon'     => ['new', 'red', 'badge'],
-            'auth/rule' => 'side',
+            '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]);
+            }
+        }
         $this->view->assign('menulist', $menulist);
         $this->view->assign('title', __('Home'));
         return $this->view->fetch();

+ 1 - 1
application/admin/lang/zh-cn/auth/rule.php

@@ -13,6 +13,6 @@ return [
     'Menu tips'                                                 => '规则任意,不可重复,仅做层级显示,无需匹配控制器和方法',
     'Node tips'                                                 => '控制器/方法名',
     'The non-menu rule must have parent'                        => '非菜单规则节点必须有父级',
-    'If not necessary, use the command line to build rule'      => '非必要情况下请直接使用命令行php think menu来生成',
+    'If not necessary, use the command line to build rule'      => '非必要情况下请直接使用命令行<a href="http://doc.fastadmin.net/docs/command.html#一键生成菜单" target="_blank">php think menu</a>来生成',
     'Name only supports letters, numbers, underscore and slash' => 'URL规则只能是小写字母、数字、下划线和/组成',
 ];

+ 2 - 0
application/admin/lang/zh-cn/user/user.php

@@ -12,6 +12,8 @@ return [
     'Avatar'         => '头像',
     'Level'          => '等级',
     'Gender'         => '性别',
+    'Male'           => '男',
+    'FeMale'         => '女',
     'Birthday'       => '生日',
     'Bio'            => '格言',
     'Score'          => '积分',

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

@@ -273,7 +273,7 @@ class Auth extends \fast\Auth
             $groupIds[] = $v['id'];
         }
         // 取出所有分组
-        $groupList = model('AuthGroup')->all(['status' => 'normal']);
+        $groupList = \app\admin\model\AuthGroup::where(['status' => 'normal'])->select();
         $objList = [];
         foreach ($groups as $K => $v)
         {
@@ -310,8 +310,8 @@ class Auth extends \fast\Auth
         if (!$this->isSuperAdmin())
         {
             $groupIds = $this->getChildrenGroupIds(false);
-            $authGroupList = model('AuthGroupAccess')
-                    ->field('uid,group_id')
+            $authGroupList = \app\admin\model\AuthGroupAccess::
+                    field('uid,group_id')
                     ->where('group_id', 'in', $groupIds)
                     ->select();
 
@@ -407,7 +407,7 @@ class Auth extends \fast\Auth
         $select_id = 0;
         $pinyin = new \Overtrue\Pinyin\Pinyin('Overtrue\Pinyin\MemoryFileDictLoader');
         // 必须将结果集转换为数组
-        $ruleList = collection(model('AuthRule')->where('status', 'normal')->where('ismenu', 1)->order('weigh', 'desc')->cache("__menu__")->select())->toArray();
+        $ruleList = collection(\app\admin\model\AuthRule::where('status', 'normal')->where('ismenu', 1)->order('weigh', 'desc')->cache("__menu__")->select())->toArray();
         foreach ($ruleList as $k => &$v)
         {
             if (!in_array($v['name'], $userRule))

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

@@ -267,7 +267,8 @@ trait Backend
                     {
                         $this->model->where($this->dataLimitField, 'in', $adminIds);
                     }
-                    $count = $this->model->where($this->model->getPk(), 'in', $ids)->update($values);
+                    $this->model->where($this->model->getPk(), 'in', $ids);
+                    $count = $this->model->allowField(true)->isUpdate(true)->save($values);
                     if ($count)
                     {
                         $this->success();

+ 9 - 0
application/admin/model/AuthRule.php

@@ -2,6 +2,7 @@
 
 namespace app\admin\model;
 
+use think\Cache;
 use think\Model;
 
 class AuthRule extends Model
@@ -13,8 +14,16 @@ class AuthRule extends Model
     protected $createTime = 'createtime';
     protected $updateTime = 'updatetime';
 
+    protected static function init()
+    {
+        self::afterWrite(function ($row) {
+            Cache::rm('__menu__');
+        });
+    }
+
     public function getTitleAttr($value, $data)
     {
         return __($value);
     }
+
 }

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

@@ -10,6 +10,12 @@
     .payimg .alipaycode {position:absolute;left:265px;top:442px;}
     .payimg .wechatcode {position:absolute;left:660px;top:442px;}
     .thumbnail img{width:100%;}
+    .fixed-table-toolbar .pull-right.search {
+        min-width: 300px;
+    }
+    .status-disabled .noimage {
+        background:#d2d6de;
+    }
 </style>
 <div id="warmtips" class="alert alert-dismissable alert-danger hide">
     <button type="button" class="close" data-dismiss="alert">×</button>
@@ -158,10 +164,10 @@
     </table>
 </script>
 <script id="itemtpl" type="text/html">
-    <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 mt-4">
-        <% var labelarr = ['primary', 'success', 'info', 'danger', 'warning']; %>
-        <% var label = labelarr[item.id % 5]; %>
-        <% var addon = typeof addons[item.name]!= 'undefined' ? addons[item.name] : null; %>
+    <% var labelarr = ['primary', 'success', 'info', 'danger', 'warning']; %>
+    <% var label = labelarr[item.id % 5]; %>
+    <% var addon = typeof addons[item.name]!= 'undefined' ? addons[item.name] : null; %>
+    <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 mt-4 status-<%=addon ? (addon.state==1?'enabled':'disabled') : 'uninstalled'%>">
         <div class="thumbnail addon">
             <%if(addon){%>
             <span>

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

@@ -31,7 +31,7 @@
         <label for="icon" class="control-label col-xs-12 col-sm-2">{:__('Icon')}:</label>
         <div class="col-xs-12 col-sm-8">
             <div class="input-group input-groupp-md">
-                <input type="text" class="form-control" id="icon" name="row[icon]" value="fa fa-dot" />
+                <input type="text" class="form-control" id="icon" name="row[icon]" value="fa fa-circle-o" />
                 <a href="javascript:;" class="btn-search-icon input-group-addon">{:__('Search icon')}</a>
             </div>
         </div>

+ 14 - 1
application/admin/view/auth/rule/index.html

@@ -1,3 +1,6 @@
+<style>
+    .bootstrap-table tr td .text-muted {color:#888;}
+</style>
 <div class="panel panel-default panel-intro">
     {:build_heading()}
 
@@ -6,7 +9,17 @@
             <div class="tab-pane fade active in" id="one">
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
-                        {:build_toolbar()}
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
+                        <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('auth/rule/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
+                        <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('auth/rule/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
+                        <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('auth/rule/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
+                        <div class="dropdown btn-group {:$auth->check('auth/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>
                         <a href="javascript:;" class="btn btn-danger btn-toggle-all"><i class="fa fa-plus"></i> {:__('Toggle all')}</a>
                     </div>
                     <table id="table" class="table table-bordered table-hover" 

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

@@ -1,6 +1,6 @@
 <div class="panel panel-default panel-intro">
     <div class="panel-heading">
-        {:build_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"}

Різницю між файлами не показано, бо вона завелика
+ 4 - 136
application/admin/view/common/control.html


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

@@ -1,5 +1,5 @@
 <!-- Logo -->
-<a href="javascript:;" class="logo">
+<a href="javascript:;" class="logo hidden-xs">
     <!-- 迷你模式下Logo的大小为50X50 -->
     <span class="logo-mini">{$site.name|mb_substr=0,4,'utf-8'|mb_strtoupper='utf-8'}</span>
     <!-- 普通模式下Logo -->

+ 4 - 4
application/admin/view/common/menu.html

@@ -29,10 +29,10 @@
     <!--如果想始终显示子菜单,则给ul加上show-submenu类即可-->
     <ul class="sidebar-menu">
         {$menulist}
-        <li class="header">{:__('Links')}</li>
-        <li><a href="http://doc.fastadmin.net" target="_blank"><i class="fa fa-list text-red"></i> <span>{:__('Docs')}</span></a></li>
-        <li><a href="http://forum.fastadmin.net" target="_blank"><i class="fa fa-comment text-yellow"></i> <span>{:__('Forum')}</span></a></li>
-        <li><a href="https://jq.qq.com/?_wv=1027&k=487PNBb" target="_blank"><i class="fa fa-qq text-aqua"></i> <span>{:__('QQ qun')}</span></a></li>
+        <li class="header" data-rel="external">{:__('Links')}</li>
+        <li data-rel="external"><a href="http://doc.fastadmin.net" target="_blank"><i class="fa fa-list text-red"></i> <span>{:__('Docs')}</span></a></li>
+        <li data-rel="external"><a href="http://forum.fastadmin.net" target="_blank"><i class="fa fa-comment text-yellow"></i> <span>{:__('Forum')}</span></a></li>
+        <li data-rel="external"><a href="https://jq.qq.com/?_wv=1027&k=487PNBb" target="_blank"><i class="fa fa-qq text-aqua"></i> <span>{:__('QQ qun')}</span></a></li>
     </ul>
 </section>
 <!-- /.sidebar -->

+ 1 - 1
application/admin/view/common/script.html

@@ -1 +1 @@
-<script src="__CDN__/assets/js/require.js" data-main="__CDN__/assets/js/require-backend{$Think.config.app_debug?'':'.min'}.js?v={$site.version}"></script>
+<script src="__CDN__/assets/js/require{$Think.config.app_debug?'':'.min'}.js" data-main="__CDN__/assets/js/require-backend{$Think.config.app_debug?'':'.min'}.js?v={$site.version}"></script>

+ 1 - 1
application/admin/view/user/rule/index.html

@@ -6,7 +6,7 @@
             <div class="tab-pane fade active in" id="one">
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
-                        {:build_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">

+ 2 - 1
application/api/config.php

@@ -1,5 +1,6 @@
 <?php
+
 //配置文件
 return [
-
+    'exception_handle'        => '\\app\\api\\library\\ExceptionHandle',
 ];

+ 2 - 2
application/api/controller/Common.php

@@ -2,9 +2,9 @@
 
 namespace app\api\controller;
 
-use app\api\model\Area;
 use app\common\controller\Api;
-use fast\Version;
+use app\common\model\Area;
+use app\common\model\Version;
 use fast\Random;
 use think\Config;
 

+ 40 - 0
application/api/library/ExceptionHandle.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace app\api\library;
+
+use Exception;
+use think\exception\Handle;
+
+/**
+ * 自定义API模块的错误显示
+ */
+class ExceptionHandle extends Handle
+{
+
+    public function render(Exception $e)
+    {
+        // 在生产环境下返回code信息
+        if (!\think\Config::get('app_debug'))
+        {
+            $statuscode = $code = 500;
+            $msg = 'An error occurred';
+            // 验证异常
+            if ($e instanceof \think\exception\ValidateException)
+            {
+                $code = 0;
+                $statuscode = 200;
+                $msg = $e->getError();
+            }
+            // Http异常
+            if ($e instanceof \think\exception\HttpException)
+            {
+                $statuscode = $code = $e->getStatusCode();
+            }
+            return json(['code' => $code, 'msg' => $msg, 'time' => time(), 'data' => null], $statuscode);
+        }
+
+        //其它此交由系统处理
+        return parent::render($e);
+    }
+
+}

+ 1 - 0
application/command.php

@@ -16,4 +16,5 @@ return [
     'app\admin\command\Install',
     'app\admin\command\Min',
     'app\admin\command\Addon',
+    'app\admin\command\Api',
 ];

+ 36 - 3
application/common.php

@@ -22,7 +22,7 @@ if (!function_exists('__'))
             array_shift($vars);
             $lang = '';
         }
-        return think\Lang::get($name, $vars, $lang);
+        return \think\Lang::get($name, $vars, $lang);
     }
 
 }
@@ -89,7 +89,7 @@ if (!function_exists('cdnurl'))
      */
     function cdnurl($url)
     {
-        return preg_match("/^https?:\/\/(.*)/i", $url) ? $url : think\Config::get('upload.cdnurl') . $url;
+        return preg_match("/^https?:\/\/(.*)/i", $url) ? $url : \think\Config::get('upload.cdnurl') . $url;
     }
 
 }
@@ -208,7 +208,6 @@ if (!function_exists('mb_ucfirst'))
 
 }
 
-
 if (!function_exists('addtion'))
 {
 
@@ -300,3 +299,37 @@ if (!function_exists('addtion'))
     }
 
 }
+
+if (!function_exists('var_export_short'))
+{
+
+    /**
+     * 返回打印数组结构
+     * @param string $var   数组
+     * @param string $indent 缩进字符
+     * @return string
+     */
+    function var_export_short($var, $indent = "")
+    {
+        switch (gettype($var))
+        {
+            case "string":
+                return '"' . addcslashes($var, "\\\$\"\r\n\t\v\f") . '"';
+            case "array":
+                $indexed = array_keys($var) === range(0, count($var) - 1);
+                $r = [];
+                foreach ($var as $key => $value)
+                {
+                    $r[] = "$indent    "
+                            . ($indexed ? "" : var_export_short($key) . " => ")
+                            . var_export_short($value, "$indent    ");
+                }
+                return "[\n" . implode(",\n", $r) . "\n" . $indent . "]";
+            case "boolean":
+                return $var ? "TRUE" : "FALSE";
+            default:
+                return var_export($var, TRUE);
+        }
+    }
+
+}

+ 5 - 0
application/common/behavior/Common.php

@@ -52,6 +52,11 @@ class Common
         {
             Config::set('app_trace', false);
         }
+        // 切换多语言
+        if (Config::get('lang_switch_on') && $request->get('lang'))
+        {
+            \think\Cookie::set('think_var', $request->get('lang'));
+        }
     }
 
     public function addonBegin(&$request)

+ 21 - 18
application/common/controller/Api.php

@@ -91,7 +91,7 @@ class Api
         $actionname = strtolower($this->request->action());
 
         // token
-        $token = $this->request->request('token') ?: $this->request->cookie('token');
+        $token = $this->request->server('HTTP_TOKEN', $this->request->request('token', \think\Cookie::get('token')));
 
         $path = str_replace('.', '/', $controllername) . '/' . $actionname;
         // 设置当前请求的URI
@@ -104,7 +104,7 @@ class Api
             //检测是否登录
             if (!$this->auth->isLogin())
             {
-                $this->error(__('Please login first'));
+                $this->error(__('Please login first'), null, 401);
             }
             // 判断是否需要验证权限
             if (!$this->auth->match($this->noNeedRight))
@@ -112,7 +112,7 @@ class Api
                 // 判断控制器和方法判断是否有对应权限
                 if (!$this->auth->check($path))
                 {
-                    $this->error(__('You have no permission'));
+                    $this->error(__('You have no permission'), null, 403);
                 }
             }
         }
@@ -141,38 +141,40 @@ class Api
      * 操作成功返回的数据
      * @param string $msg   提示信息
      * @param mixed $data   要返回的数据
+     * @param int   $code   错误码,默认为1
      * @param string $type  输出类型
      * @param array $header 发送的 Header 信息
      */
-    protected function success($msg = '', $data = '', $type = 'json', array $header = [])
+    protected function success($msg = '', $data = null, $code = 1, $type = 'json', array $header = [])
     {
-        $this->result($data, 1, $msg, $type, $header);
+        $this->result($msg, $data, $code, $type, $header);
     }
 
     /**
      * 操作失败返回的数据
      * @param string $msg   提示信息
      * @param mixed $data   要返回的数据
+     * @param int   $code   错误码,默认为0
      * @param string $type  输出类型
      * @param array $header 发送的 Header 信息
      */
-    protected function error($msg = '', $data = '', $type = 'json', array $header = [])
+    protected function error($msg = '', $data = null, $code = 0, $type = 'json', array $header = [])
     {
-        $this->result($data, 0, $msg, $type, $header);
+        $this->result($msg, $data, $code, $type, $header);
     }
 
     /**
      * 返回封装后的 API 数据到客户端
      * @access protected
+     * @param mixed  $msg    提示信息
      * @param mixed  $data   要返回的数据
      * @param int    $code   返回的 code
-     * @param mixed  $msg    提示信息
      * @param string $type   返回数据格式
      * @param array  $header 发送的 Header 信息
      * @return void
      * @throws HttpResponseException
      */
-    protected function result($data, $code = 0, $msg = '', $type = '', array $header = [])
+    protected function result($msg, $data = null, $code = 0, $type = 'json', array $header = [])
     {
         $result = [
             'code' => $code,
@@ -181,20 +183,21 @@ class Api
             'data' => $data,
         ];
         $type = $type ?: $this->getResponseType();
-        $response = Response::create($result, $type)->header($header);
+        if (isset($header['statuscode']))
+        {
+            $code = $header['statuscode'];
+            unset($header['statuscode']);
+        }
+        else
+        {
+            $code = $code >= 1000 ? 200 : $code;
+        }
+        $response = Response::create($result, $type, $code)->header($header);
 
         throw new HttpResponseException($response);
     }
 
     /**
-     * 未找到请求的接口
-     */
-    public function _empty()
-    {
-        return $this->error('Api not found');
-    }
-
-    /**
      * 前置操作
      * @access protected
      * @param  string $method  前置操作方法名

+ 13 - 8
application/common/controller/Backend.php

@@ -144,6 +144,7 @@ class Backend extends Controller
             $url = preg_replace_callback("/([\?|&]+)ref=addtabs(&?)/i", function($matches) {
                 return $matches[2] == '&' ? $matches[1] : '';
             }, $this->request->url());
+            $url = url($url, '', false);
             $this->redirect('index/index', [], 302, ['referer' => $url]);
             exit;
         }
@@ -290,6 +291,10 @@ class Backend extends Controller
                 case '<=':
                     $where[] = [$k, $sym, intval($v)];
                     break;
+                case 'FINDIN':
+                case 'FIND_IN_SET':
+                    $where[] = "FIND_IN_SET('{$v}', `{$k}`)";
+                    break;
                 case 'IN':
                 case 'IN(...)':
                 case 'NOT IN':
@@ -401,21 +406,21 @@ class Backend extends Controller
         //搜索关键词,客户端输入以空格分开,这里接收为数组
         $word = (array) $this->request->request("q_word/a");
         //当前页
-        $page = $this->request->request("page");
+        $page = $this->request->request("pageNumber");
         //分页大小
-        $pagesize = $this->request->request("per_page");
+        $pagesize = $this->request->request("pageSize");
         //搜索条件
-        $andor = $this->request->request("and_or");
+        $andor = $this->request->request("andOr");
         //排序方式
-        $orderby = (array) $this->request->request("order_by/a");
+        $orderby = (array) $this->request->request("orderBy/a");
         //显示的字段
-        $field = $this->request->request("field");
+        $field = $this->request->request("showField");
         //主键
-        $primarykey = $this->request->request("pkey_name");
+        $primarykey = $this->request->request("keyField");
         //主键值
-        $primaryvalue = $this->request->request("pkey_value");
+        $primaryvalue = $this->request->request("keyValue");
         //搜索字段
-        $searchfield = (array) $this->request->request("search_field/a");
+        $searchfield = (array) $this->request->request("searchField/a");
         //自定义搜索条件
         $custom = (array) $this->request->request("custom/a");
         $order = [];

+ 1 - 2
application/common/controller/Frontend.php

@@ -58,8 +58,7 @@ class Frontend extends Controller
         $actionname = strtolower($this->request->action());
 
         // token
-        $token = $this->request->request('token');
-        $token = $token ? $token : \think\Cookie::get('token');
+        $token = $this->request->server('HTTP_TOKEN', $this->request->request('token', \think\Cookie::get('token')));
 
         $path = str_replace('.', '/', $controllername) . '/' . $actionname;
         // 设置当前请求的URI

+ 98 - 5
application/common/lang/zh-cn/addon.php

@@ -1,9 +1,102 @@
 <?php
 
 return [
-    'addon %s not found'            => '插件未找到',
-    'addon %s is disabled'          => '插件已禁用',
-    'addon controller %s not found' => '插件控制器未找到',
-    'addon action %s not found'     => '插件控制器方法未找到',
-    'addon can not be empty'        => '插件不能为空',
+    'addon %s not found'                          => '插件未找到',
+    'addon %s is disabled'                        => '插件已禁用',
+    'addon controller %s not found'               => '插件控制器未找到',
+    'addon action %s not found'                   => '插件控制器方法未找到',
+    'addon can not be empty'                      => '插件不能为空',
+    'Keep login'                                  => '保持会话',
+    'Forgot password'                             => '忘记密码?',
+    'Sign in'                                     => '登入',
+    'Username'                                    => '用户名',
+    'User id'                                     => '会员ID',
+    'Username'                                    => '用户名',
+    'Nickname'                                    => '昵称',
+    'Password'                                    => '密码',
+    'Sign up'                                     => '注 册',
+    'Sign in'                                     => '登 录',
+    'Sign out'                                    => '注 销',
+    'Guest'                                       => '游客',
+    'Welcome'                                     => '%s,你好!',
+    'Add'                                         => '添加',
+    'Edit'                                        => '编辑',
+    'Delete'                                      => '删除',
+    'Move'                                        => '移动',
+    'Name'                                        => '名称',
+    'Status'                                      => '状态',
+    'Weigh'                                       => '权重',
+    'Operate'                                     => '操作',
+    'Warning'                                     => '温馨提示',
+    'Default'                                     => '默认',
+    'Article'                                     => '文章',
+    'Page'                                        => '单页',
+    'OK'                                          => '确定',
+    'Cancel'                                      => '取消',
+    'Loading'                                     => '加载中',
+    'More'                                        => '更多',
+    'Normal'                                      => '正常',
+    'Hidden'                                      => '隐藏',
+    'Submit'                                      => '提交',
+    'Reset'                                       => '重置',
+    'Execute'                                     => '执行',
+    'Close'                                       => '关闭',
+    'Search'                                      => '搜索',
+    'Refresh'                                     => '刷新',
+    'First'                                       => '首页',
+    'Previous'                                    => '上一页',
+    'Next'                                        => '下一页',
+    'Last'                                        => '末页',
+    'None'                                        => '无',
+    'Home'                                        => '主页',
+    'Online'                                      => '在线',
+    'Logout'                                      => '注销',
+    'Profile'                                     => '个人资料',
+    'Index'                                       => '首页',
+    'Hot'                                         => '热门',
+    'Recommend'                                   => '推荐',
+    'Dashboard'                                   => '控制台',
+    'Code'                                        => '编号',
+    'Message'                                     => '内容',
+    'Line'                                        => '行号',
+    'File'                                        => '文件',
+    'Menu'                                        => '菜单',
+    'Name'                                        => '名称',
+    'Weigh'                                       => '权重',
+    'Type'                                        => '类型',
+    'Title'                                       => '标题',
+    'Content'                                     => '内容',
+    'Status'                                      => '状态',
+    'Operate'                                     => '操作',
+    'Append'                                      => '追加',
+    'Memo'                                        => '备注',
+    'Parent'                                      => '父级',
+    'Params'                                      => '参数',
+    'Permission'                                  => '权限',
+    'Begin time'                                  => '开始时间',
+    'End time'                                    => '结束时间',
+    'Create time'                                 => '创建时间',
+    'Flag'                                        => '标志',
+    'Home'                                        => '首页',
+    'Store'                                       => '插件市场',
+    'Services'                                    => '服务',
+    'Download'                                    => '下载',
+    'Demo'                                        => '演示',
+    'Donation'                                    => '捐赠',
+    'Forum'                                       => '社区',
+    'Docs'                                        => '文档',
+    'Please login first'                          => '请登录后再操作',
+    'Send verification code'                      => '发送验证码',
+    'Redirect now'                                => '立即跳转',
+    'Operation completed'                         => '操作成功!',
+    'Operation failed'                            => '操作失败!',
+    'Unknown data format'                         => '未知的数据格式!',
+    'Network error'                               => '网络错误!',
+    'Advanced search'                             => '高级搜索',
+    'Invalid parameters'                          => '未知参数',
+    'No results were found'                       => '记录未找到',
+    'Parameter %s can not be empty'               => '参数%s不能为空',
+    'You have no permission'                      => '你没有权限访问',
+    'An unexpected error occurred'                => '发生了一个意外错误,程序猿正在紧急处理中',
+    'This page will be re-directed in %s seconds' => '页面将在 %s 秒后自动跳转',
 ];

+ 25 - 3
application/common/library/Menu.php

@@ -67,7 +67,7 @@ class Menu
         AuthRule::destroy($ids);
         return true;
     }
-    
+
     /**
      * 启用菜单
      * @param string $name
@@ -83,7 +83,7 @@ class Menu
         AuthRule::where('id', 'in', $ids)->update(['status' => 'normal']);
         return true;
     }
-    
+
     /**
      * 禁用菜单
      * @param string $name
@@ -101,6 +101,28 @@ class Menu
     }
 
     /**
+     * 导出指定名称的菜单规则
+     * @param string $name
+     * @return array
+     */
+    public static function export($name)
+    {
+        $ids = self::getAuthRuleIdsByName($name);
+        if (!$ids)
+        {
+            return [];
+        }
+        $menuList = [];
+        $menu = AuthRule::getByName($name);
+        if ($menu)
+        {
+            $ruleList = collection(AuthRule::where('id', 'in', $ids)->select())->toArray();
+            $menuList = Tree::instance()->init($ruleList)->getTreeArray($menu['id']);
+        }
+        return $menuList;
+    }
+
+    /**
      * 根据名称获取规则IDS
      * @param string $name
      * @return array
@@ -112,7 +134,7 @@ class Menu
         if ($menu)
         {
             // 必须将结果集转换为数组
-            $ruleList = collection(model('AuthRule')->order('weigh', 'desc')->field('id,pid,name')->select())->toArray();
+            $ruleList = collection(AuthRule::order('weigh', 'desc')->field('id,pid,name')->select())->toArray();
             // 构造菜单数据
             $ids = Tree::instance()->init($ruleList)->getChildrenIds($menu['id'], true);
         }

+ 1 - 1
application/api/model/Area.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace app\api\model;
+namespace app\common\model;
 
 use think\Cache;
 use think\Model;

+ 1 - 1
application/common/model/ScoreLog.php

@@ -11,7 +11,7 @@ class ScoreLog Extends Model
 {
 
     // 表名
-    protected $name = 'score_log';
+    protected $name = 'user_score_log';
     // 开启自动写入时间戳字段
     protected $autoWriteTimestamp = 'int';
     // 定义时间戳字段名

+ 54 - 0
application/common/model/Version.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+class Version extends Model
+{
+
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 定义字段类型
+    protected $type = [
+    ];
+
+    /**
+     * 检测版本号
+     *
+     * @param string $version 客户端版本号
+     * @return array
+     */
+    public static function check($version)
+    {
+        $versionlist = self::where('status', 'normal')->cache('__version__')->order('weigh desc,id desc')->select();
+        foreach ($versionlist as $k => $v)
+        {
+            // 版本正常且新版本号不等于验证的版本号且找到匹配的旧版本
+            if ($v['status'] == 'normal' && $v['newversion'] !== $version && \fast\Version::check($version, $v['oldversion']))
+            {
+                $updateversion = $v;
+                break;
+            }
+        }
+        if (isset($updateversion))
+        {
+            $search = ['{version}', '{newversion}', '{downloadurl}', '{url}', '{packagesize}'];
+            $replace = [$version, $updateversion['newversion'], $updateversion['downloadurl'], $updateversion['downloadurl'], $updateversion['packagesize']];
+            $upgradetext = str_replace($search, $replace, $updateversion['content']);
+            return [
+                "enforce"     => $updateversion['enforce'],
+                "version"     => $version,
+                "newversion"  => $updateversion['newversion'],
+                "downloadurl" => $updateversion['downloadurl'],
+                "packagesize" => $updateversion['packagesize'],
+                "upgradetext" => $upgradetext
+            ];
+        }
+        return NULL;
+    }
+
+}

+ 1 - 1
application/config.php

@@ -253,7 +253,7 @@ return [
         //自动检测更新
         'checkupdate'      => false,
         //版本号
-        'version'          => '1.0.0.20180227_beta',
+        'version'          => '1.0.0.20180308_beta',
         'api_url'          => 'http://api.fastadmin.net',
     ],
 ];

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

@@ -97,7 +97,7 @@ return [
     'Forum'                                                  => '社区',
     'Docs'                                                   => '文档',
     'Please login first'                                     => '请登录后再操作',
-    'Send verification code'                                 => '发验证码',
+    'Send verification code'                                 => '发验证码',
     'Redirect now'                                           => '立即跳转',
     'Operation completed'                                    => '操作成功!',
     'Operation failed'                                       => '操作失败!',

+ 1 - 1
application/index/view/common/script.html

@@ -1 +1 @@
-<script src="__CDN__/assets/js/require.js" data-main="__CDN__/assets/js/require-frontend{$Think.config.app_debug?'':'.min'}.js?v={$site.version}"></script>
+<script src="__CDN__/assets/js/require{$Think.config.app_debug?'':'.min'}.js" data-main="__CDN__/assets/js/require-frontend{$Think.config.app_debug?'':'.min'}.js?v={$site.version}"></script>

+ 0 - 2
application/index/view/index/index.html

@@ -174,11 +174,9 @@
                     $("#mainNav").toggleClass("affix", $(window).height() - $(window).scrollTop() <= 50);
                 });
 
-
                 //发送版本统计信息
                 try {
                     var installed = localStorage.getItem("installed");
-                    console.log(installed);
                     if (!installed) {
                         $.ajax({
                             url: "{$Think.config.fastadmin.api_url}/statistics/installed",

+ 9 - 15
bower.json

@@ -8,37 +8,31 @@
   "dependencies": {
     "jquery": "^2.1.4",
     "bootstrap": "^3.3.7",
-    "font-awesome": "fontawesome#^4.6.1",
+    "font-awesome": "^4.6.1",
     "bootstrap-table": "^1.11.0",
-    "layer": "*",
+    "layer": "^3.0",
     "jstree": "^3.3.2",
-    "summernote": "^0.8.2",
-    "jquery-pjax": "^1.9.6",
     "moment": "^2.15.2",
     "plupload": "^2.2.0",
     "toastr": "^2.1.3",
-    "devbridge-autocomplete": "^1.2.26",
-    "jcrop": "jcrop#^2.0.4",
-    "jquery-qrcode": "*",
+    "jcrop": "^2.0.4",
     "eonasdan-bootstrap-datetimepicker": "^4.17.43",
     "bootstrap-select": "^1.11.2",
     "require-css": "^0.1.8",
     "less": "^2.7.1",
     "tableExport.jquery.plugin": "^1.9.0",
-    "jquery-slimscroll": "slimscroll#^1.3.8",
+    "jquery-slimscroll": "^1.3.8",
     "jquery.cookie": "^1.4.1",
     "Sortable": "^1.5.0",
     "nice-validator": "^1.1.1",
     "art-template": "^3.0.1",
     "requirejs-plugins": "^1.0.3",
     "bootstrap-daterangepicker": "^2.1.25",
-    "city-picker":"^1.1.0"
-  },
-  "devDependencies": {
-    "dragsort": "https://github.com/karsonzhang/dragsort.git",
-    "jquery-addtabs": "https://github.com/karsonzhang/jquery-addtabs.git",
-    "jquery-cxselect": "https://github.com/karsonzhang/cxSelect.git",
-    "selectpage": "https://github.com/karsonzhang/selectpage.git"
+    "city-picker": "^1.1.0",
+    "fastadmin-cxselect": "~1.4.0",
+    "fastadmin-dragsort": "~1.0.0",
+    "fastadmin-addtabs": "~1.0.0",
+    "fastadmin-selectpage": "~1.0.0"
   },
   "resolutions": {
     "jspdf": "1.1.239 || 1.3.2"

+ 2 - 2
composer.json

@@ -10,7 +10,7 @@
     "license": "Apache-2.0",
     "authors": [
         {
-            "name": "karson",
+            "name": "Karson",
             "email": "karsonzhang@163.com"
         }
     ],
@@ -22,7 +22,7 @@
         "topthink/think-captcha": "^1.0",
         "mtdowling/cron-expression": "^1.2",
         "phpmailer/phpmailer": "^5.2",
-        "karsonzhang/fastadmin-addons": "dev-master",
+        "karsonzhang/fastadmin-addons": "~1.1.0",
         "overtrue/pinyin": "~3.0",
         "phpoffice/phpexcel": "^1.8"
     },

Різницю між файлами не показано, бо вона завелика
+ 2878 - 0
public/api.html


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

@@ -9,7 +9,7 @@
 @import url("../libs/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css");
 @import url("../libs/bootstrap-daterangepicker/daterangepicker.css");
 @import url("../libs/nice-validator/dist/jquery.validator.css");
-@import url("../libs/selectpage/selectpage.css");
+@import url("../libs/fastadmin-selectpage/selectpage.css");
 body {
   background: #f1f4f6;
 }
@@ -44,9 +44,6 @@ body.is-dialog {
   position: absolute;
   right: 0;
 }
-.note-dialog .modal {
-  z-index: 1060;
-}
 .bootstrap-dialog .modal-dialog {
   /*width: 70%;*/
   max-width: 885px;
@@ -645,13 +642,16 @@ form.form-horizontal .control-label {
   overflow: hidden;
 }
 .layui-layer-fast .layui-layer-btn a {
-  background-color: #95a5a6!important;
-  border-color: #95a5a6!important;
+  background-color: #95a5a6;
+  border-color: #95a5a6;
   color: #fff!important;
+  height: 31px;
+  margin-top: 0;
+  border: 1px solid transparent;
 }
 .layui-layer-fast .layui-layer-btn .layui-layer-btn0 {
-  background-color: #18bc9c!important;
-  border-color: #18bc9c!important;
+  background-color: #18bc9c;
+  border-color: #18bc9c;
 }
 .layui-layer-fast .layui-layer-footer {
   padding: 8px 20px;
@@ -731,6 +731,14 @@ form.form-horizontal .control-label {
 .n-bootstrap .input-group > .n-right {
   position: absolute;
 }
+@media (min-width: 564px) {
+  body.is-dialog .daterangepicker {
+    min-width: 130px;
+  }
+  body.is-dialog .daterangepicker .ranges ul {
+    width: 130px;
+  }
+}
 /*手机版样式*/
 @media (max-width: 480px) {
   .nav-addtabs {
@@ -739,6 +747,10 @@ form.form-horizontal .control-label {
   .fixed-table-toolbar .columns-right.btn-group {
     display: none;
   }
+  .fixed .content-wrapper,
+  .fixed .right-side {
+    padding-top: 50px;
+  }
 }
 /*平板样式*/
 @media (max-width: 768px) {

Різницю між файлами не показано, бо вона завелика
+ 1 - 1
public/assets/css/backend.min.css


+ 2 - 8
public/assets/css/frontend.css

@@ -41,14 +41,6 @@ body {
   -moz-box-shadow: none;
   box-shadow: none;
 }
-.layui-layer-fast {
-  -webkit-animation-fill-mode: both;
-  animation-fill-mode: both;
-  -webkit-animation-duration: .3s;
-  animation-duration: .3s;
-  -webkit-animation-name: layer-bounceIn;
-  animation-name: layer-bounceIn;
-}
 /*修复nice-validator和summernote的编辑框冲突*/
 .nice-validator .note-editor .note-editing-area .note-editable {
   display: inherit;
@@ -317,6 +309,8 @@ footer.footer {
   color: #aaa;
   background: #555;
   margin-top: 25px;
+  position: fixed;
+  bottom: 0;
 }
 footer.footer ul {
   margin: 60px 0 30px 0;

Різницю між файлами не показано, бо вона завелика
+ 1 - 1
public/assets/css/frontend.min.css


+ 3 - 0
public/assets/js/backend-init.js

@@ -0,0 +1,3 @@
+define(['backend'], function (Backend) {
+    
+});

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

@@ -99,6 +99,9 @@ define(['fast', 'moment'], function (Fast, Moment) {
                     url = url.replace(/\{ids\}/g, ids);
                 }
                 return url;
+            },
+            refreshmenu: function () {
+                top.window.$(".sidebar-menu").trigger("refresh");
             }
         },
         init: function () {

+ 177 - 160
public/assets/js/backend/addon.js

@@ -56,6 +56,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 commonSearch: false,
                 searchFormVisible: false,
                 pageSize: 12,
+                pagination: false,
                 queryParams: function (params) {
                     var filter = params.filter ? JSON.parse(params.filter) : {};
                     var op = params.op ? JSON.parse(params.op) : {};
@@ -110,16 +111,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 $("#warmtips").removeClass("hide");
                 $(".btn-switch,.btn-userinfo").addClass("disabled");
             }
-
+            
+            // 离线安装
             require(['upload'], function (Upload) {
                 Upload.api.plupload("#plupload-addon", function (data, ret) {
                     Config['addons'][data.addon.name] = data.addon;
-                    $('.btn-refresh').trigger('click');
                     Toastr.success(ret.msg);
+                    operate(data.addon.name, 'enable', false);
                 });
             });
 
-            //查看插件首页
+            // 查看插件首页
             $(document).on("click", ".btn-addonindex", function () {
                 if ($(this).attr("href") == 'javascript:;') {
                     Layer.msg(__('Not installed tips'), {icon: 7});
@@ -128,12 +130,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     return false;
                 }
             });
-            //切换URL
+            
+            // 切换URL
             $(document).on("click", ".btn-switch", function () {
                 $(".btn-switch").removeClass("active");
                 $(this).addClass("active");
                 table.bootstrapTable('refresh', {url: $(this).data("url"), pageNumber: 1});
             });
+            
             // 会员信息
             $(document).on("click", ".btn-userinfo", function () {
                 var userinfo = Controller.api.userinfo.get();
@@ -195,46 +199,24 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 }
             });
 
-            // 点击安装
-            $(document).on("click", ".btn-install", function () {
-                var that = this;
-                var name = $(this).closest(".operate").data("name");
-                var version = $(this).data("version");
+            var install = function (name, version, force) {
                 var userinfo = Controller.api.userinfo.get();
                 var uid = userinfo ? userinfo.id : 0;
                 var token = userinfo ? userinfo.token : '';
-                var install = function (name, force) {
-                    Fast.api.ajax({
-                        url: 'addon/install',
-                        data: {name: name, force: force ? 1 : 0, uid: uid, token: token, version: version, faversion: Config.fastadmin.version}
-                    }, function (data, ret) {
-                        Layer.closeAll();
-                        Config['addons'][data.addon.name] = ret.data.addon;
-                        Layer.alert(__('Online installed tips'), {
-                            btn: [__('OK'), __('Donate')],
-                            title: __('Warning'),
-                            icon: 1,
-                            btn2: function () {
-                                //打赏
-                                Layer.open({
-                                    content: Template("paytpl", {payimg: $(that).data("donateimage")}),
-                                    shade: 0.8,
-                                    area: ['800px', '600px'],
-                                    skin: 'layui-layer-msg layui-layer-pay',
-                                    title: false,
-                                    closeBtn: true,
-                                    btn: false,
-                                    resize: false,
-                                });
-                            }
-                        });
-                        $('.btn-refresh').trigger('click');
-                    }, function (data, ret) {
-                        //如果是需要购买的插件则弹出二维码提示
-                        if (ret && ret.code === -1) {
-                            //扫码支付
+                Fast.api.ajax({
+                    url: 'addon/install',
+                    data: {name: name, force: force ? 1 : 0, uid: uid, token: token, version: version, faversion: Config.fastadmin.version}
+                }, function (data, ret) {
+                    Layer.closeAll();
+                    Config['addons'][data.addon.name] = ret.data.addon;
+                    Layer.alert(__('Online installed tips'), {
+                        btn: [__('OK'), __('Donate')],
+                        title: __('Warning'),
+                        icon: 1,
+                        btn2: function () {
+                            //打赏
                             Layer.open({
-                                content: Template("paytpl", ret.data),
+                                content: Template("paytpl", {payimg: $(that).data("donateimage")}),
                                 shade: 0.8,
                                 area: ['800px', '600px'],
                                 skin: 'layui-layer-msg layui-layer-pay',
@@ -242,44 +224,157 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                                 closeBtn: true,
                                 btn: false,
                                 resize: false,
-                                end: function () {
-                                    Layer.alert(__('Pay tips'));
-                                }
-                            });
-                        } else if (ret && ret.code === -2) {
-                            //跳转支付
-                            Layer.alert(__('Pay click tips'), {
-                                btn: [__('Pay now'), __('Cancel')],
-                                icon: 0,
-                                success: function (layero) {
-                                    $(".layui-layer-btn0", layero).attr("href", ret.data.payurl).attr("target", "_blank");
-                                }
-                            }, function () {
-                                Layer.alert(__('Pay new window tips'), {icon: 0});
                             });
+                        }
+                    });
+                    $('.btn-refresh').trigger('click');
+                    Fast.api.refreshmenu();
+                }, function (data, ret) {
+                    //如果是需要购买的插件则弹出二维码提示
+                    if (ret && ret.code === -1) {
+                        //扫码支付
+                        Layer.open({
+                            content: Template("paytpl", ret.data),
+                            shade: 0.8,
+                            area: ['800px', '600px'],
+                            skin: 'layui-layer-msg layui-layer-pay',
+                            title: false,
+                            closeBtn: true,
+                            btn: false,
+                            resize: false,
+                            end: function () {
+                                Layer.alert(__('Pay tips'));
+                            }
+                        });
+                    } else if (ret && ret.code === -2) {
+                        //跳转支付
+                        Layer.alert(__('Pay click tips'), {
+                            btn: [__('Pay now'), __('Cancel')],
+                            icon: 0,
+                            success: function (layero) {
+                                $(".layui-layer-btn0", layero).attr("href", ret.data.payurl).attr("target", "_blank");
+                            }
+                        }, function () {
+                            Layer.alert(__('Pay new window tips'), {icon: 0});
+                        });
 
-                        } else if (ret && ret.code === -3) {
-                            //插件目录发现影响全局的文件
-                            Layer.open({
-                                content: Template("conflicttpl", ret.data),
-                                shade: 0.8,
-                                area: ['800px', '600px'],
-                                title: __('Warning'),
-                                btn: [__('Continue install'), __('Cancel')],
-                                end: function () {
+                    } else if (ret && ret.code === -3) {
+                        //插件目录发现影响全局的文件
+                        Layer.open({
+                            content: Template("conflicttpl", ret.data),
+                            shade: 0.8,
+                            area: ['800px', '600px'],
+                            title: __('Warning'),
+                            btn: [__('Continue install'), __('Cancel')],
+                            end: function () {
 
-                                },
-                                yes: function () {
-                                    install(name, true);
-                                }
-                            });
+                            },
+                            yes: function () {
+                                install(name, true);
+                            }
+                        });
+
+                    } else {
+                        Layer.alert(ret.msg);
+                    }
+                    return false;
+                });
+            };
+
+            var uninstall = function (name, force) {
+                Fast.api.ajax({
+                    url: 'addon/uninstall',
+                    data: {name: name, force: force ? 1 : 0}
+                }, function (data, ret) {
+                    delete Config['addons'][name];
+                    Layer.closeAll();
+                    $('.btn-refresh').trigger('click');
+                    Fast.api.refreshmenu();
+                }, function (data, ret) {
+                    if (ret && ret.code === -3) {
+                        //插件目录发现影响全局的文件
+                        Layer.open({
+                            content: Template("conflicttpl", ret.data),
+                            shade: 0.8,
+                            area: ['800px', '600px'],
+                            title: __('Warning'),
+                            btn: [__('Continue uninstall'), __('Cancel')],
+                            end: function () {
+
+                            },
+                            yes: function () {
+                                uninstall(name, true);
+                            }
+                        });
+
+                    } else {
+                        Layer.alert(ret.msg);
+                    }
+                    return false;
+                });
+            };
+
+            var operate = function (name, action, force) {
+                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();
+                    $('.btn-refresh').trigger('click');
+                    Fast.api.refreshmenu();
+                }, function (data, ret) {
+                    if (ret && ret.code === -3) {
+                        //插件目录发现影响全局的文件
+                        Layer.open({
+                            content: Template("conflicttpl", ret.data),
+                            shade: 0.8,
+                            area: ['800px', '600px'],
+                            title: __('Warning'),
+                            btn: [__('Continue operate'), __('Cancel')],
+                            end: function () {
+
+                            },
+                            yes: function () {
+                                operate(name, action, true);
+                            }
+                        });
+
+                    } else {
+                        Layer.alert(ret.msg);
+                    }
+                    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.fastadmin.version}
+                }, function (data, ret) {
+                    Config['addons'][name].version = version;
+                    Layer.closeAll();
+                    $('.btn-refresh').trigger('click');
+                    Fast.api.refreshmenu();
+                }, function (data, ret) {
+                    Layer.alert(ret.msg);
+                    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;
 
-                        } else {
-                            Layer.alert(ret.msg);
-                        }
-                        return false;
-                    });
-                };
                 if ($(that).data("type") !== 'free') {
                     if (parseInt(uid) === 0) {
                         return Layer.alert(__('Not login tips'), {
@@ -289,99 +384,36 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                                 $(".btn-userinfo").trigger("click");
                             },
                             btn2: function () {
-                                install(name, false);
+                                install(name, version, false);
                             }
                         });
                     }
                 }
-                install(name, false);
+                install(name, version, false);
             });
 
-            //点击卸载
+            // 点击卸载
             $(document).on("click", ".btn-uninstall", function () {
                 var name = $(this).closest(".operate").data("name");
-                var uninstall = function (name, force) {
-                    Fast.api.ajax({
-                        url: 'addon/uninstall',
-                        data: {name: name, force: force ? 1 : 0}
-                    }, function (data, ret) {
-                        delete Config['addons'][name];
-                        Layer.closeAll();
-                        $('.btn-refresh').trigger('click');
-                    }, function (data, ret) {
-                        if (ret && ret.code === -3) {
-                            //插件目录发现影响全局的文件
-                            Layer.open({
-                                content: Template("conflicttpl", ret.data),
-                                shade: 0.8,
-                                area: ['800px', '600px'],
-                                title: __('Warning'),
-                                btn: [__('Continue uninstall'), __('Cancel')],
-                                end: function () {
-
-                                },
-                                yes: function () {
-                                    uninstall(name, true);
-                                }
-                            });
-
-                        } else {
-                            Layer.alert(ret.msg);
-                        }
-                        return false;
-                    });
-                };
                 Layer.confirm(__('Uninstall tips'), function () {
                     uninstall(name, false);
                 });
             });
 
-            //点击配置
+            // 点击配置
             $(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).closest(".operate").data("name");
                 var action = $(this).data("action");
-                var operate = function (name, action, force) {
-                    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();
-                        $('.btn-refresh').trigger('click');
-                    }, function (data, ret) {
-                        if (ret && ret.code === -3) {
-                            //插件目录发现影响全局的文件
-                            Layer.open({
-                                content: Template("conflicttpl", ret.data),
-                                shade: 0.8,
-                                area: ['800px', '600px'],
-                                title: __('Warning'),
-                                btn: [__('Continue operate'), __('Cancel')],
-                                end: function () {
-
-                                },
-                                yes: function () {
-                                    operate(name, action, true);
-                                }
-                            });
-
-                        } else {
-                            Layer.alert(ret.msg);
-                        }
-                        return false;
-                    });
-                };
                 operate(name, action, false);
             });
 
-            //点击升级
+            // 点击升级
             $(document).on("click", ".btn-upgrade", function () {
                 if ($(this).closest(".operate").find("a.btn-disable").size() > 0) {
                     Layer.alert(__('Please disable addon first'), {icon: 7});
@@ -389,24 +421,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 }
                 var name = $(this).closest(".operate").data("name");
                 var version = $(this).data("version");
-                var userinfo = Controller.api.userinfo.get();
-                var uid = userinfo ? userinfo.id : 0;
-                var token = userinfo ? userinfo.token : '';
-                var upgrade = function (name) {
-                    Fast.api.ajax({
-                        url: 'addon/upgrade',
-                        data: {name: name, uid: uid, token: token, version: version, faversion: Config.fastadmin.version}
-                    }, function (data, ret) {
-                        Config['addons'][name].version = version;
-                        Layer.closeAll();
-                        $('.btn-refresh').trigger('click');
-                    }, function (data, ret) {
-                        Layer.alert(ret.msg);
-                        return false;
-                    });
-                };
+
                 Layer.confirm(__('Upgrade tips'), function () {
-                    upgrade(name);
+                    upgrade(name, version);
                 });
             });
 

+ 19 - 9
public/assets/js/backend/auth/rule.js

@@ -20,7 +20,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
             table.bootstrapTable({
                 url: $.fn.bootstrapTable.defaults.extend.index_url,
                 sortName: 'weigh',
-                escape:false, 
+                escape: false,
                 columns: [
                     [
                         {field: 'state', checkbox: true, },
@@ -41,10 +41,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
             });
 
             // 为表格绑定事件
-            Table.api.bindevent(table);//当内容渲染完成后
+            Table.api.bindevent(table);
 
-            //默认隐藏所有子节点
+            //当内容渲染完成后
             table.on('post-body.bs.table', function (e, settings, json, xhr) {
+                //默认隐藏所有子节点
                 //$("a.btn[data-id][data-pid][data-pid!=0]").closest("tr").hide();
                 $(".btn-node-sub.disabled").closest("tr").hide();
 
@@ -57,8 +58,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     $(this).data("shown", !status);
                     return false;
                 });
+                $(".btn-change[data-id],.btn-delone,.btn-dragsort").data("success", function (data, ret) {
+                    Fast.api.refreshmenu();
+                });
 
             });
+            //批量删除后的回调
+            $(".toolbar > .btn-del,.toolbar .btn-more~ul>li>a").data("success", function (e) {
+                Fast.api.refreshmenu();
+            });
             //展开隐藏一级
             $(document.body).on("click", ".btn-toggle", function (e) {
                 $("a.btn[data-id][data-pid][data-pid!=0].disabled").closest("tr").hide();
@@ -88,21 +96,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
         api: {
             formatter: {
                 title: function (value, row, index) {
-                    return !row.ismenu ? "<span class='text-muted'>" + value + "</span>" : value;
+                    return !row.ismenu || row.status == 'hidden' ? "<span class='text-muted'>" + value + "</span>" : value;
                 },
                 name: function (value, row, index) {
-                    return !row.ismenu ? "<span class='text-muted'>" + value + "</span>" : value;
+                    return !row.ismenu || row.status == 'hidden' ? "<span class='text-muted'>" + value + "</span>" : value;
                 },
                 menu: function (value, row, index) {
                     return "<a href='javascript:;' class='btn btn-" + (value ? "info" : "default") + " btn-xs btn-change' data-id='"
                             + row.id + "' data-params='ismenu=" + (value ? 0 : 1) + "'>" + (value ? __('Yes') : __('No')) + "</a>";
                 },
                 icon: function (value, row, index) {
-                    return '<i class="' + value + '"></i>';
+                    return '<span class="' + (!row.ismenu || row.status == 'hidden' ? 'text-muted' : '') + '"><i class="' + value + '"></i></span>';
                 },
                 subnode: function (value, row, index) {
-                    return '<a href="javascript:;" data-id="' + row['id'] + '" data-pid="' + row['pid'] + '" class="btn btn-xs '
-                            + (row['haschild'] == 1 ? 'btn-success' : 'btn-default disabled') + ' btn-node-sub"><i class="fa fa-sitemap"></i></a>';
+                    return '<a href="javascript:;" data-id="' + row.id + '" data-pid="' + row.pid + '" class="btn btn-xs '
+                            + (row.haschild == 1 || row.ismenu == 1 ? 'btn-success' : 'btn-default disabled') + ' btn-node-sub"><i class="fa fa-sitemap"></i></a>';
                 }
             },
             bindevent: function () {
@@ -113,7 +121,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 $("input[name='row[ismenu]']:checked").trigger("click");
 
                 var iconlist = [];
-                Form.api.bindevent($("form[role=form]"));
+                Form.api.bindevent($("form[role=form]"), function (data) {
+                    Fast.api.refreshmenu();
+                });
                 $(document).on('click', ".btn-search-icon", function () {
                     if (iconlist.length == 0) {
                         $.get(Config.site.cdnurl + "/assets/libs/font-awesome/less/variables.less", function (ret) {

+ 20 - 58
public/assets/js/backend/index.js

@@ -178,6 +178,21 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
                 }
             });
 
+            //刷新菜单事件
+            $(document).on('refresh', '.sidebar-menu', function () {
+                Fast.api.ajax({
+                    url: 'index/index',
+                    data: {action: 'refreshmenu'}
+                }, function (data) {
+                    $(".sidebar-menu li:not([data-rel='external'])").remove();
+                    $(data.menulist).insertBefore($(".sidebar-menu li:first"));
+                    $("#nav ul li[role='presentation'].active a").trigger('click');
+                    return false;
+                }, function () {
+                    return false;
+                });
+            });
+
             //这一行需要放在点击左侧链接事件之前
             var addtabs = Config.referer ? localStorage.getItem("addtabs") : null;
 
@@ -188,6 +203,7 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
             } else {
                 $("ul.sidebar-menu li a[url!='javascript:;']:first").trigger("click");
             }
+
             //如果是刷新操作则直接返回刷新前的页面
             if (Config.referer) {
                 if (Config.referer === $(addtabs).attr("url")) {
@@ -203,11 +219,6 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
                 }
             }
 
-            /**
-             * List of all the available skins
-             *
-             * @type Array
-             */
             var my_skins = [
                 "skin-blue",
                 "skin-white",
@@ -224,19 +235,13 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
             ];
             setup();
 
-            /**
-             * Toggles layout classes
-             *
-             * @param String cls the layout class to toggle
-             * @returns void
-             */
             function change_layout(cls) {
                 $("body").toggleClass(cls);
                 AdminLTE.layout.fixSidebar();
                 //Fix the problem with right sidebar and layout boxed
                 if (cls == "layout-boxed")
                     AdminLTE.controlSidebar._fix($(".control-sidebar-bg"));
-                if ($('body').hasClass('fixed') && cls == 'fixed' && false) {
+                if ($('body').hasClass('fixed') && cls == 'fixed') {
                     AdminLTE.pushMenu.expandOnHover();
                     AdminLTE.layout.activate();
                 }
@@ -244,61 +249,18 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
                 AdminLTE.controlSidebar._fix($(".control-sidebar"));
             }
 
-            /**
-             * Replaces the old skin with the new skin
-             * @param String cls the new skin class
-             * @returns Boolean false to prevent link's default action
-             */
             function change_skin(cls) {
                 if (!$("body").hasClass(cls)) {
-                    $.each(my_skins, function (i) {
-                        $("body").removeClass(my_skins[i]);
-                    });
-
-                    $("body").addClass(cls);
-                    store('skin', cls);
+                    $("body").removeClass(my_skins.join(' ')).addClass(cls);
+                    localStorage.setItem('skin', cls);
                     var cssfile = Config.site.cdnurl + "/assets/css/skins/" + cls + ".css";
                     $('head').append('<link rel="stylesheet" href="' + cssfile + '" type="text/css" />');
                 }
                 return false;
             }
 
-            /**
-             * Store a new settings in the browser
-             *
-             * @param String name Name of the setting
-             * @param String val Value of the setting
-             * @returns void
-             */
-            function store(name, val) {
-                if (typeof (Storage) !== "undefined") {
-                    localStorage.setItem(name, val);
-                } else {
-                    window.alert('Please use a modern browser to properly view this template!');
-                }
-            }
-
-            /**
-             * Get a prestored setting
-             *
-             * @param String name Name of of the setting
-             * @returns String The value of the setting | null
-             */
-            function get(name) {
-                if (typeof (Storage) !== "undefined") {
-                    return localStorage.getItem(name);
-                } else {
-                    window.alert('Please use a modern browser to properly view this template!');
-                }
-            }
-
-            /**
-             * Retrieve default settings and apply them to the template
-             *
-             * @returns void
-             */
             function setup() {
-                var tmp = get('skin');
+                var tmp = localStorage.getItem('skin');
                 if (tmp && $.inArray(tmp, my_skins))
                     change_skin(tmp);
 

+ 1 - 1
public/assets/js/backend/user/rule.js

@@ -28,7 +28,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         {field: 'id', title: __('Id')},
                         {field: 'pid', title: __('Pid'), visible: false},
                         {field: 'title', title: __('Title'), align: 'left'},
-                        {field: 'name', title: __('Name')},
+                        {field: 'name', title: __('Name'), align: 'left'},
                         {field: 'remark', title: __('Remark')},
                         {field: 'ismenu', title: __('Ismenu'), formatter: Controller.api.formatter.toggle},
                         {field: 'createtime', title: __('Createtime'), formatter: Table.api.formatter.datetime, visible: false},

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

@@ -104,7 +104,7 @@
 
     var createFormCommon = function (pColumns, that) {
         var htmlForm = [];
-        var opList = ['=', '>', '>=', '<', '<=', '!=', 'LIKE', 'LIKE %...%', 'NOT LIKE', 'IN', 'NOT IN', 'IN(...)', 'NOT IN(...)', 'BETWEEN', 'NOT BETWEEN', 'RANGE', 'NOT RANGE', 'IS NULL', 'IS NOT NULL'];
+        var opList = ['=', '>', '>=', '<', '<=', '!=', 'FIND_IN_SET', 'LIKE', 'LIKE %...%', 'NOT LIKE', 'IN', 'NOT IN', 'IN(...)', 'NOT IN(...)', 'BETWEEN', 'NOT BETWEEN', 'RANGE', 'NOT RANGE', 'IS NULL', 'IS NOT NULL'];
         htmlForm.push(sprintf('<form class="form-horizontal form-commonsearch" action="%s" >', that.options.actionForm));
         htmlForm.push('<fieldset>');
         if (that.options.titleForm.length > 0)

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

@@ -33,6 +33,7 @@
         if (!that.options.templateView) {
             return;
         }
+        that.options.cardView = true;
 
     };
 

+ 1 - 2
public/assets/js/fast.js

@@ -164,8 +164,7 @@ define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang'], function ($, undefine
                     options.area = [top.$(".tab-pane.active").width() + "px", top.$(".tab-pane.active").height() + "px"];
                     options.offset = [top.$(".tab-pane.active").scrollTop() + "px", "0px"];
                 }
-                Layer.open(options);
-                return false;
+                return Layer.open(options);
             },
             //关闭窗口并回传数据
             close: function (data) {

+ 3 - 0
public/assets/js/frontend-init.js

@@ -0,0 +1,3 @@
+define(['frontend'], function (Frontend) {
+
+});

+ 8 - 11
public/assets/js/require-backend.js

@@ -7,7 +7,7 @@ require.config({
         }
     ],
     //在打包压缩时将会把include中的模块合并到主文件中
-    include: ['css', 'layer', 'toastr', 'fast', 'backend', 'table', 'form', 'dragsort', 'drag', 'drop', 'addtabs', 'selectpage'],
+    include: ['css', 'layer', 'toastr', 'fast', 'backend', 'backend-init', 'table', 'form', 'dragsort', 'drag', 'drop', 'addtabs', 'selectpage'],
     paths: {
         'lang': "empty:",
         'form': 'require-form',
@@ -34,22 +34,20 @@ require.config({
         'bootstrap-table-mobile': '../libs/bootstrap-table/dist/extensions/mobile/bootstrap-table-mobile',
         'bootstrap-table-lang': '../libs/bootstrap-table/dist/locale/bootstrap-table-zh-CN',
         'tableexport': '../libs/tableExport.jquery.plugin/tableExport.min',
-        'dragsort': '../libs/dragsort/jquery.dragsort',
-        'qrcode': '../libs/jquery-qrcode/jquery.qrcode.min',
+        'dragsort': '../libs/fastadmin-dragsort/jquery.dragsort',
         'sortable': '../libs/Sortable/Sortable.min',
-        'addtabs': '../libs/jquery-addtabs/jquery.addtabs',
+        'addtabs': '../libs/fastadmin-addtabs/jquery.addtabs',
         'slimscroll': '../libs/jquery-slimscroll/jquery.slimscroll',
-        'summernote': '../libs/summernote/dist/lang/summernote-zh-CN.min',
         'validator-core': '../libs/nice-validator/dist/jquery.validator',
         'validator-lang': '../libs/nice-validator/dist/local/zh-CN',
         'plupload': '../libs/plupload/js/plupload.min',
         'toastr': '../libs/toastr/toastr',
         'jstree': '../libs/jstree/dist/jstree.min',
-        'layer': '../libs/layer/src/layer',
+        'layer': '../libs/layer/dist/layer',
         'cookie': '../libs/jquery.cookie/jquery.cookie',
-        'cxselect': '../libs/jquery-cxselect/js/jquery.cxselect',
+        'cxselect': '../libs/fastadmin-cxselect/js/jquery.cxselect',
         'template': '../libs/art-template/dist/template-native',
-        'selectpage': '../libs/selectpage/selectpage',
+        'selectpage': '../libs/fastadmin-selectpage/selectpage',
         'citypicker': '../libs/city-picker/dist/js/city-picker.min',
         'citypicker-data': '../libs/city-picker/dist/js/city-picker.data',
     },
@@ -106,7 +104,6 @@ require.config({
         ],
         'bootstrap-select': ['css!../libs/bootstrap-select/dist/css/bootstrap-select.min.css', ],
         'bootstrap-select-lang': ['bootstrap-select'],
-        'summernote': ['../libs/summernote/dist/summernote.min', 'css!../libs/summernote/dist/summernote.css'],
 //        'toastr': ['css!../libs/toastr/toastr.min.css'],
         'jstree': ['css!../libs/jstree/dist/themes/default/style.css', ],
         'plupload': {
@@ -116,7 +113,7 @@ require.config({
 //        'layer': ['css!../libs/layer/dist/theme/default/layer.css'],
 //        'validator-core': ['css!../libs/nice-validator/dist/jquery.validator.css'],
         'validator-lang': ['validator-core'],
-//        'selectpage': ['css!../libs/selectpage/selectpage.css'],
+//        'selectpage': ['css!../libs/fastadmin-selectpage/selectpage.css'],
         'citypicker': ['citypicker-data', 'css!../libs/city-picker/dist/css/city-picker.css']
     },
     baseUrl: requirejs.s.contexts._.config.config.site.cdnurl + '/assets/js/', //资源基础路径
@@ -144,7 +141,7 @@ require(['jquery', 'bootstrap'], function ($, undefined) {
     // 初始化
     $(function () {
         require(['fast'], function (Fast) {
-            require(['backend', 'addons'], function (Backend, Addons) {
+            require(['backend', 'backend-init', 'addons'], function (Backend, undefined, Addons) {
                 //加载相应模块
                 if (Config.jsname) {
                     require([Config.jsname], function (Controller) {

Різницю між файлами не показано, бо вона завелика
+ 1321 - 2858
public/assets/js/require-backend.min.js


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

@@ -40,6 +40,9 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                         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;
@@ -54,6 +57,9 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                             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)) {
@@ -81,13 +87,25 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                 if ($(".selectpage", form).size() > 0) {
                     require(['selectpage'], function () {
                         $('.selectpage', form).selectPage({
-                            source: 'ajax/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验证触发事件
-                    $(form).on("change", ".selectpage-input-hidden", function () {
+                    $(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) {
@@ -132,6 +150,48 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                     });
                 }
             },
+            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(options.locale.format) + " - " + end.format(options.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({}, options, $(this).data()), callback);
+                        });
+                    });
+                }
+            },
             plupload: function (form) {
                 //绑定plupload上传元素事件
                 if ($(".plupload", form).size() > 0) {
@@ -287,6 +347,8 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
 
                 events.selectpicker(form);
 
+                events.daterangepicker(form);
+
                 events.selectpage(form);
 
                 events.cxselect(form);

+ 7 - 10
public/assets/js/require-frontend.js

@@ -7,7 +7,7 @@ require.config({
         }
     ],
     //在打包压缩时将会把include中的模块合并到主文件中
-    include: ['css', 'layer', 'toastr', 'fast', 'frontend'],
+    include: ['css', 'layer', 'toastr', 'fast', 'frontend', 'frontend-init'],
     paths: {
         'lang': "empty:",
         'form': 'require-form',
@@ -34,22 +34,20 @@ require.config({
         'bootstrap-table-mobile': '../libs/bootstrap-table/dist/extensions/mobile/bootstrap-table-mobile',
         'bootstrap-table-lang': '../libs/bootstrap-table/dist/locale/bootstrap-table-zh-CN',
         'tableexport': '../libs/tableExport.jquery.plugin/tableExport.min',
-        'dragsort': '../libs/dragsort/jquery.dragsort',
-        'qrcode': '../libs/jquery-qrcode/jquery.qrcode.min',
+        'dragsort': '../libs/fastadmin-dragsort/jquery.dragsort',
         'sortable': '../libs/Sortable/Sortable.min',
-        'addtabs': '../libs/jquery-addtabs/jquery.addtabs',
+        'addtabs': '../libs/fastadmin-addtabs/jquery.addtabs',
         'slimscroll': '../libs/jquery-slimscroll/jquery.slimscroll',
-        'summernote': '../libs/summernote/dist/lang/summernote-zh-CN.min',
         'validator-core': '../libs/nice-validator/dist/jquery.validator',
         'validator-lang': '../libs/nice-validator/dist/local/zh-CN',
         'plupload': '../libs/plupload/js/plupload.min',
         'toastr': '../libs/toastr/toastr',
         'jstree': '../libs/jstree/dist/jstree.min',
-        'layer': '../libs/layer/src/layer',
+        'layer': '../libs/layer/dist/layer',
         'cookie': '../libs/jquery.cookie/jquery.cookie',
-        'cxselect': '../libs/jquery-cxselect/js/jquery.cxselect',
+        'cxselect': '../libs/fastadmin-cxselect/js/jquery.cxselect',
         'template': '../libs/art-template/dist/template-native',
-        'selectpage': '../libs/selectpage/selectpage',
+        'selectpage': '../libs/fastadmin-selectpage/selectpage',
         'citypicker': '../libs/city-picker/dist/js/city-picker.min',
         'citypicker-data': '../libs/city-picker/dist/js/city-picker.data',
     },
@@ -106,7 +104,6 @@ require.config({
         ],
         'bootstrap-select': ['css!../libs/bootstrap-select/dist/css/bootstrap-select.min.css', ],
         'bootstrap-select-lang': ['bootstrap-select'],
-        'summernote': ['../libs/summernote/dist/summernote.min', 'css!../libs/summernote/dist/summernote.css'],
 //        'toastr': ['css!../libs/toastr/toastr.min.css'],
         'jstree': ['css!../libs/jstree/dist/themes/default/style.css', ],
         'plupload': {
@@ -116,7 +113,7 @@ require.config({
 //        'layer': ['css!../libs/layer/dist/theme/default/layer.css'],
 //        'validator-core': ['css!../libs/nice-validator/dist/jquery.validator.css'],
         'validator-lang': ['validator-core'],
-//        'selectpage': ['css!../libs/selectpage/selectpage.css'],
+//        'selectpage': ['css!../libs/fastadmin-selectpage/selectpage.css'],
         'citypicker': ['citypicker-data', 'css!../libs/city-picker/dist/css/city-picker.css']
     },
     baseUrl: requirejs.s.contexts._.config.config.site.cdnurl + '/assets/js/', //资源基础路径

Різницю між файлами не показано, бо вона завелика
+ 13 - 1317
public/assets/js/require-frontend.min.js


+ 54 - 16
public/assets/js/require-table.js

@@ -1,4 +1,4 @@
-define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table', 'bootstrap-table-lang', 'bootstrap-table-mobile', 'bootstrap-table-export', 'bootstrap-table-commonsearch', 'bootstrap-table-template'], function ($, undefined, Moment) {
+define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table', 'bootstrap-table-lang', 'bootstrap-table-export', 'bootstrap-table-commonsearch', 'bootstrap-table-template'], function ($, undefined, Moment) {
     var Table = {
         list: {},
         // Bootstrap-table 基础配置
@@ -32,7 +32,6 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
             paginationPreText: __("Previous"),
             paginationNextText: __("Next"),
             paginationLastText: __("Last"),
-            mobileResponsive: true, //是否自适应移动端
             cardView: false, //卡片视图
             checkOnInit: true, //是否在初始化时判断
             escape: true, //是否对内容进行转义
@@ -152,7 +151,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     }
                 });
                 // 处理选中筛选框后按钮的状态统一变更
-                table.on('check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table fa.event.check', function () {
+                table.on('check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table', function () {
                     var ids = Table.api.selectedids(table);
                     $(Table.config.disabledbtn, toolbar).toggleClass('disabled', !ids.length);
                 });
@@ -176,7 +175,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                             Fast.api.ajax({
                                 url: options.extend.import_url,
                                 data: {file: data.url},
-                            }, function () {
+                            }, function (data, ret) {
                                 table.bootstrapTable('refresh');
                             });
                         });
@@ -217,7 +216,8 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     $("tbody", table).dragsort({
                         itemSelector: 'tr:visible',
                         dragSelector: "a.btn-dragsort",
-                        dragEnd: function () {
+                        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');
@@ -238,7 +238,21 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                                     table: options.extend.table
                                 }
                             };
-                            Fast.api.ajax(params, function (data) {
+                            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 () {
+                                var error = $(element).data("error") || $.noop;
+                                if (typeof error === 'function') {
+                                    if (false === error.call(element, data, ret)) {
+                                        return false;
+                                    }
+                                }
                                 table.bootstrapTable('refresh');
                             });
                         },
@@ -246,7 +260,9 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     });
                 });
                 $(table).on("click", "input[data-id][name='checkbox']", function (e) {
-                    table.trigger('fa.event.check');
+                    var ids = $(this).data("id");
+                    var row = Table.api.getrowbyid(ids);
+                    table.trigger('check.bs.table', [row, this]);
                 });
                 $(table).on("click", "[data-id].btn-change", function (e) {
                     e.preventDefault();
@@ -255,14 +271,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 $(table).on("click", "[data-id].btn-edit", function (e) {
                     e.preventDefault();
                     var ids = $(this).data("id");
-                    var row = {};
-                    var options = table.bootstrapTable("getOptions");
-                    $.each(table.bootstrapTable('getData'), function (i, j) {
-                        if (j[options.pk] == ids) {
-                            row = j;
-                            return false;
-                        }
-                    });
+                    var row = Table.api.getrowbyid(ids);
                     row.ids = ids;
                     var url = Table.api.replaceurl(options.extend.edit_url, row, table);
                     Fast.api.open(url, __('Edit'), $(this).data() || {});
@@ -293,8 +302,21 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 url = this.replaceurl(url, {ids: ids}, table);
                 var params = typeof data.params !== "undefined" ? (typeof data.params == 'object' ? $.param(data.params) : data.params) : '';
                 var options = {url: url, data: {action: action, ids: ids, params: params}};
-                Fast.api.ajax(options, function (data) {
+                Fast.api.ajax(options, 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;
+                        }
+                    }
                 });
             },
             // 单元格元素事件
@@ -521,6 +543,22 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 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.bootstrapTable('getData'), function (i, j) {
+                    if (j[options.pk] == id) {
+                        row = j;
+                        return false;
+                    }
+                });
+                return row;
             }
         },
     };

Різницю між файлами не показано, бо вона завелика
+ 2 - 0
public/assets/js/require.min.js


+ 19 - 6
public/assets/less/backend.less

@@ -15,7 +15,7 @@
 @import url("../libs/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css");
 @import url("../libs/bootstrap-daterangepicker/daterangepicker.css");
 @import url("../libs/nice-validator/dist/jquery.validator.css");
-@import url("../libs/selectpage/selectpage.css");
+@import url("../libs/fastadmin-selectpage/selectpage.css");
 
 @main-bg:           #f1f4f6;
 @panel-intro-bg:    darken(@main-bg,3%);
@@ -54,7 +54,6 @@ body.is-dialog {
         }
     }
 }
-.note-dialog .modal {z-index:1060;}
 
 .bootstrap-dialog .modal-dialog {
     /*width: 70%;*/
@@ -668,13 +667,16 @@ form.form-horizontal .control-label {
         background: #ecf0f1;
         overflow:hidden;
         a {
-            background-color: #95a5a6!important;
-            border-color: #95a5a6!important;
+            background-color: #95a5a6;
+            border-color: #95a5a6;
             color:#fff!important;
+            height:31px;
+            margin-top:0;
+            border: 1px solid transparent;
         }
         .layui-layer-btn0{
-            background-color: #18bc9c!important;
-            border-color: #18bc9c!important;
+            background-color: #18bc9c;
+            border-color: #18bc9c;
         }
     }
     .layui-layer-footer {
@@ -763,6 +765,14 @@ form.form-horizontal .control-label {
         position:absolute;
     }
 }
+@media (min-width: 564px){
+    body.is-dialog .daterangepicker  {
+        min-width: 130px;
+    }
+    body.is-dialog .daterangepicker .ranges ul {
+        width: 130px;
+    }
+}
 
 /*手机版样式*/
 @media (max-width: @screen-phone) {
@@ -774,6 +784,9 @@ form.form-horizontal .control-label {
             display:none;
         }
     }
+    .fixed .content-wrapper, .fixed .right-side {
+        padding-top: 50px;
+    }
 }
 /*平板样式*/
 @media (max-width: @screen-tablet) {

+ 1 - 10
public/assets/less/frontend.less

@@ -61,15 +61,6 @@ body {
     .box-shadow(none);
 }
 
-.layui-layer-fast {
-    -webkit-animation-fill-mode: both;
-    animation-fill-mode: both;
-    -webkit-animation-duration: .3s;
-    animation-duration: .3s;
-    -webkit-animation-name: layer-bounceIn;
-    animation-name: layer-bounceIn;
-}
-
 /*修复nice-validator和summernote的编辑框冲突*/
 .nice-validator .note-editor .note-editing-area .note-editable{
     display:inherit;
@@ -329,7 +320,7 @@ body {
     }
 }
 
-footer.footer{width:100%;color: #aaa;background: #555;margin-top:25px;}
+footer.footer{width:100%;color: #aaa;background: #555;margin-top:25px;position: fixed;bottom: 0;}
 footer.footer ul{margin:60px 0 30px 0;padding:0;}
 footer.footer ul li.f-tit{margin-bottom:10px;font-size: 14px;color: #fff;}
 footer.footer ul li{line-height: 26px;white-space: nowrap;list-style: none;margin:0;padding:0;}