Browse Source

Merge branch 'develop'

# Conflicts:
#	application/admin/command/Api.php
#	application/admin/command/Install.php
#	application/config.php
Karson 4 years ago
parent
commit
b82dbd7eee
44 changed files with 31611 additions and 368 deletions
  1. 1 0
      .gitignore
  2. 27 14
      application/admin/command/Api.php
  3. 3 0
      application/admin/command/Api/lang/zh-cn.php
  4. 28 23
      application/admin/command/Api/library/Builder.php
  5. 46 13
      application/admin/command/Api/library/Extractor.php
  6. 96 23
      application/admin/command/Api/template/index.html
  7. 20 8
      application/admin/command/Install.php
  8. 2 2
      application/admin/command/Install/fastadmin.sql
  9. 1 1
      application/admin/controller/Addon.php
  10. 1 1
      application/admin/controller/Category.php
  11. 10 3
      application/admin/controller/auth/Admin.php
  12. 5 5
      application/admin/controller/auth/Adminlog.php
  13. 14 17
      application/admin/controller/auth/Group.php
  14. 3 0
      application/admin/controller/auth/Rule.php
  15. 2 2
      application/admin/controller/general/Config.php
  16. 2 2
      application/admin/lang/zh-cn.php
  17. 1 1
      application/admin/lang/zh-cn/addon.php
  18. 6 5
      application/admin/lang/zh-cn/auth/admin.php
  19. 1 1
      application/admin/lang/zh-cn/auth/group.php
  20. 2 1
      application/admin/lang/zh-cn/auth/rule.php
  21. 2 2
      application/admin/lang/zh-cn/general/config.php
  22. 5 5
      application/admin/library/Auth.php
  23. 1 1
      application/admin/view/general/config/index.html
  24. 63 7
      application/common.php
  25. 38 40
      application/common/library/Email.php
  26. 1 0
      application/common/model/Config.php
  27. 3 1
      application/config.php
  28. 1 1
      application/extra/upload.php
  29. 17 2
      application/index/controller/User.php
  30. 1 1
      bower.json
  31. 3 4
      composer.json
  32. 866 100
      public/api.html
  33. 1 1
      public/assets/css/backend.min.css
  34. 1 1
      public/assets/css/frontend.min.css
  35. 20 0
      public/assets/js/backend/addon.js
  36. 2 1
      public/assets/js/fast.js
  37. 7 10
      public/assets/js/frontend/user.js
  38. 1 1
      public/assets/js/require-backend.js
  39. 15307 17
      public/assets/js/require-backend.min.js
  40. 13 10
      public/assets/js/require-form.js
  41. 1 1
      public/assets/js/require-frontend.js
  42. 14915 17
      public/assets/js/require-frontend.min.js
  43. 65 20
      public/assets/js/require-table.js
  44. 6 3
      public/assets/js/require-upload.js

+ 1 - 0
.gitignore

@@ -15,3 +15,4 @@ composer.lock
 .env
 .svn
 .vscode
+node_modules

+ 27 - 14
application/admin/command/Api.php

@@ -23,9 +23,9 @@ class Api extends Command
             ->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')
+            ->addOption('addon', 'a', Option::VALUE_OPTIONAL, 'addon name', null)
             ->addOption('controller', 'r', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'controller name', null)
             ->setDescription('Build Api document from controller');
     }
@@ -59,12 +59,21 @@ class Api extends Command
         $classes = $input->getOption('class');
         // 标题
         $title = $input->getOption('title');
-        // 作者
-        $author = $input->getOption('author');
         // 模块
         $module = $input->getOption('module');
-
-        $moduleDir = APP_PATH . $module . DS;
+        // 插件
+        $addon = $input->getOption('addon');
+
+        $moduleDir = $addonDir = '';
+        if ($addon) {
+            $addonInfo = get_addon_info($addon);
+            if (!$addonInfo) {
+                throw new Exception('addon not found');
+            }
+            $moduleDir = ADDON_PATH . $addon . DS;
+        } else {
+            $moduleDir = APP_PATH . $module . DS;
+        }
         if (!is_dir($moduleDir)) {
             throw new Exception('module not found');
         }
@@ -81,9 +90,10 @@ class Api extends Command
                 throw new Exception("Please make sure opcache already enabled, Get help:https://forum.fastadmin.net/d/1321");
             }
         }
+
         //控制器名
-        $controller = $input->getOption('controller') ?: '';
-        if(!$controller) {
+        $controller = $input->getOption('controller') ?: [];
+        if (!$controller) {
             $controllerDir = $moduleDir . Config::get('url_controller_layer') . DS;
             $files = new \RecursiveIteratorIterator(
                 new \RecursiveDirectoryIterator($controllerDir),
@@ -96,26 +106,29 @@ class Api extends Command
                     $classes[] = $this->get_class_from_file($filePath);
                 }
             }
-        }
-        else{
+        } else {
             foreach ($controller as $index => $item) {
-                $filePath=$moduleDir . Config::get('url_controller_layer') . DS .$item.'.php';
+                $filePath = $moduleDir . Config::get('url_controller_layer') . DS . $item . '.php';
                 $classes[] = $this->get_class_from_file($filePath);
             }
         }
+
         $classes = array_unique(array_filter($classes));
 
         $config = [
             'sitename'    => config('site.name'),
             'title'       => $title,
-            'author'      => $author,
+            'author'      => config('site.name'),
             'description' => '',
             'apiurl'      => $url,
             'language'    => $language,
         ];
-        $builder = new Builder($classes);
-        $content = $builder->render($template_file, ['config' => $config, 'lang' => $lang]);
-
+        try {
+            $builder = new Builder($classes);
+            $content = $builder->render($template_file, ['config' => $config, 'lang' => $lang]);
+        } catch (\Exception $e) {
+            print_r($e);
+        }
         if (!file_put_contents($output_file, $content)) {
             throw new Exception('Cannot save the content to ' . $output_file);
         }

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

@@ -16,6 +16,9 @@ return [
     'Tokentips'        => 'Token在会员注册或登录后都会返回,WEB端同时存在于Cookie中',
     'Apiurltips'       => 'API接口URL',
     'Savetips'         => '点击保存后Token和Api url都将保存在本地Localstorage中',
+    'Authorization'    => '权限',
+    'NeedLogin'        => '登录',
+    'NeedRight'        => '鉴权',
     'ReturnHeaders'    => '响应头',
     'ReturnParameters' => '返回参数',
     'Response'         => '响应输出',

+ 28 - 23
application/admin/command/Api/library/Builder.php

@@ -43,9 +43,11 @@ class Builder
                 continue;
             }
             Extractor::getClassMethodAnnotations($class);
+            //Extractor::getClassPropertyValues($class);
         }
         $allClassAnnotation = Extractor::getAllClassAnnotations();
         $allClassMethodAnnotation = Extractor::getAllClassMethodAnnotations();
+        //$allClassPropertyValue = Extractor::getAllClassPropertyValues();
 
 //        foreach ($allClassMethodAnnotation as $className => &$methods) {
 //            foreach ($methods as &$method) {
@@ -162,10 +164,12 @@ class Builder
         list($allClassAnnotations, $allClassMethodAnnotations) = $this->extractAnnotations();
 
         $sectorArr = [];
-        foreach ($allClassAnnotations as $index => $allClassAnnotation) {
+        foreach ($allClassAnnotations as $index => &$allClassAnnotation) {
             $sector = isset($allClassAnnotation['ApiSector']) ? $allClassAnnotation['ApiSector'][0] : $allClassAnnotation['ApiTitle'][0];
             $sectorArr[$sector] = isset($allClassAnnotation['ApiWeigh']) ? $allClassAnnotation['ApiWeigh'][0] : 0;
         }
+        unset($allClassAnnotation);
+
         arsort($sectorArr);
         $routes = include_once CONF_PATH . 'route.php';
         $subdomain = false;
@@ -175,7 +179,7 @@ class Builder
         $counter = 0;
         $section = null;
         $weigh = 0;
-        $docslist = [];
+        $docsList = [];
         foreach ($allClassMethodAnnotations as $class => $methods) {
             foreach ($methods as $name => $docs) {
                 if (isset($docs['ApiSector'][0])) {
@@ -190,28 +194,30 @@ class Builder
                 if ($subdomain) {
                     $route = substr($route, 4);
                 }
-                $docslist[$section][$class . $name] = [
-                    'id'                => $counter,
-                    'method'            => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0],
-                    'method_label'      => $this->generateBadgeForMethod($docs),
-                    'section'           => $section,
-                    'route'             => $route,
-                    'title'             => is_array($docs['ApiTitle'][0]) ? $docs['ApiTitle'][0]['data'] : $docs['ApiTitle'][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),
-                    'weigh'             => is_array($docs['ApiWeigh'][0]) ? $docs['ApiWeigh'][0]['data'] : $docs['ApiWeigh'][0],
-                    'return'            => isset($docs['ApiReturn']) ? is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0] : '',
+                $docsList[$section][$name] = [
+                    'id'                 => $counter,
+                    'method'             => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0],
+                    'methodLabel'        => $this->generateBadgeForMethod($docs),
+                    'section'            => $section,
+                    'route'              => $route,
+                    'title'              => is_array($docs['ApiTitle'][0]) ? $docs['ApiTitle'][0]['data'] : $docs['ApiTitle'][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),
+                    'weigh'              => is_array($docs['ApiWeigh'][0]) ? $docs['ApiWeigh'][0]['data'] : $docs['ApiWeigh'][0],
+                    'return'             => isset($docs['ApiReturn']) ? is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0] : '',
+                    'needLogin' => $docs['ApiPermissionLogin'][0],
+                    'needRight' => $docs['ApiPermissionRight'][0],
                 ];
                 $counter++;
             }
         }
 
         //重建排序
-        foreach ($docslist as $index => &$methods) {
+        foreach ($docsList as $index => &$methods) {
             $methodSectorArr = [];
             foreach ($methods as $name => $method) {
                 $methodSectorArr[$name] = isset($method['weigh']) ? $method['weigh'] : 0;
@@ -219,9 +225,8 @@ class Builder
             arsort($methodSectorArr);
             $methods = array_merge(array_flip(array_keys($methodSectorArr)), $methods);
         }
-        $docslist = array_merge(array_flip(array_keys($sectorArr)), $docslist);
-        $docslist = array_filter($docslist , function($v) {return is_array($v) ; }) ;
-        return $docslist;
+        $docsList = array_merge(array_flip(array_keys($sectorArr)), $docsList);
+        return $docsList;
     }
 
     public function getView()
@@ -237,8 +242,8 @@ class Builder
      */
     public function render($template, $vars = [])
     {
-        $docslist = $this->parse();
+        $docsList = $this->parse();
 
-        return $this->view->display(file_get_contents($template), array_merge($vars, ['docslist' => $docslist]));
+        return $this->view->display(file_get_contents($template), array_merge($vars, ['docsList' => $docsList]));
     }
 }

+ 46 - 13
application/admin/command/Api/library/Extractor.php

@@ -24,6 +24,8 @@ class Extractor
 
     private static $classMethodAnnotationCache;
 
+    private static $classPropertyValueCache;
+
     /**
      * Indicates that annotations should has strict behavior, 'false' by default
      * @var boolean
@@ -66,14 +68,16 @@ class Extractor
     /**
      * Gets all anotations with pattern @SomeAnnotation() from a given class
      *
-     * @param  string $className class name to get annotations
+     * @param string $className class name to get annotations
      * @return array  self::$classAnnotationCache all annotated elements
      */
     public static function getClassAnnotations($className)
     {
         if (!isset(self::$classAnnotationCache[$className])) {
             $class = new \ReflectionClass($className);
-            self::$classAnnotationCache[$className] = self::parseAnnotations($class->getDocComment());
+            $annotationArr = self::parseAnnotations($class->getDocComment());
+            $annotationArr['ApiTitle'] = !isset($annotationArr['ApiTitle'][0]) || !trim($annotationArr['ApiTitle'][0]) ? [$class->getShortName()] : $annotationArr['ApiTitle'];
+            self::$classAnnotationCache[$className] = $annotationArr;
         }
 
         return self::$classAnnotationCache[$className];
@@ -96,6 +100,17 @@ class Extractor
         return self::$classMethodAnnotationCache[$className];
     }
 
+    public static function getClassPropertyValues($className)
+    {
+        $class = new \ReflectionClass($className);
+
+        foreach ($class->getProperties() as $object) {
+            self::$classPropertyValueCache[$className][$object->name] = self::getClassPropertyValue($className, $object->name);
+        }
+
+        return self::$classMethodAnnotationCache[$className];
+    }
+
     public static function getAllClassAnnotations()
     {
         return self::$classAnnotationCache;
@@ -106,11 +121,25 @@ class Extractor
         return self::$classMethodAnnotationCache;
     }
 
+    public static function getAllClassPropertyValues()
+    {
+        return self::$classPropertyValueCache;
+    }
+
+    public static function getClassPropertyValue($className, $property)
+    {
+        $_SERVER['REQUEST_METHOD'] = 'GET';
+        $reflectionClass = new \ReflectionClass($className);
+        $reflectionProperty = $reflectionClass->getProperty($property);
+        $reflectionProperty->setAccessible(true);
+        return $reflectionProperty->getValue($reflectionClass->newInstanceWithoutConstructor());
+    }
+
     /**
      * 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
+     * @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)
@@ -138,8 +167,8 @@ class Extractor
      * 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
+     * @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)
@@ -189,7 +218,11 @@ class Extractor
         $methodName = $method->getName();
 
         $methodAnnotations = self::parseAnnotations($docblockMethod);
+        $methodAnnotations['ApiTitle'] = !isset($methodAnnotations['ApiTitle'][0]) || !trim($methodAnnotations['ApiTitle'][0]) ? [$method->getName()] : $methodAnnotations['ApiTitle'];
+
         $classAnnotations = self::parseAnnotations($dockblockClass);
+        $classAnnotations['ApiTitle'] = !isset($classAnnotations['ApiTitle'][0]) || !trim($classAnnotations['ApiTitle'][0]) ? [$class->getShortName()] : $classAnnotations['ApiTitle'];
+
         if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty') {
             return [];
         }
@@ -264,15 +297,15 @@ class Extractor
             }
         }
         $methodAnnotations['ApiPermissionLogin'] = [!in_array('*', $noNeedLogin) && !in_array($methodName, $noNeedLogin)];
-        $methodAnnotations['ApiPermissionRight'] = [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
+        $methodAnnotations['ApiPermissionRight'] = !$methodAnnotations['ApiPermissionLogin'][0] ? false : [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
         return $methodAnnotations;
     }
 
     /**
      * Parse annotations
      *
-     * @param  string $docblock
-     * @param  string $name
+     * @param string $docblock
+     * @param string $name
      * @return array  parsed annotations params
      */
     private static function parseCustomAnnotations($docblock, $name = 'param')
@@ -291,7 +324,7 @@ class Extractor
     /**
      * Parse annotations
      *
-     * @param  string $docblock
+     * @param string $docblock
      * @return array  parsed annotations params
      */
     private static function parseAnnotations($docblock)
@@ -337,7 +370,7 @@ class Extractor
     /**
      * Parse individual annotation arguments
      *
-     * @param  string $content arguments string
+     * @param string $content arguments string
      * @return array  annotated arguments
      */
     private static function parseArgs($content)
@@ -480,8 +513,8 @@ class Extractor
     /**
      * 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
+     * @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)

+ 96 - 23
application/admin/command/Api/template/index.html

@@ -27,12 +27,12 @@
                 font-family: "Roboto", "SF Pro SC", "SF Pro Display", "SF Pro Icons", "PingFang SC", BlinkMacSystemFont, -apple-system, "Segoe UI", "Microsoft Yahei", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
                 font-weight: 400;
             }
-            h2        { font-size: 1.6em; }
+            h2        { font-size: 1.2em; }
             hr        { margin-top: 10px; }
             .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; }
+            .docs-list .label    { display: inline-block; min-width: 65px; padding: 0.3em 0.6em 0.3em; }
             .string   { color: green; }
             .number   { color: darkorange; }
             .boolean  { color: blue; }
@@ -65,12 +65,24 @@
             #sidebar > .list-group > a{
                 text-indent:0;
             }
+            #sidebar .child > a .tag{
+                position: absolute;
+                right: 10px;
+                top: 11px;
+            }
+            #sidebar .child > a .pull-right{
+                margin-left:3px;
+            }
             #sidebar .child {
                 border:1px solid #ddd;
                 border-bottom:none;
             }
+            #sidebar .child:last-child {
+                border-bottom:1px solid #ddd;
+            }
             #sidebar .child > a {
                 border:0;
+                min-height: 40px;
             }
             #sidebar .list-group a.current {
                 background:#f5f5f5;
@@ -94,6 +106,9 @@
             .label-primary {
                 background-color: #248aff;
             }
+            .docs-list .panel .panel-body .table {
+                margin-bottom: 0;
+            }
 
         </style>
     </head>
@@ -138,25 +153,34 @@
             <!-- menu -->
             <div id="sidebar">
                 <div class="list-group panel">
-                    {foreach name="docslist" id="docs"}
+                    {foreach name="docsList" id="docs"}
                     <a href="#{$key}" class="list-group-item" data-toggle="collapse" data-parent="#sidebar">{$key}  <i class="fa fa-caret-down"></i></a>
                     <div class="child collapse" id="{$key}">
                         {foreach name="docs" id="api" }
-                        <a href="javascript:;" data-id="{$api.id}" class="list-group-item">{$api.title}</a>
+                        <a href="javascript:;" data-id="{$api.id}" class="list-group-item">{$api.title}
+                            <span class="tag">
+                                {if $api.needRight}
+                                    <span class="label label-danger pull-right">鉴</span>
+                                {/if}
+                                {if $api.needLogin}
+                                    <span class="label label-success pull-right noneedlogin">登</span>
+                                {/if}
+                            </span>
+                        </a>
                         {/foreach}
                     </div>
                     {/foreach}
                 </div>
             </div>
-            <div class="panel-group" id="accordion">
-                {foreach name="docslist" id="docs"}
+            <div class="panel-group docs-list" 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" id="heading-{$api.id}">
                         <h4 class="panel-title">
-                            <span class="label {$api.method_label}">{$api.method|strtoupper}</span>
+                            <span class="label {$api.methodLabel}">{$api.method|strtoupper}</span>
                             <a data-toggle="collapse" data-parent="#accordion{$api.id}" href="#collapseOne{$api.id}"> {$api.title} <span class="text-muted">{$api.route}</span></a>
                         </h4>
                     </div>
@@ -178,9 +202,26 @@
                                         {$api.summary}
                                     </div>
                                     <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>{$lang.Authorization}</strong></div>
+                                        <div class="panel-body">
+                                            <table class="table table-hover">
+                                                <tbody>
+                                                <tr>
+                                                    <td>{$lang.NeedLogin}</td>
+                                                    <td>{$api.needLogin?'是':'否'}</td>
+                                                </tr>
+                                                <tr>
+                                                    <td>{$lang.NeedRight}</td>
+                                                    <td>{$api.needRight?'是':'否'}</td>
+                                                </tr>
+                                                </tbody>
+                                            </table>
+                                        </div>
+                                    </div>
+                                    <div class="panel panel-default">
                                         <div class="panel-heading"><strong>{$lang.Headers}</strong></div>
                                         <div class="panel-body">
-                                            {if $api.headerslist}
+                                            {if $api.headersList}
                                             <table class="table table-hover">
                                                 <thead>
                                                     <tr>
@@ -191,7 +232,7 @@
                                                     </tr>
                                                 </thead>
                                                 <tbody>
-                                                    {foreach name="api['headerslist']" id="header"}
+                                                    {foreach name="api['headersList']" id="header"}
                                                     <tr>
                                                         <td>{$header.name}</td>
                                                         <td>{$header.type}</td>
@@ -209,7 +250,7 @@
                                     <div class="panel panel-default">
                                         <div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
                                         <div class="panel-body">
-                                            {if $api.paramslist}
+                                            {if $api.paramsList}
                                             <table class="table table-hover">
                                                 <thead>
                                                     <tr>
@@ -220,7 +261,7 @@
                                                     </tr>
                                                 </thead>
                                                 <tbody>
-                                                    {foreach name="api['paramslist']" id="param"}
+                                                    {foreach name="api['paramsList']" id="param"}
                                                     <tr>
                                                         <td>{$param.name}</td>
                                                         <td>{$param.type}</td>
@@ -246,12 +287,12 @@
                                 <div class="tab-pane" id="sandbox{$api.id}">
                                     <div class="row">
                                         <div class="col-md-12">
-                                            {if $api.headerslist}
+                                            {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"}
+                                                        {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}">
@@ -262,11 +303,15 @@
                                             </div>
                                             {/if}
                                             <div class="panel panel-default">
-                                                <div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
+                                                <div class="panel-heading"><strong>{$lang.Parameters}</strong>
+                                                <div class="pull-right">
+                                                    <a href="javascript:" class="btn btn-xs btn-info btn-append">追加</a>
+                                                </div>
+                                                </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"}
+                                                        {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}">
@@ -277,7 +322,7 @@
                                                         </div>
                                                         {/if}
-                                                        <div class="form-group">
+                                                        <div class="form-group form-group-submit">
                                                             <button type="submit" class="btn btn-success send" rel="{$api.id}">{$lang.Send}</button>
                                                             <button type="reset" class="btn btn-info" rel="{$api.id}">{$lang.Reset}</button>
                                                         </div>
@@ -298,7 +343,7 @@
                                             <div class="panel panel-default">
                                                 <div class="panel-heading"><strong>{$lang.ReturnParameters}</strong></div>
                                                 <div class="panel-body">
-                                                    {if $api.returnparamslist}
+                                                    {if $api.returnParamsList}
                                                     <table class="table table-hover">
                                                         <thead>
                                                             <tr>
@@ -308,7 +353,7 @@
                                                             </tr>
                                                         </thead>
                                                         <tbody>
-                                                            {foreach name="api['returnparamslist']" id="param"}
+                                                            {foreach name="api['returnParamsList']" id="param"}
                                                             <tr>
                                                                 <td>{$param.name}</td>
                                                                 <td>{$param.type}</td>
@@ -346,10 +391,10 @@
 
             <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="./" target="_blank">{$config.sitename}</a>
+                    Generated on {:date('Y-m-d H:i:s')} <a href="./" target="_blank">{$config.sitename}</a>
                 </div>
             </div>
 
@@ -475,7 +520,7 @@
                     $sample.html('<pre>' + str + '</pre>');
                 });
 
-                $('body').on('click', '#save_data', function (e) {
+                $(document).on('click', '#save_data', function (e) {
                     if (storage) {
                         storage.setItem('token', $('#token').val());
                         storage.setItem('apiUrl', $('#apiUrl').val());
@@ -483,8 +528,20 @@
                         alert('Your browser does not support local storage');
                     }
                 });
+                $(document).on('click', '.btn-append', function (e) {
+                    $($("#appendtpl").html()).insertBefore($(this).closest(".panel").find(".form-group-submit"));
+                    return false;
+                });
+                $(document).on('click', '.btn-remove', function (e) {
+                    $(this).closest(".form-group").remove();
+                    return false;
+                });
+                $(document).on('keyup', '.input-custom-name', function (e) {
+                    $(this).closest(".row").find(".input-custom-value").attr("name", $(this).val());
+                    return false;
+                });
 
-                $('body').on('click', '.send', function (e) {
+                $(document).on('click', '.send', function (e) {
                     e.preventDefault();
                     var form = $(this).closest('form');
                     //added /g to get all the matched params instead of only first
@@ -577,5 +634,21 @@
                 });
             });
         </script>
+        <script type="text/html" id="appendtpl">
+            <div class="form-group">
+                <label class="control-label">自定义</label>
+                <div class="row">
+                    <div class="col-xs-4">
+                        <input type="text" class="form-control input-sm input-custom-name" placeholder="名称">
+                    </div>
+                    <div class="col-xs-6">
+                        <input type="text" class="form-control input-sm input-custom-value" placeholder="值">
+                    </div>
+                    <div class="col-xs-2 text-center">
+                        <a href="javascript:" class="btn btn-sm btn-danger btn-remove">删除</a>
+                    </div>
+                </div>
+            </div>
+        </script>
     </body>
 </html>

+ 20 - 8
application/admin/command/Install.php

@@ -201,7 +201,7 @@ class Install extends Command
 
         // 数据库配置文件
         $dbConfigFile = APP_PATH . 'database.php';
-        $config = @file_get_contents($dbConfigFile);
+        $dbConfigText = @file_get_contents($dbConfigFile);
         $callback = function ($matches) use ($mysqlHostname, $mysqlHostport, $mysqlUsername, $mysqlPassword, $mysqlDatabase, $mysqlPrefix) {
             $field = "mysql" . ucfirst($matches[1]);
             $replace = $$field;
@@ -210,14 +210,26 @@ class Install extends Command
             }
             return "'{$matches[1]}'{$matches[2]}=>{$matches[3]}Env::get('database.{$matches[1]}', '{$replace}'),";
         };
-        $config = preg_replace_callback("/'(hostname|database|username|password|hostport|prefix)'(\s+)=>(\s+)Env::get\((.*)\)\,/", $callback, $config);
+        $dbConfigText = preg_replace_callback("/'(hostname|database|username|password|hostport|prefix)'(\s+)=>(\s+)Env::get\((.*)\)\,/", $callback, $config);
 
         // 检测能否成功写入数据库配置
-        $result = @file_put_contents($dbConfigFile, $config);
+        $result = @file_put_contents($dbConfigFile, $dbConfigText);
         if (!$result) {
             throw new Exception(__('The current permissions are insufficient to write the file %s', 'application/database.php'));
         }
 
+        // 设置新的Token随机密钥key
+        $oldTokenKey = config('token.key');
+        $newTokenKey = \fast\Random::alnum(32);
+        $coreConfigFile = CONF_PATH . 'config.php';
+        $coreConfigText = @file_get_contents($coreConfigFile);
+        $coreConfigText = preg_replace("/'key'(\s+)=>(\s+)'{$oldTokenKey}'/", "'key'\$1=>\$2'{$newTokenKey}'", $coreConfigText);
+
+        $result = @file_put_contents($coreConfigFile, $coreConfigText);
+        if (!$result) {
+            throw new Exception(__('The current permissions are insufficient to write the file %s', 'application/config.php'));
+        }
+
         // 变更默认管理员密码
         $adminPassword = $adminPassword ? $adminPassword : Random::alnum(8);
         $adminEmail = $adminEmail ? $adminEmail : "admin@admin.com";
@@ -241,8 +253,8 @@ class Install extends Command
         //修改站点名称
         if ($siteName != config('site.name')) {
             $instance->name('config')->where('name', 'name')->update(['value' => $siteName]);
-            $configFile = CONF_PATH . 'extra' . DS . 'site.php';
-            $config = include $configFile;
+            $siteConfigFile = CONF_PATH . 'extra' . DS . 'site.php';
+            $siteConfig = include $siteConfigFile;
             $configList = $instance->name("config")->select();
             foreach ($configList as $k => $value) {
                 if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) {
@@ -251,10 +263,10 @@ class Install extends Command
                 if ($value['type'] == 'array') {
                     $value['value'] = (array)json_decode($value['value'], true);
                 }
-                $config[$value['name']] = $value['value'];
+                $siteConfig[$value['name']] = $value['value'];
             }
-            $config['name'] = $siteName;
-            file_put_contents($configFile, '<?php' . "\n\nreturn " . var_export_short($config) . ";\n");
+            $siteConfig['name'] = $siteName;
+            file_put_contents($siteConfigFile, '<?php' . "\n\nreturn " . var_export_short($siteConfig) . ";\n");
         }
 
         $installLockFile = INSTALL_PATH . "install.lock";

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

@@ -340,11 +340,11 @@ INSERT INTO `fa_config` VALUES (7, 'languages', 'basic', 'Languages', '', 'array
 INSERT INTO `fa_config` VALUES (8, 'fixedpage', 'basic', 'Fixed page', '请尽量输入左侧菜单栏存在的链接', 'string', 'dashboard', '', 'required', '', '');
 INSERT INTO `fa_config` VALUES (9, 'categorytype', 'dictionary', 'Category type', '', 'array', '{\"default\":\"Default\",\"page\":\"Page\",\"article\":\"Article\",\"test\":\"Test\"}', '', '', '', '');
 INSERT INTO `fa_config` VALUES (10, 'configgroup', 'dictionary', 'Config group', '', 'array', '{\"basic\":\"Basic\",\"email\":\"Email\",\"dictionary\":\"Dictionary\",\"user\":\"User\",\"example\":\"Example\"}', '', '', '', '');
-INSERT INTO `fa_config` VALUES (11, 'mail_type', 'email', 'Mail type', '选择邮件发送方式', 'select', '1', '[\"请选择\",\"SMTP\",\"Mail\"]', '', '', '');
+INSERT INTO `fa_config` VALUES (11, 'mail_type', 'email', 'Mail type', '选择邮件发送方式', 'select', '1', '[\"请选择\",\"SMTP\"]', '', '', '');
 INSERT INTO `fa_config` VALUES (12, 'mail_smtp_host', 'email', 'Mail smtp host', '错误的配置发送邮件会导致服务器超时', 'string', 'smtp.qq.com', '', '', '', '');
 INSERT INTO `fa_config` VALUES (13, 'mail_smtp_port', 'email', 'Mail smtp port', '(不加密默认25,SSL默认465,TLS默认587)', 'string', '465', '', '', '', '');
 INSERT INTO `fa_config` VALUES (14, 'mail_smtp_user', 'email', 'Mail smtp user', '(填写完整用户名)', 'string', '10000', '', '', '', '');
-INSERT INTO `fa_config` VALUES (15, 'mail_smtp_pass', 'email', 'Mail smtp password', '(填写您的密码)', 'string', 'password', '', '', '', '');
+INSERT INTO `fa_config` VALUES (15, 'mail_smtp_pass', 'email', 'Mail smtp password', '(填写您的密码或授权码)', 'string', 'password', '', '', '', '');
 INSERT INTO `fa_config` VALUES (16, 'mail_verify_type', 'email', 'Mail vertify type', '(SMTP验证方式[推荐SSL])', 'select', '2', '[\"无\",\"TLS\",\"SSL\"]', '', '', '');
 INSERT INTO `fa_config` VALUES (17, 'mail_from', 'email', 'Mail from', '', 'string', '10000@qq.com', '', '', '', '');
 COMMIT;

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

@@ -15,7 +15,7 @@ use think\Exception;
  * 插件管理
  *
  * @icon   fa fa-cube
- * @remark 可在线安装、卸载、禁用、启用插件,同时支持添加本地插件。FastAdmin已上线插件商店 ,你可以发布你的免费或付费插件:<a href="https://www.fastadmin.net/store.html" target="_blank">https://www.fastadmin.net/store.html</a>
+ * @remark 可在线安装、卸载、禁用、启用、配置、升级插件,插件升级前请做好备份
  */
 class Addon extends Backend
 {

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

@@ -10,7 +10,7 @@ use fast\Tree;
  * 分类管理
  *
  * @icon   fa fa-list
- * @remark 用于统一管理网站的所有分类,分类可进行无限级分类,分类类型请在常规管理->系统配置->字典配置中添加
+ * @remark 用于管理网站的所有分类,分类可进行无限级分类,分类类型请在常规管理->系统配置->字典配置中添加
  */
 class Category extends Backend
 {

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

@@ -12,7 +12,7 @@ use think\Validate;
 /**
  * 管理员管理
  *
- * @icon fa fa-users
+ * @icon   fa fa-users
  * @remark 一个管理员可以有多个角色组,左侧的菜单根据管理员所拥有的权限进行生成
  */
 class Admin extends Backend
@@ -32,8 +32,8 @@ class Admin extends Backend
         parent::_initialize();
         $this->model = model('Admin');
 
-        $this->childrenAdminIds = $this->auth->getChildrenAdminIds(true);
-        $this->childrenGroupIds = $this->auth->getChildrenGroupIds(true);
+        $this->childrenAdminIds = $this->auth->getChildrenAdminIds($this->auth->isSuperAdmin());
+        $this->childrenGroupIds = $this->auth->getChildrenGroupIds($this->auth->isSuperAdmin());
 
         $groupList = collection(AuthGroup::where('id', 'in', $this->childrenGroupIds)->select())->toArray();
 
@@ -136,6 +136,10 @@ class Admin extends Backend
 
                 //过滤不允许的组别,避免越权
                 $group = array_intersect($this->childrenGroupIds, $group);
+                if (!$group) {
+                    $this->error(__('The parent group exceeds permission limit'));
+                }
+
                 $dataset = [];
                 foreach ($group as $value) {
                     $dataset[] = ['uid' => $this->model->id, 'group_id' => $value];
@@ -192,6 +196,9 @@ class Admin extends Backend
 
                 // 过滤不允许的组别,避免越权
                 $group = array_intersect($this->childrenGroupIds, $group);
+                if (!$group) {
+                    $this->error(__('The parent group exceeds permission limit'));
+                }
 
                 $dataset = [];
                 foreach ($group as $value) {

+ 5 - 5
application/admin/controller/auth/Adminlog.php

@@ -27,7 +27,7 @@ class Adminlog extends Backend
         $this->model = model('AdminLog');
 
         $this->childrenAdminIds = $this->auth->getChildrenAdminIds(true);
-        $this->childrenGroupIds = $this->auth->getChildrenGroupIds($this->auth->isSuperAdmin() ? true : false);
+        $this->childrenGroupIds = $this->auth->getChildrenGroupIds(true);
 
         $groupName = AuthGroup::where('id', 'in', $this->childrenGroupIds)
             ->column('id,name');
@@ -66,6 +66,9 @@ class Adminlog extends Backend
         if (!$row) {
             $this->error(__('No Results were found'));
         }
+        if (!$row['admin_id'] || !in_array($row['admin_id'], $this->childrenAdminIds)) {
+            $this->error(__('You have no permission'));
+        }
         $this->view->assign("row", $row->toArray());
         return $this->view->fetch();
     }
@@ -98,10 +101,7 @@ class Adminlog extends Backend
         }
         $ids = $ids ? $ids : $this->request->post("ids");
         if ($ids) {
-            $childrenGroupIds = $this->childrenGroupIds;
-            $adminList = $this->model->where('id', 'in', $ids)->where('admin_id', 'in', function ($query) use ($childrenGroupIds) {
-                $query->name('auth_group_access')->field('uid');
-            })->select();
+            $adminList = $this->model->where('id', 'in', $ids)->where('admin_id', 'in', $this->childrenAdminIds)->select();
             if ($adminList) {
                 $deleteIds = [];
                 foreach ($adminList as $k => $v) {

+ 14 - 17
application/admin/controller/auth/Group.php

@@ -24,6 +24,7 @@ class Group extends Backend
     //当前登录管理员所有子组别
     protected $childrenGroupIds = [];
     //当前组别列表数据
+    protected $grouplist = [];
     protected $groupdata = [];
     //无需要权限判断的方法
     protected $noNeedRight = ['roletree'];
@@ -38,20 +39,28 @@ class Group extends Backend
         $groupList = collection(AuthGroup::where('id', 'in', $this->childrenGroupIds)->select())->toArray();
 
         Tree::instance()->init($groupList);
-        $result = [];
+        $groupList = [];
         if ($this->auth->isSuperAdmin()) {
-            $result = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0));
+            $groupList = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0));
         } else {
             $groups = $this->auth->getGroups();
+            $groupIds = [];
             foreach ($groups as $m => $n) {
-                $result = array_merge($result, Tree::instance()->getTreeList(Tree::instance()->getTreeArray($n['pid'])));
+                if (in_array($n['id'], $groupIds) || in_array($n['pid'], $groupIds)) {
+                    continue;
+                }
+                $groupList = array_merge($groupList, Tree::instance()->getTreeList(Tree::instance()->getTreeArray($n['pid'])));
+                foreach ($groupList as $index => $item) {
+                    $groupIds[] = $item['id'];
+                }
             }
         }
         $groupName = [];
-        foreach ($result as $k => $v) {
+        foreach ($groupList as $k => $v) {
             $groupName[$v['id']] = $v['name'];
         }
 
+        $this->grouplist = $groupList;
         $this->groupdata = $groupName;
         $this->assignconfig("admin", ['id' => $this->auth->id, 'group_ids' => $this->auth->getGroupIds()]);
 
@@ -64,19 +73,7 @@ class Group extends Backend
     public function index()
     {
         if ($this->request->isAjax()) {
-            $list = AuthGroup::all(array_keys($this->groupdata));
-            $list = collection($list)->toArray();
-            $groupList = [];
-            foreach ($list as $k => $v) {
-                $groupList[$v['id']] = $v;
-            }
-            $list = [];
-            foreach ($this->groupdata as $k => $v) {
-                if (isset($groupList[$k])) {
-                    $groupList[$k]['name'] = $v;
-                    $list[] = $groupList[$k];
-                }
-            }
+            $list = $this->grouplist;
             $total = count($list);
             $result = array("total" => $total, "rows" => $list);
 

+ 3 - 0
application/admin/controller/auth/Rule.php

@@ -105,6 +105,9 @@ class Rule extends Backend
                 if (!$params['ismenu'] && !$params['pid']) {
                     $this->error(__('The non-menu rule must have parent'));
                 }
+                if ($params['pid'] == $row['id']) {
+                    $this->error(__('Can not change the parent to self'));
+                }
                 if ($params['pid'] != $row['pid']) {
                     $childrenIds = Tree::instance()->init(collection(AuthRule::select())->toArray())->getChildrenIds($row['id']);
                     if (in_array($params['pid'], $childrenIds)) {

+ 2 - 2
application/admin/controller/general/Config.php

@@ -256,8 +256,8 @@ class Config extends Backend
             $email = new Email;
             $result = $email
                 ->to($receiver)
-                ->subject(__("This is a test mail"))
-                ->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . __('This is a test mail content') . '</div>')
+                ->subject(__("This is a test mail", config('site.name')))
+                ->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . __('This is a test mail content', config('site.name')) . '</div>')
                 ->send();
             if ($result) {
                 $this->success();

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

@@ -194,9 +194,9 @@ return [
     'Third group 2'                                         => '三级管理组2',
     'Dashboard tips'                                        => '用于展示当前系统中的统计数据、统计报表及重要实时数据',
     'Config tips'                                           => '可以在此增改系统的变量和分组,也可以自定义分组和变量',
-    'Category tips'                                         => '用于统一管理网站的所有分类,分类可进行无限级分类,分类类型请在常规管理->系统配置->字典配置中添加',
+    'Category tips'                                         => '用于管理网站的所有分类,分类可进行无限级分类,分类类型请在常规管理->系统配置->字典配置中添加',
     'Attachment tips'                                       => '主要用于管理上传到服务器或第三方存储的数据',
-    'Addon tips'                                            => '可在线安装、卸载、禁用、启用插件,同时支持添加本地插件。',
+    'Addon tips'                                            => '可在线安装、卸载、禁用、启用、配置、升级插件,插件升级前请做好备份。',
     'Admin tips'                                            => '一个管理员可以有多个角色组,左侧的菜单根据管理员所拥有的权限进行生成',
     'Admin log tips'                                        => '管理员可以查看自己所拥有的权限的管理员日志',
     'Group tips'                                            => '角色组可以有多个,角色有上下级层级关系,如果子角色有角色组和管理员的权限则可以派生属于自己组别的下级角色组或管理员',

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

@@ -25,7 +25,7 @@ return [
     'Offline installed tips'                                  => '安装成功!清除浏览器缓存和框架缓存后生效!',
     'Online installed tips'                                   => '安装成功!清除浏览器缓存和框架缓存后生效!',
     'Not login tips'                                          => '你当前未登录FastAdmin,登录后将同步已购买的记录,下载时无需二次付费!',
-    'Please login and try to install'                         => '请登录后再进行离线安装!',
+    'Please login and try to install'                         => '请登录FastAdmin后再进行离线安装!',
     'Not installed tips'                                      => '请安装后再访问插件前台页面!',
     'Not enabled tips'                                        => '插件已经禁用,请启用后再访问插件前台页面!',
     'New version tips'                                        => '发现新版本:%s 点击查看更新日志',

+ 6 - 5
application/admin/lang/zh-cn/auth/admin.php

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

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

@@ -5,7 +5,7 @@ return [
     'The parent group can not found'                                       => '父组别未找到',
     'Group not found'                                                      => '组别未找到',
     'Can not change the parent to child'                                   => '父组别不能是它的子组别',
-    'Can not change the parent to self'                                    => '父组别不能是它的子组别',
+    'Can not change the parent to self'                                    => '父组别不能是它自己',
     'You can not delete group that contain child group and administrators' => '你不能删除含有子组和管理员的组',
     'The parent group exceeds permission limit'                            => '父组别超出权限范围',
     'The parent group can not be its own child or itself'                  => '父组别不能是它的子组别及本身',

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

@@ -15,6 +15,7 @@ return [
     'Menu tips'                                                 => '父级菜单无需匹配控制器和方法,子级菜单请使用控制器名',
     'Node tips'                                                 => '控制器/方法名,如果有目录请使用 目录名/控制器名/方法名',
     'The non-menu rule must have parent'                        => '非菜单规则节点必须有父级',
-    'Can not change the parent to child'                        => '父组别不能是它的子组别',
+    'Can not change the parent to child'                        => '父级不能是它的子级',
+    'Can not change the parent to self'                         => '父级不能是它自己',
     'Name only supports letters, numbers, underscore and slash' => 'URL规则只能是小写字母、数字、下划线和/组成',
 ];

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

@@ -70,8 +70,8 @@ return [
     'Name already exist'          => '变量名称已经存在',
     'Add new config'              => '点击添加新的配置',
     'Send a test message'         => '发送测试邮件',
-    'This is a test mail content' => '这是一封来自FastAdmin校验邮件,用于校验邮件配置是否正常!',
-    'This is a test mail'         => '这是一封来自FastAdmin的邮件',
+    'This is a test mail content' => '这是一封来自{sitename}校验邮件,用于校验邮件配置是否正常!',
+    'This is a test mail'         => '这是一封来自{sitename}的邮件',
     'Please input your email'     => '请输入测试接收者邮箱',
     'Please input correct email'  => '请输入正确的邮箱地址',
 ];

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

@@ -100,7 +100,7 @@ class Auth extends \fast\Auth
                 return false;
             }
             //token有变更
-            if ($key != md5(md5($id) . md5($keeptime) . md5($expiretime) . $admin->token)) {
+            if ($key != md5(md5($id) . md5($keeptime) . md5($expiretime) . $admin->token . config('token.key'))) {
                 return false;
             }
             $ip = request()->ip();
@@ -127,9 +127,9 @@ class Auth extends \fast\Auth
     {
         if ($keeptime) {
             $expiretime = time() + $keeptime;
-            $key = md5(md5($this->id) . md5($keeptime) . md5($expiretime) . $this->token);
+            $key = md5(md5($this->id) . md5($keeptime) . md5($expiretime) . $this->token . config('token.key'));
             $data = [$this->id, $keeptime, $expiretime, $key];
-            Cookie::set('keeplogin', implode('|', $data), 86400 * 30);
+            Cookie::set('keeplogin', implode('|', $data), 86400 * 7);
             return true;
         }
         return false;
@@ -292,8 +292,8 @@ class Auth extends \fast\Auth
                 break;
             }
             // 取出包含自己的所有子节点
-            $childrenList = Tree::instance()->init($groupList)->getChildren($v['id'], true);
-            $obj = Tree::instance()->init($childrenList)->getTreeArray($v['pid']);
+            $childrenList = Tree::instance()->init($groupList, 'pid')->getChildren($v['id'], true);
+            $obj = Tree::instance()->init($childrenList, 'pid')->getTreeArray($v['pid']);
             $objList = array_merge($objList, Tree::instance()->getTreeList($obj));
         }
         $childrenGroupIds = [];

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

@@ -160,7 +160,7 @@
                                             {/case}
                                             {case value="selectpage" break="0"}{/case}
                                             {case value="selectpages"}
-                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" id="c-{$item.name}" value="{$item.value|htmlentities}" class="form-control selectpage" data-source="{:url('general/config/selectpage')}?id={$item.id}" data-primary-key="{$item.setting.primarykey}" data-field="{$item.setting.field}" data-multiple="{$item.type=='selectpage'?'false':'true'}" data-tip="{$item.tip}" data-rule="{$item.rule}" />
+                                            <input {$item.extend_html} type="text" name="row[{$item.name}]" id="c-{$item.name}" value="{$item.value|htmlentities}" class="form-control selectpage" data-source="{:url('general.config/selectpage')}?id={$item.id}" data-primary-key="{$item.setting.primarykey}" data-field="{$item.setting.field}" data-multiple="{$item.type=='selectpage'?'false':'true'}" data-tip="{$item.tip}" data-rule="{$item.rule}" />
                                             {/case}
                                             {case custom}
                                             {$item.extend_html}

+ 63 - 7
application/common.php

@@ -85,7 +85,8 @@ if (!function_exists('cdnurl')) {
     function cdnurl($url, $domain = false)
     {
         $regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i";
-        $url = preg_match($regex, $url) ? $url : \think\Config::get('upload.cdnurl') . $url;
+        $cdnurl = \think\Config::get('upload.cdnurl');
+        $url = preg_match($regex, $url) || ($cdnurl && stripos($url, $cdnurl) === 0) ? $url : $cdnurl . $url;
         if ($domain && !preg_match($regex, $url)) {
             $domain = is_bool($domain) ? request()->domain() : $domain;
             $url = $domain . $url;
@@ -246,7 +247,7 @@ if (!function_exists('addtion')) {
                 $model = $v['name'] ? \think\Db::name($v['name']) : \think\Db::table($v['table']);
             }
             $primary = $v['primary'] ? $v['primary'] : $model->getPk();
-            $result[$v['field']] = $model->where($primary, 'in', $ids[$v['field']])->column("{$primary},{$v['column']}");
+            $result[$v['field']] = isset($ids[$v['field']]) ? $model->where($primary, 'in', $ids[$v['field']])->column("{$primary},{$v['column']}") : [];
         }
 
         foreach ($items as $k => &$v) {
@@ -265,13 +266,68 @@ if (!function_exists('addtion')) {
 if (!function_exists('var_export_short')) {
 
     /**
-     * 返回打印数组结构
-     * @param string $var 数组
+     * 使用短标签打印或返回数组结构
+     * @param mixed   $data
+     * @param boolean $return 是否返回数据
      * @return string
      */
-    function var_export_short($var)
+    function var_export_short($data, $return = true)
     {
-        return VarExporter::export($var);
+        return var_export($data, $return);
+        $replaced = [];
+        $count = 0;
+
+        //判断是否是对象
+        if (is_resource($data) || is_object($data)) {
+            return var_export($data, $return);
+        }
+
+        //判断是否有特殊的键名
+        $specialKey = false;
+        array_walk_recursive($data, function (&$value, &$key) use (&$specialKey) {
+            if (is_string($key) && (stripos($key, "\n") !== false || stripos($key, "array (") !== false)) {
+                $specialKey = true;
+            }
+        });
+        if ($specialKey) {
+            return var_export($data, $return);
+        }
+        array_walk_recursive($data, function (&$value, &$key) use (&$replaced, &$count, &$stringcheck) {
+            if (is_object($value) || is_resource($value)) {
+                $replaced[$count] = var_export($value, true);
+                $value = "##<{$count}>##";
+            } else {
+                if (is_string($value) && (stripos($value, "\n") !== false || stripos($value, "array (") !== false)) {
+                    $index = array_search($value, $replaced);
+                    if ($index === false) {
+                        $replaced[$count] = var_export($value, true);
+                        $value = "##<{$count}>##";
+                    } else {
+                        $value = "##<{$index}>##";
+                    }
+                }
+            }
+            $count++;
+        });
+
+        $dump = var_export($data, true);
+
+        $dump = preg_replace('#(?:\A|\n)([ ]*)array \(#i', '[', $dump); // Starts
+        $dump = preg_replace('#\n([ ]*)\),#', "\n$1],", $dump); // Ends
+        $dump = preg_replace('#=> \[\n\s+\],\n#', "=> [],\n", $dump); // Empties
+        $dump = preg_replace('#\)$#', "]", $dump); //End
+
+        if ($replaced) {
+            $dump = preg_replace_callback("/'##<(\d+)>##'/", function ($matches) use ($replaced) {
+                return isset($replaced[$matches[1]]) ? $replaced[$matches[1]] : "''";
+            }, $dump);
+        }
+
+        if ($return === true) {
+            return $dump;
+        } else {
+            echo $dump;
+        }
     }
 }
 
@@ -290,7 +346,7 @@ if (!function_exists('letter_avatar')) {
         $bg = "rgb({$r},{$g},{$b})";
         $color = "#ffffff";
         $first = mb_strtoupper(mb_substr($text, 0, 1));
-        $src = base64_encode('<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="100" width="100"><rect fill="' . $bg . '" x="0" y="0" width="100" height="100"></rect><text x="50" y="50" font-size="50" text-copy="fast" fill="' . $color . '" text-anchor="middle" text-rights="admin" alignment-baseline="central">' . $first . '</text></svg>');
+        $src = base64_encode('<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="100" width="100"><rect fill="' . $bg . '" x="0" y="0" width="100" height="100"></rect><text x="50" y="50" font-size="50" text-copy="fast" fill="' . $color . '" text-anchor="middle" text-rights="admin" dominant-baseline="central">' . $first . '</text></svg>');
         $value = 'data:image/svg+xml;base64,' . $src;
         return $value;
     }

+ 38 - 40
application/common/library/Email.php

@@ -3,6 +3,9 @@
 namespace app\common\library;
 
 use think\Config;
+use Tx\Mailer;
+use Tx\Mailer\Exceptions\CodeException;
+use Tx\Mailer\Exceptions\SendException;
 
 class Email
 {
@@ -20,14 +23,15 @@ class Email
     /**
      * 错误内容
      */
-    protected $_error = '';
+    protected $error = '';
 
     /**
      * 默认配置
      */
     public $options = [
-        'charset' => 'utf-8', //编码格式
-        'debug'   => false, //调式模式
+        'charset'   => 'utf-8', //编码格式
+        'debug'     => false, //调式模式
+        'mail_type' => 0, //状态
     ];
 
     /**
@@ -55,22 +59,12 @@ class Email
             $this->options = array_merge($this->options, $config);
         }
         $this->options = array_merge($this->options, $options);
-        $securArr = [1 => 'tls', 2 => 'ssl'];
-
-        $this->mail = new \PHPMailer\PHPMailer\PHPMailer(true);
-        $this->mail->CharSet = $this->options['charset'];
-        if ($this->options['mail_type'] == 1) {
-            $this->mail->SMTPDebug = $this->options['debug'];
-            $this->mail->isSMTP();
-            $this->mail->SMTPAuth = true;
-        } else {
-            $this->mail->isMail();
-        }
-        $this->mail->Host = $this->options['mail_smtp_host'];
-        $this->mail->Username = $this->options['mail_from'];
-        $this->mail->Password = $this->options['mail_smtp_pass'];
-        $this->mail->SMTPSecure = isset($securArr[$this->options['mail_verify_type']]) ? $securArr[$this->options['mail_verify_type']] : '';
-        $this->mail->Port = $this->options['mail_smtp_port'];
+        $secureArr = [0 => '', 1 => 'tls', 2 => 'ssl'];
+        $secure = isset($secureArr[$this->options['mail_verify_type']]) ? $secureArr[$this->options['mail_verify_type']] : '';
+
+        $this->mail = new Mailer(new Log);
+        $this->mail->setServer($this->options['mail_smtp_host'], $this->options['mail_smtp_port'], $secure);
+        $this->mail->setAuth($this->options['mail_from'], $this->options['mail_smtp_pass']);
 
         //设置发件人
         $this->from($this->options['mail_from'], $this->options['mail_smtp_user']);
@@ -83,7 +77,7 @@ class Email
      */
     public function subject($subject)
     {
-        $this->mail->Subject = $subject;
+        $this->mail->setSubject($subject);
         return $this;
     }
 
@@ -95,7 +89,7 @@ class Email
      */
     public function from($email, $name = '')
     {
-        $this->mail->setFrom($email, $name);
+        $this->mail->setFrom($name, $email);
         return $this;
     }
 
@@ -109,7 +103,7 @@ class Email
     {
         $emailArr = $this->buildAddress($email);
         foreach ($emailArr as $address => $name) {
-            $this->mail->addAddress($address, $name);
+            $this->mail->addTo($name, $address);
         }
 
         return $this;
@@ -124,6 +118,9 @@ class Email
     public function cc($email, $name = '')
     {
         $emailArr = $this->buildAddress($email);
+        if (count($emailArr) == 1 && $name) {
+            $emailArr[key($emailArr)] = $name;
+        }
         foreach ($emailArr as $address => $name) {
             $this->mail->addCC($address, $name);
         }
@@ -139,8 +136,11 @@ class Email
     public function bcc($email, $name = '')
     {
         $emailArr = $this->buildAddress($email);
+        if (count($emailArr) == 1 && $name) {
+            $emailArr[key($emailArr)] = $name;
+        }
         foreach ($emailArr as $address => $name) {
-            $this->mail->addBCC($address, $name);
+            $this->mail->addBCC($name, $address);
         }
         return $this;
     }
@@ -153,11 +153,7 @@ class Email
      */
     public function message($body, $ishtml = true)
     {
-        if ($ishtml) {
-            $this->mail->msgHTML($body);
-        } else {
-            $this->mail->Body = $body;
-        }
+        $this->mail->setBody($body);
         return $this;
     }
 
@@ -169,7 +165,7 @@ class Email
      */
     public function attachment($path, $name = '')
     {
-        $this->mail->addAttachment($path, $name);
+        $this->mail->addAttachment($name, $path);
         return $this;
     }
 
@@ -180,13 +176,8 @@ class Email
      */
     protected function buildAddress($emails)
     {
-        $emails = is_array($emails) ? $emails : explode(',', str_replace(";", ",", $emails));
-        $result = [];
-        foreach ($emails as $key => $value) {
-            $email = is_numeric($key) ? $value : $key;
-            $result[$email] = is_numeric($key) ? "" : $value;
-        }
-        return $result;
+        $emails = is_array($emails) ? $emails : array_flip(explode(',', str_replace(";", ",", $emails)));
+        return $emails;
     }
 
     /**
@@ -195,7 +186,7 @@ class Email
      */
     public function getError()
     {
-        return $this->_error;
+        return $this->error;
     }
 
     /**
@@ -204,7 +195,7 @@ class Email
      */
     protected function setError($error)
     {
-        $this->_error = $error;
+        $this->error = $error;
     }
 
     /**
@@ -217,11 +208,18 @@ class Email
         if (in_array($this->options['mail_type'], [1, 2])) {
             try {
                 $result = $this->mail->send();
-            } catch (\PHPMailer\PHPMailer\Exception $e) {
+            } catch (SendException $e) {
+                $this->setError($e->getCode() . $e->getMessage());
+            } catch (CodeException $e) {
+                preg_match_all("/Expected: (\d+)\, Got: (\d+)( \| (.*))?\$/i", $e->getMessage(), $matches);
+                $code = isset($matches[2][3]) ? $matches[2][3] : 0;
+                $message = isset($matches[2][0]) ? $matches[4][0] : $e->getMessage();
+                $this->setError($message);
+            } catch (\Exception $e) {
                 $this->setError($e->getMessage());
             }
 
-            $this->setError($result ? '' : $this->mail->ErrorInfo);
+            $this->setError($result ? '' : $this->getError());
         } else {
             //邮件功能已关闭
             $this->setError(__('Mail already closed'));

+ 1 - 0
application/common/model/Config.php

@@ -183,6 +183,7 @@ class Config extends Model
             'mimetype'  => $uploadcfg['mimetype'],
             'chunking'  => $uploadcfg['chunking'],
             'chunksize' => $uploadcfg['chunksize'],
+            'savekey'   => $uploadcfg['savekey'],
             'multipart' => [],
             'multiple'  => $uploadcfg['multiple'],
             'storage'   => 'local'

+ 3 - 1
application/config.php

@@ -51,6 +51,8 @@ return [
     'class_suffix'           => false,
     // 控制器类后缀
     'controller_suffix'      => false,
+    // 获取IP的变量
+    'http_agent_ip'          => 'REMOTE_ADDR',
     // +----------------------------------------------------------------------
     // | 模块设置
     // +----------------------------------------------------------------------
@@ -290,7 +292,7 @@ return [
         //允许跨域的域名,多个以,分隔
         'cors_request_domain'   => 'localhost,127.0.0.1',
         //版本号
-        'version'               => '1.2.0.20201008_beta',
+        'version'               => '1.2.0.20210125_beta',
         //API接口地址
         'api_url'               => 'https://api.fastadmin.net',
     ],

+ 1 - 1
application/extra/upload.php

@@ -21,7 +21,7 @@ return [
     /**
      * 可上传的文件类型
      */
-    'mimetype'  => 'jpg,png,bmp,jpeg,gif,zip,rar,xls,xlsx',
+    'mimetype'  => 'jpg,png,bmp,jpeg,gif,zip,rar,xls,xlsx,wav,mp4,mp3,pdf',
     /**
      * 是否支持批量上传
      */

+ 17 - 2
application/index/controller/User.php

@@ -65,7 +65,7 @@ class User extends Frontend
      */
     public function register()
     {
-        $url = $this->request->request('url', '');
+        $url = $this->request->request('url', '', 'trim');
         if ($this->auth->id) {
             $this->success(__('You\'ve logged in, do not login again'), $url ? $url : url('user/index'));
         }
@@ -144,7 +144,7 @@ class User extends Frontend
      */
     public function login()
     {
-        $url = $this->request->request('url', '');
+        $url = $this->request->request('url', '', 'trim');
         if ($this->auth->id) {
             $this->success(__('You\'ve logged in, do not login again'), $url ? $url : url('user/index'));
         }
@@ -267,6 +267,7 @@ class User extends Frontend
         $this->request->filter(['strip_tags']);
         if ($this->request->isAjax()) {
             $mimetypeQuery = [];
+            $where = [];
             $filter = $this->request->request('filter');
             $filterArr = (array)json_decode($filter, true);
             if (isset($filterArr['mimetype']) && preg_match("/[]\,|\*]/", $filterArr['mimetype'])) {
@@ -281,17 +282,31 @@ class User extends Frontend
                         }
                     }
                 };
+            } elseif (isset($filterArr['mimetype'])) {
+                $where['mimetype'] = ['like', '%' . $filterArr['mimetype'] . '%'];
             }
+
+            if (isset($filterArr['filename'])) {
+                $where['filename'] = ['like', '%' . $filterArr['filename'] . '%'];
+            }
+
+            if (isset($filterArr['createtime'])) {
+                $timeArr = explode(' - ', $filterArr['createtime']);
+                $where['createtime'] = ['between', [strtotime($timeArr[0]), strtotime($timeArr[1])]];
+            }
+
             $model = new Attachment();
             $offset = $this->request->get("offset", 0);
             $limit = $this->request->get("limit", 0);
             $total = $model
+                ->where($where)
                 ->where($mimetypeQuery)
                 ->where('user_id', $this->auth->id)
                 ->order("id", "DESC")
                 ->count();
 
             $list = $model
+                ->where($where)
                 ->where($mimetypeQuery)
                 ->where('user_id', $this->auth->id)
                 ->order("id", "DESC")

+ 1 - 1
bower.json

@@ -27,7 +27,7 @@
     "fastadmin-cxselect": "~1.4.0",
     "fastadmin-dragsort": "~1.0.0",
     "fastadmin-addtabs": "~1.0.5",
-    "fastadmin-selectpage": "~1.0.0",
+    "fastadmin-selectpage": "~1.0.6",
     "fastadmin-layer": "~3.1.2",
     "bootstrap-slider": "*"
   }

+ 3 - 4
composer.json

@@ -15,19 +15,18 @@
         }
     ],
     "require": {
-        "php": ">=7.0.0",
+        "php": ">=7.1.0",
         "topthink/framework": "~5.0.24",
         "topthink/think-captcha": "^1.0",
-        "phpmailer/phpmailer": "~6.1.6",
         "karsonzhang/fastadmin-addons": "~1.2.4",
         "overtrue/pinyin": "~3.0",
         "phpoffice/phpspreadsheet": "^1.2",
         "overtrue/wechat": "4.2.11",
         "nelexa/zip": "^3.3",
-        "symfony/var-exporter": "^4.4.13",
         "ext-json": "*",
         "ext-curl": "*",
-        "ext-pdo": "*"
+        "ext-pdo": "*",
+        "txthinking/mailer": "^2.0"
     },
     "config": {
         "preferred-install": "dist"

File diff suppressed because it is too large
+ 866 - 100
public/api.html


File diff suppressed because it is too large
+ 1 - 1
public/assets/css/backend.min.css


File diff suppressed because it is too large
+ 1 - 1
public/assets/css/frontend.min.css


+ 20 - 0
public/assets/js/backend/addon.js

@@ -180,6 +180,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     Toastr.success(ret.msg);
                     operate(data.addon.name, 'enable', false);
                     return false;
+                }, function (data, ret) {
+                    if (ret.msg && ret.msg.match(/(login|登录)/g)) {
+                        return Layer.alert(ret.msg, {
+                            title: __('Warning'),
+                            btn: [__('Login now')],
+                            yes: function (index, layero) {
+                                $(".btn-userinfo").trigger("click");
+                            }
+                        });
+                    }
                 });
             });
 
@@ -262,7 +272,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                             return false;
                         },
                         success: function (layero, index) {
+                            this.checkEnterKey = function(event){
+                                if(event.keyCode === 13){
+                                    $(".layui-layer-btn0").trigger("click");
+                                    return false;
+                                }
+                            };
+                            $(document).on('keydown', this.checkEnterKey);
                             $(".layui-layer-btn1", layero).prop("href", "http://www.fastadmin.net/user/register.html").prop("target", "_blank");
+                        },
+                        end: function(){
+                            $(document).off('keydown', this.checkEnterKey);
                         }
                     });
                 } else {

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

@@ -103,7 +103,8 @@ define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang'], function ($, undefine
             //获取修复后可访问的cdn链接
             cdnurl: function (url, domain) {
                 var rule = new RegExp("^((?:[a-z]+:)?\\/\\/|data:image\\/)", "i");
-                var url = rule.test(url) ? url : Config.upload.cdnurl + url;
+                var cdnurl = Config.upload.cdnurl;
+                url = rule.test(url) || (cdnurl && url.indexOf(cdnurl) === 0) ? url : cdnurl + url;
                 if (domain && !rule.test(url)) {
                     domain = typeof domain === 'string' ? domain : location.origin;
                     url = domain + url;

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

@@ -140,25 +140,22 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
                     showExport: false,
                     columns: [
                         [
-                            {field: 'state', checkbox: multiple, visible: multiple, operate:false},
-                            {field: 'id', title: __('Id')},
-                            {field: 'url', title: __('Preview'), formatter: function (value, row, index) {
+                            {field: 'state', checkbox: multiple, visible: multiple, operate: false},
+                            {field: 'id', title: __('Id'), operate: false},
+                            {
+                                field: 'url', title: __('Preview'), formatter: function (value, row, index) {
                                     if (row.mimetype.indexOf("image") > -1) {
                                         var style = row.storage === 'upyun' ? '!/fwfh/120x90' : '';
                                         return '<a href="' + row.fullurl + '" target="_blank"><img src="' + row.fullurl + style + '" alt="" style="max-height:90px;max-width:120px"></a>';
                                     } else {
                                         return '<a href="' + row.fullurl + '" target="_blank"><img src="' + Fast.api.fixurl("ajax/icon") + "?suffix=" + row.imagetype + '" alt="" style="max-height:90px;max-width:120px"></a>';
                                     }
-                                }, operate: false},
+                                }, operate: false
+                            },
                             {field: 'filename', title: __('Filename'), formatter: Table.api.formatter.search, operate: 'like'},
                             {field: 'imagewidth', title: __('Imagewidth'), operate: false},
                             {field: 'imageheight', title: __('Imageheight'), operate: false},
-                            {
-                                field: 'mimetype', title: __('Mimetype'), operate: 'LIKE %...%',
-                                process: function (value, arg) {
-                                    return value.replace(/\*/g, '%');
-                                }
-                            },
+                            {field: 'mimetype', title: __('Mimetype'), formatter: Table.api.formatter.search},
                             {field: 'createtime', title: __('Createtime'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true},
                             {
                                 field: 'operate', title: __('Operate'), events: {

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

@@ -121,7 +121,7 @@ require.config({
             'css': '../libs/require-css/css.min'
         }
     },
-    waitSeconds: 30,
+    waitSeconds: 60,
     charset: 'utf-8' // 文件编码
 });
 

File diff suppressed because it is too large
+ 15307 - 17
public/assets/js/require-backend.min.js


+ 13 - 10
public/assets/js/require-form.js

@@ -335,7 +335,7 @@ define(['jquery', 'bootstrap', 'upload', 'validator', 'validator-lang'], functio
                             row = row ? row : {};
                             var vars = {index: index, name: name, data: data, row: row};
                             var html = template ? Template(template, vars) : Template.render(Form.config.fieldlisttpl, vars);
-                            $(html).insertBefore($(tagName + ":last", container));
+                            $(html).attr("fieldlist-item", true).insertBefore($(tagName + ":last", container));
                             $(this).trigger("fa.event.appendfieldlist", $(this).closest(tagName).prev());
                         });
                         //移除控制
@@ -362,17 +362,20 @@ define(['jquery', 'bootstrap', 'upload', 'validator', 'validator-lang'], functio
                                 return true;
                             }
                             var template = $(this).data("template");
-                            var json = {};
-                            try {
-                                json = JSON.parse(textarea.val());
-                            } catch (e) {
-                            }
-                            $.each(json, function (i, j) {
-                                $(".btn-append,.append", container).trigger('click', template ? j : {
-                                    key: i,
-                                    value: j
+                            textarea.on("fa.event.refreshfieldlist", function () {
+                                $("[fieldlist-item]", container).remove();
+                                var json = {};
+                                try {
+                                    json = JSON.parse($(this).val());
+                                } catch (e) {
+                                }
+                                $.each(json, function (i, j) {
+                                    $(".btn-append,.append", container).trigger('click', template ? j : {
+                                        key: i, value: j
+                                    });
                                 });
                             });
+                            textarea.trigger("fa.event.refreshfieldlist");
                         });
                     });
                 }

+ 1 - 1
public/assets/js/require-frontend.js

@@ -120,7 +120,7 @@ require.config({
             'css': '../libs/require-css/css.min'
         }
     },
-    waitSeconds: 30,
+    waitSeconds: 60,
     charset: 'utf-8' // 文件编码
 });
 

File diff suppressed because it is too large
+ 14915 - 17
public/assets/js/require-frontend.min.js


+ 65 - 20
public/assets/js/require-table.js

@@ -48,6 +48,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
             iosCardView: true, //ios卡片视图
             checkOnInit: true, //是否在初始化时判断
             escape: true, //是否对内容进行转义
+            fixDropdownPosition: true, //是否修复下拉的定位
             selectedIds: [],
             selectedData: [],
             extend: {
@@ -253,8 +254,9 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 var exportDataType = options.exportDataType;
                 // 处理选中筛选框后按钮的状态统一变更
                 table.on('check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table post-body.bs.table', function (e) {
-                    var allIds = table.bootstrapTable("getData").map(function (item) {
-                        return item[options.pk];
+                    var allIds = [];
+                    $.each(table.bootstrapTable("getData"), function (i, item) {
+                        allIds.push(typeof item[options.pk] != 'undefined' ? item[options.pk] : '');
                     });
                     var selectedIds = Table.api.selectedids(table, true),
                         selectedData = Table.api.selecteddata(table, true);
@@ -307,7 +309,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     if (url.indexOf("{ids}") !== -1) {
                         url = Table.api.replaceurl(url, {ids: ids.length > 0 ? ids.join(",") : 0}, table);
                     }
-                    Fast.api.open(url, __('Add'), $(this).data() || {});
+                    Fast.api.open(url, $(this).data("original-title") || $(this).attr("title") || __('Add'), $(this).data() || {});
                 });
                 // 导入按钮事件
                 if ($(Table.config.importbtn, toolbar).size() > 0) {
@@ -330,12 +332,15 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     if (ids.length > 10) {
                         return;
                     }
+                    var title = $(that).data('title') || $(that).attr("title") || __('Edit');
+                    var data = $(that).data() || {};
+                    delete data.title;
                     //循环弹出多个编辑框
                     $.each(Table.api.selecteddata(table), function (index, row) {
                         var url = options.extend.edit_url;
                         row = $.extend({}, row ? row : {}, {ids: row[options.pk]});
                         url = Table.api.replaceurl(url, row, table);
-                        Fast.api.open(url, __('Edit'), $(that).data() || {});
+                        Fast.api.open(url, typeof title === 'function' ? title.call(table, row) : title, data);
                     });
                 });
                 //清空回收站
@@ -472,7 +477,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     var row = Table.api.getrowbyid(table, ids);
                     row.ids = ids;
                     var url = Table.api.replaceurl(options.extend.edit_url, row, table);
-                    Fast.api.open(url, __('Edit'), $(this).data() || {});
+                    Fast.api.open(url, $(this).data("original-title") || $(this).attr("title") || __('Edit'), $(this).data() || {});
                 });
                 table.on("click", "[data-id].btn-del", function (e) {
                     e.preventDefault();
@@ -487,6 +492,45 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                         }
                     );
                 });
+
+                //修复dropdown定位溢出的情况
+                if (options.fixDropdownPosition) {
+                    var tableBody = table.closest(".fixed-table-body");
+                    table.on('show.bs.dropdown fa.event.refreshdropdown', ".btn-group", function (e) {
+                        var dropdownMenu = $(".dropdown-menu", this);
+                        var btnGroup = $(this);
+                        var isPullRight = dropdownMenu.hasClass("pull-right") || dropdownMenu.hasClass("dropdown-menu-right");
+                        var left, top, position;
+                        if (dropdownMenu.outerHeight() + btnGroup.outerHeight() > tableBody.outerHeight() - 41) {
+                            position = 'fixed';
+                            top = btnGroup.offset().top - $(window).scrollTop() + btnGroup.outerHeight();
+                            left = isPullRight ? btnGroup.offset().left + btnGroup.outerWidth() - dropdownMenu.outerWidth() : btnGroup.offset().left;
+                        } else {
+                            if (btnGroup.offset().top + btnGroup.outerHeight() + dropdownMenu.outerHeight() > tableBody.offset().top + tableBody.outerHeight() - 30) {
+                                position = 'absolute';
+                                left = isPullRight ? -(dropdownMenu.outerWidth() - btnGroup.outerWidth()) : 0;
+                                top = -(dropdownMenu.outerHeight() + 3);
+                            }
+                        }
+                        if (left || top) {
+                            dropdownMenu.css({
+                                position: position, left: left, top: top, right: 'inherit'
+                            });
+                        }
+                    });
+                    var checkdropdown = function () {
+                        if ($(".btn-group.open", table).length > 0 && $(".btn-group.open .dropdown-menu", table).css("position") == 'fixed') {
+                            $(".btn-group.open", table).trigger("fa.event.refreshdropdown");
+                        }
+                    };
+                    $(window).on("scroll", function () {
+                        checkdropdown();
+                    });
+                    tableBody.on("scroll", function () {
+                        checkdropdown();
+                    });
+                }
+
                 var id = table.attr("id");
                 Table.list[id] = table;
                 return table;
@@ -528,7 +572,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                         var ids = row[options.pk];
                         row = $.extend({}, row ? row : {}, {ids: ids});
                         var url = options.extend.edit_url;
-                        Fast.api.open(Table.api.replaceurl(url, row, table), __('Edit'), $(this).data() || {});
+                        Fast.api.open(Table.api.replaceurl(url, row, table), $(this).data("original-title") || $(this).attr("title") || __('Edit'), $(this).data() || {});
                     },
                     'click .btn-delone': function (e, value, row, index) {
                         e.stopPropagation();
@@ -557,8 +601,9 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 image: {
                     'click .img-center': function (e, value, row, index) {
                         var data = [];
-                        value = value.toString().split(",");
-                        $.each(value, function (index, value) {
+                        value = value === null ? '' : value.toString();
+                        var arr = value != '' ? split(",") : [];
+                        $.each(arr, function (index, value) {
                             data.push({
                                 src: Fast.api.cdnurl(value),
                             });
@@ -576,22 +621,21 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
             // 单元格数据格式化
             formatter: {
                 icon: function (value, row, index) {
-                    if (!value)
-                        return '';
                     value = value === null ? '' : value.toString();
                     value = value.indexOf(" ") > -1 ? value : "fa fa-" + value;
                     //渲染fontawesome图标
                     return '<i class="' + value + '"></i> ' + value;
                 },
                 image: function (value, row, index) {
+                    value = value == null || value.length === 0 ? '' : value.toString();
                     value = value ? value : '/assets/img/blank.gif';
                     var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
                     return '<a href="javascript:"><img class="' + classname + '" src="' + Fast.api.cdnurl(value) + '" /></a>';
                 },
                 images: function (value, row, index) {
-                    value = value === null ? '' : value.toString();
+                    value = value == null || value.length === 0 ? '' : value.toString();
                     var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
-                    var arr = value.split(',');
+                    var arr = value != '' ? value.split(',') : [];
                     var html = [];
                     $.each(arr, function (i, value) {
                         value = value ? value : '/assets/img/blank.gif';
@@ -618,7 +662,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     if (typeof this.custom !== 'undefined') {
                         custom = $.extend(custom, this.custom);
                     }
-                    value = value === null ? '' : value.toString();
+                    value = value == null || value.length === 0 ? '' : value.toString();
                     var keys = typeof this.searchList === 'object' ? Object.keys(this.searchList) : [];
                     var index = keys.indexOf(value);
                     var color = value && typeof custom[value] !== 'undefined' ? custom[value] : null;
@@ -656,7 +700,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                         + row[pk] + "' " + (url ? "data-url='" + url + "'" : "") + (confirm ? "data-confirm='" + confirm + "'" : "") + " data-params='" + this.field + "=" + (value == yes ? no : yes) + "'><i class='fa fa-toggle-on text-success text-" + color + " " + (value == yes ? '' : 'fa-flip-horizontal text-gray') + " fa-2x'></i></a>";
                 },
                 url: function (value, row, index) {
-                    value = value === null ? '' : value.toString();
+                    value = value == null || value.length === 0 ? '' : value.toString();
                     return '<div class="input-group input-group-sm" style="width:250px;margin:0 auto;"><input type="text" class="form-control input-sm" value="' + value + '"><span class="input-group-btn input-group-sm"><a href="' + value + '" target="_blank" class="btn btn-default btn-sm"><i class="fa fa-link"></i></a></span></div>';
                 },
                 search: function (value, row, index) {
@@ -668,18 +712,18 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                     return '<a href="javascript:;" class="searchit" data-toggle="tooltip" title="' + __('Click to search %s', value) + '" data-field="' + field + '" data-value="' + value + '">' + value + '</a>';
                 },
                 addtabs: function (value, row, index) {
-                    var url = Table.api.replaceurl(this.url, row, this.table);
+                    var url = Table.api.replaceurl(this.url || '', row, this.table);
                     var title = this.atitle ? this.atitle : __("Search %s", value);
                     return '<a href="' + Fast.api.fixurl(url) + '" class="addtabsit" data-value="' + value + '" title="' + title + '">' + value + '</a>';
                 },
                 dialog: function (value, row, index) {
-                    var url = Table.api.replaceurl(this.url, row, this.table);
+                    var url = Table.api.replaceurl(this.url || '', row, this.table);
                     var title = this.atitle ? this.atitle : __("View %s", value);
                     return '<a href="' + Fast.api.fixurl(url) + '" class="dialogit" data-value="' + value + '" title="' + title + '">' + value + '</a>';
                 },
                 flag: function (value, row, index) {
                     var that = this;
-                    value = value === null ? '' : value.toString();
+                    value = value == null || value.length === 0 ? '' : value.toString();
                     var colorArr = {index: 'success', hot: 'warning', recommend: 'danger', 'new': 'info'};
                     //如果字段列有定义custom
                     if (typeof this.custom !== 'undefined') {
@@ -693,10 +737,10 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
 
                     //渲染Flag
                     var html = [];
-                    var arr = value.split(',');
+                    var arr = value != '' ? value.split(',') : [];
                     var color, display, label;
                     $.each(arr, function (i, value) {
-                        value = value === null ? '' : value.toString();
+                        value = value == null || value.length === 0 ? '' : value.toString();
                         if (value == '')
                             return true;
                         color = value && typeof colorArr[value] !== 'undefined' ? colorArr[value] : 'primary';
@@ -809,7 +853,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 if (!$.isEmptyObject(dropdowns)) {
                     var dropdownHtml = [];
                     $.each(dropdowns, function (i, j) {
-                        dropdownHtml.push('<div class="btn-group"><button type="button" class="btn btn-primary dropdown-toggle btn-xs" data-toggle="dropdown">' + i + '</button><button type="button" class="btn btn-primary dropdown-toggle btn-xs" data-toggle="dropdown"><span class="caret"></span></button><ul class="dropdown-menu pull-right"><li>' + j.join('</li><li>') + '</li></ul></div>');
+                        dropdownHtml.push('<div class="btn-group"><button type="button" class="btn btn-primary dropdown-toggle btn-xs" data-toggle="dropdown">' + i + '</button><button type="button" class="btn btn-primary dropdown-toggle btn-xs" data-toggle="dropdown"><span class="caret"></span></button><ul class="dropdown-menu dropdown-menu-right"><li>' + j.join('</li><li>') + '</li></ul></div>');
                     });
                     html.unshift(dropdownHtml);
                 }
@@ -820,6 +864,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 var options = table ? table.bootstrapTable('getOptions') : null;
                 var ids = options ? row[options.pk] : 0;
                 row.ids = ids ? ids : (typeof row.ids !== 'undefined' ? row.ids : 0);
+                url = url == null || url.length === 0 ? '' : url.toString();
                 //自动添加ids参数
                 url = !url.match(/\{ids\}/i) ? url + (url.match(/(\?|&)+/) ? "&ids=" : "/ids/") + '{ids}' : url;
                 url = url.replace(/\{(.*?)\}/gi, function (matched) {

+ 6 - 3
public/assets/js/require-upload.js

@@ -141,7 +141,7 @@ define(['jquery', 'bootstrap', 'dropzone', 'template'], function ($, undefined,
                         //上传URL
                         url = url ? url : Config.upload.uploadurl;
                         url = Fast.api.fixurl(url);
-                        var chunking = false, chunkSize = Config.upload.chunksize || 2097152;
+                        var chunking = false, chunkSize = Config.upload.chunksize || 2097152, timeout = Config.upload.timeout || 600000;
 
                         //最大可上传文件大小
                         maxsize = typeof maxsize !== "undefined" ? maxsize : Config.upload.maxsize;
@@ -198,6 +198,7 @@ define(['jquery', 'bootstrap', 'dropzone', 'template'], function ($, undefined,
                             maxFilesize: maxFilesize,
                             acceptedFiles: mimetype,
                             maxFiles: (maxcount && parseInt(maxcount) > 1 ? maxcount : (multiple ? null : 1)),
+                            timeout: timeout,
                             previewsContainer: false,
                             dictDefaultMessage: __("Drop files here to upload"),
                             dictFallbackMessage: __("Your browser does not support drag'n'drop file uploads"),
@@ -243,7 +244,7 @@ define(['jquery', 'bootstrap', 'dropzone', 'template'], function ($, undefined,
                                 }
                             },
                             error: function (file, response, xhr) {
-                                var responseObj = $("<div>" + xhr.responseText + "</div>");
+                                var responseObj = $("<div>" + (xhr && typeof xhr.responseText !== 'undefined' ? xhr.responseText : response) + "</div>");
                                 responseObj.find("style, title, script").remove();
                                 var ret = {code: 0, data: null, msg: responseObj.text()};
                                 Upload.events.onUploadError(this, ret, file);
@@ -343,10 +344,12 @@ define(['jquery', 'bootstrap', 'dropzone', 'template'], function ($, undefined,
                                     }
                                     var suffix = /[\.]?([a-zA-Z0-9]+)$/.exec(j);
                                     suffix = suffix ? suffix[1] : 'file';
-                                    var data = {url: j, fullurl: Fast.api.cdnurl(j), data: $(that).data(), key: i, index: i, value: (json && typeof json[i] !== 'undefined' ? json[i] : null), suffix: suffix};
+                                    var value = (json && typeof json[i] !== 'undefined' ? json[i] : null);
+                                    var data = {url: j, fullurl: Fast.api.cdnurl(j), data: $(that).data(), key: i, index: i, value: value, row: value, suffix: suffix};
                                     var html = tpl ? Template(tpl, data) : Template.render(Upload.config.previewtpl, data);
                                     $("#" + preview_id).append(html);
                                 });
+                                refresh($("#" + preview_id).data("name"));
                             });
                             $("#" + input_id).trigger("change");
                         }