瀏覽代碼

新增CRUD一键生成筛选选项卡功能
新增一键生成API文档导航列表功能
优化后台默认加载页的响应速度
优化二级栏目挺拽排序功能
修复后台部分列表会被截断的BUG
修复一键生成API文档@ApiInternal不生效的BUG
修复一键生成API文档空类的BUG
修复一键生成CRUD模型不正在字段的BUG

Karson 6 年之前
父節點
當前提交
9753d94bc0
共有 46 個文件被更改,包括 1259 次插入757 次删除
  1. 1 0
      application/admin/command/Api.php
  2. 1 0
      application/admin/command/Api/lang/zh-cn.php
  3. 23 32
      application/admin/command/Api/library/Builder.php
  4. 83 160
      application/admin/command/Api/library/Extractor.php
  5. 89 2
      application/admin/command/Api/template/index.html
  6. 20 0
      application/admin/command/Crud.php
  7. 10 0
      application/admin/command/Crud/stubs/html/heading-html.stub
  8. 18 0
      application/admin/command/Crud/stubs/html/heading-js.stub
  9. 1 1
      application/admin/command/Crud/stubs/index.stub
  10. 2 0
      application/admin/command/Crud/stubs/javascript.stub
  11. 1 1
      application/admin/command/Crud/stubs/mixins/checkbox.stub
  12. 1 1
      application/admin/command/Crud/stubs/mixins/datetime.stub
  13. 1 1
      application/admin/command/Crud/stubs/mixins/multiple.stub
  14. 1 1
      application/admin/command/Crud/stubs/mixins/radio.stub
  15. 1 1
      application/admin/command/Crud/stubs/mixins/select.stub
  16. 20 34
      application/admin/controller/Ajax.php
  17. 3 0
      application/admin/controller/Category.php
  18. 3 1
      application/admin/controller/Index.php
  19. 3 0
      application/admin/controller/auth/Admin.php
  20. 3 0
      application/admin/controller/auth/Adminlog.php
  21. 3 0
      application/admin/controller/auth/Group.php
  22. 3 0
      application/admin/controller/auth/Rule.php
  23. 3 0
      application/admin/controller/general/Attachment.php
  24. 3 0
      application/admin/controller/general/Config.php
  25. 1 1
      application/admin/controller/user/Group.php
  26. 2 1
      application/admin/controller/user/Rule.php
  27. 2 1
      application/admin/controller/user/User.php
  28. 2 0
      application/admin/lang/zh-cn.php
  29. 15 3
      application/admin/library/Auth.php
  30. 6 0
      application/admin/library/traits/Backend.php
  31. 1 1
      application/admin/view/category/index.html
  32. 6 0
      application/admin/view/common/header.html
  33. 11 2
      application/admin/view/index/index.html
  34. 1 1
      application/admin/view/user/group/index.html
  35. 28 4
      application/api/controller/Demo.php
  36. 1 1
      application/common/controller/Backend.php
  37. 1 1
      bower.json
  38. 756 381
      public/api.html
  39. 8 3
      public/assets/css/backend.css
  40. 1 1
      public/assets/css/backend.min.css
  41. 68 73
      public/assets/js/adminlte.js
  42. 1 1
      public/assets/js/backend/category.js
  43. 26 27
      public/assets/js/backend/dashboard.js
  44. 0 5
      public/assets/js/backend/index.js
  45. 15 13
      public/assets/js/require-backend.min.js
  46. 10 2
      public/assets/less/backend.less

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

@@ -93,6 +93,7 @@ class Api extends Command
                 $classes[] = $this->get_class_from_file($filePath);
             }
         }
+        $classes = array_unique(array_filter($classes));
 
         $config = [
             'title'       => $title,

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

@@ -12,6 +12,7 @@ return [
     'Required'         => '必选',
     'Description'      => '描述',
     'Send'             => '提交',
+    'Reset'            => '重置',
     'Tokentips'        => 'Token在会员注册或登录后都会返回,WEB端同时存在于Cookie中',
     'Apiurltips'       => 'API接口URL',
     'Savetips'         => '点击保存后Token和Api url都将保存在本地Localstorage中',

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

@@ -14,18 +14,18 @@ class Builder
 
     /**
      *
-     * @var \think\View 
+     * @var \think\View
      */
     public $view = null;
 
     /**
      * parse classes
-     * @var array 
+     * @var array
      */
     protected $classes = [];
 
     /**
-     * 
+     *
      * @param array $classes
      */
     public function __construct($classes = [])
@@ -37,8 +37,12 @@ class Builder
     protected function extractAnnotations()
     {
         $st_output = [];
-        foreach ($this->classes as $class)
-        {
+        foreach ($this->classes as $class) {
+            $classAnnotation = Extractor::getClassAnnotations($class);
+            // 如果忽略
+            if (isset($classAnnotation['ApiInternal'])) {
+                continue;
+            }
             $st_output[] = Extractor::getAllClassAnnotations($class);
         }
         return end($st_output);
@@ -46,14 +50,12 @@ class Builder
 
     protected function generateHeadersTemplate($docs)
     {
-        if (!isset($docs['ApiHeaders']))
-        {
+        if (!isset($docs['ApiHeaders'])) {
             return [];
         }
 
         $headerslist = array();
-        foreach ($docs['ApiHeaders'] as $params)
-        {
+        foreach ($docs['ApiHeaders'] as $params) {
             $tr = array(
                 'name'        => $params['name'],
                 'type'        => $params['type'],
@@ -69,14 +71,12 @@ class Builder
 
     protected function generateParamsTemplate($docs)
     {
-        if (!isset($docs['ApiParams']))
-        {
+        if (!isset($docs['ApiParams'])) {
             return [];
         }
 
         $paramslist = array();
-        foreach ($docs['ApiParams'] as $params)
-        {
+        foreach ($docs['ApiParams'] as $params) {
             $tr = array(
                 'name'        => $params['name'],
                 'type'        => isset($params['type']) ? $params['type'] : 'string',
@@ -92,14 +92,12 @@ class Builder
 
     protected function generateReturnHeadersTemplate($docs)
     {
-        if (!isset($docs['ApiReturnHeaders']))
-        {
+        if (!isset($docs['ApiReturnHeaders'])) {
             return [];
         }
 
         $headerslist = array();
-        foreach ($docs['ApiReturnHeaders'] as $params)
-        {
+        foreach ($docs['ApiReturnHeaders'] as $params) {
             $tr = array(
                 'name'        => $params['name'],
                 'type'        => 'string',
@@ -115,14 +113,12 @@ class Builder
 
     protected function generateReturnParamsTemplate($st_params)
     {
-        if (!isset($st_params['ApiReturnParams']))
-        {
+        if (!isset($st_params['ApiReturnParams'])) {
             return [];
         }
 
         $paramslist = array();
-        foreach ($st_params['ApiReturnParams'] as $params)
-        {
+        foreach ($st_params['ApiReturnParams'] as $params) {
             $tr = array(
                 'name'        => $params['name'],
                 'type'        => isset($params['type']) ? $params['type'] : 'string',
@@ -157,20 +153,14 @@ class Builder
         $counter = 0;
         $section = null;
         $docslist = [];
-        foreach ($annotations as $class => $methods)
-        {
-            foreach ($methods as $name => $docs)
-            {
-                if (isset($docs['ApiSector'][0]))
-                {
+        foreach ($annotations as $class => $methods) {
+            foreach ($methods as $name => $docs) {
+                if (isset($docs['ApiSector'][0])) {
                     $section = is_array($docs['ApiSector'][0]) ? $docs['ApiSector'][0]['data'] : $docs['ApiSector'][0];
-                }
-                else
-                {
+                } else {
                     $section = $class;
                 }
-                if (0 === count($docs))
-                {
+                if (0 === count($docs)) {
                     continue;
                 }
 
@@ -180,6 +170,7 @@ class Builder
                     'method_label'      => $this->generateBadgeForMethod($docs),
                     'section'           => $section,
                     'route'             => is_array($docs['ApiRoute'][0]) ? $docs['ApiRoute'][0]['data'] : $docs['ApiRoute'][0],
+                    '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),

+ 83 - 160
application/admin/command/Api/library/Extractor.php

@@ -38,7 +38,7 @@ class Extractor
      */
     public function setStrict($value)
     {
-        $this->strict = (bool) $value;
+        $this->strict = (bool)$value;
     }
 
     /**
@@ -67,8 +67,7 @@ class Extractor
      */
     public static function getClassAnnotations($className)
     {
-        if (!isset(self::$annotationCache[$className]))
-        {
+        if (!isset(self::$annotationCache[$className])) {
             $class = new \ReflectionClass($className);
             self::$annotationCache[$className] = self::parseAnnotations($class->getDocComment());
         }
@@ -80,8 +79,7 @@ class Extractor
     {
         $class = new \ReflectionClass($className);
 
-        foreach ($class->getMethods() as $object)
-        {
+        foreach ($class->getMethods() as $object) {
             self::$annotationCache['annotations'][$className][$object->name] = self::getMethodAnnotations($className, $object->name);
         }
 
@@ -91,29 +89,22 @@ class Extractor
     /**
      * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
      *
-     * @param  string $className  class name
+     * @param  string $className class name
      * @param  string $methodName method name to get annotations
      * @return array  self::$annotationCache all annotated elements of a method given
      */
     public static function getMethodAnnotations($className, $methodName)
     {
-        if (!isset(self::$annotationCache[$className . '::' . $methodName]))
-        {
-            try
-            {
+        if (!isset(self::$annotationCache[$className . '::' . $methodName])) {
+            try {
                 $method = new \ReflectionMethod($className, $methodName);
                 $class = new \ReflectionClass($className);
-                if (!$method->isPublic() || $method->isConstructor())
-                {
+                if (!$method->isPublic() || $method->isConstructor()) {
                     $annotations = array();
-                }
-                else
-                {
+                } else {
                     $annotations = self::consolidateAnnotations($method, $class);
                 }
-            }
-            catch (\ReflectionException $e)
-            {
+            } catch (\ReflectionException $e) {
                 $annotations = array();
             }
 
@@ -127,7 +118,7 @@ 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 $className class name
      * @param  string $methodName method name to get annotations
      * @return array  self::$annotationCache all annotated objects of a method given
      */
@@ -138,42 +129,31 @@ class Extractor
 
         $i = 0;
 
-        foreach ($annotations as $annotationClass => $listParams)
-        {
+        foreach ($annotations as $annotationClass => $listParams) {
             $annotationClass = ucfirst($annotationClass);
             $class = $this->defaultNamespace . $annotationClass . 'Annotation';
 
             // verify is the annotation class exists, depending if Annotations::strict is true
             // if not, just skip the annotation instance creation.
-            if (!class_exists($class))
-            {
-                if ($this->strict)
-                {
+            if (!class_exists($class)) {
+                if ($this->strict) {
                     throw new Exception(sprintf('Runtime Error: Annotation Class Not Found: %s', $class));
-                }
-                else
-                {
+                } else {
                     // silent skip & continue
                     continue;
                 }
             }
 
-            if (empty($objects[$annotationClass]))
-            {
+            if (empty($objects[$annotationClass])) {
                 $objects[$annotationClass] = new $class();
             }
 
-            foreach ($listParams as $params)
-            {
-                if (is_array($params))
-                {
-                    foreach ($params as $key => $value)
-                    {
+            foreach ($listParams as $params) {
+                if (is_array($params)) {
+                    foreach ($params as $key => $value) {
                         $objects[$annotationClass]->set($key, $value);
                     }
-                }
-                else
-                {
+                } else {
                     $objects[$annotationClass]->set($i++, $params);
                 }
             }
@@ -190,8 +170,7 @@ class Extractor
 
         $methodAnnotations = self::parseAnnotations($docblockMethod);
         $classAnnotations = self::parseAnnotations($dockblockClass);
-        if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty')
-        {
+        if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty') {
             return [];
         }
 
@@ -205,74 +184,58 @@ class Extractor
         $methodTitle = isset($methodArr[1]) && isset($methodArr[1][0]) ? $methodArr[1][0] : '';
         $classTitle = isset($classArr[1]) && isset($classArr[1][0]) ? $classArr[1][0] : '';
 
-        if (!isset($methodAnnotations['ApiMethod']))
-        {
+        if (!isset($methodAnnotations['ApiMethod'])) {
             $methodAnnotations['ApiMethod'] = ['get'];
         }
-        if (!isset($methodAnnotations['ApiSummary']))
-        {
+        if (!isset($methodAnnotations['ApiSummary'])) {
             $methodAnnotations['ApiSummary'] = [$methodTitle];
         }
 
-        if ($methodAnnotations)
-        {
-            foreach ($classAnnotations as $name => $valueClass)
-            {
-                if (count($valueClass) !== 1)
-                {
+        if ($methodAnnotations) {
+            foreach ($classAnnotations as $name => $valueClass) {
+                if (count($valueClass) !== 1) {
                     continue;
                 }
 
-                if ($name === 'ApiRoute')
-                {
-                    if (isset($methodAnnotations[$name]))
-                    {
+                if ($name === 'ApiRoute') {
+                    if (isset($methodAnnotations[$name])) {
                         $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . $methodAnnotations[$name][0]];
-                    }
-                    else
-                    {
+                    } else {
                         $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . '/' . $method->getName()];
                     }
                 }
 
-                if ($name === 'ApiSector')
-                {
+                if ($name === 'ApiSector') {
                     $methodAnnotations[$name] = $valueClass;
                 }
             }
         }
-        if (!isset($methodAnnotations['ApiTitle']))
-        {
+        if (!isset($methodAnnotations['ApiTitle'])) {
             $methodAnnotations['ApiTitle'] = [$methodTitle];
         }
-        if (!isset($methodAnnotations['ApiRoute']))
-        {
+        if (!isset($methodAnnotations['ApiRoute'])) {
             $urlArr = [];
             $className = $class->getName();
 
             list($prefix, $suffix) = explode('\\' . \think\Config::get('url_controller_layer') . '\\', $className);
             $prefixArr = explode('\\', $prefix);
             $suffixArr = explode('\\', $suffix);
-            if ($prefixArr[0] == \think\Config::get('app_namespace'))
-            {
+            if ($prefixArr[0] == \think\Config::get('app_namespace')) {
                 $prefixArr[0] = '';
             }
             $urlArr = array_merge($urlArr, $prefixArr);
-            $urlArr[] = implode('.', array_map(function($item) {
-                        return \think\Loader::parseName($item);
-                    }, $suffixArr));
+            $urlArr[] = implode('.', array_map(function ($item) {
+                return \think\Loader::parseName($item);
+            }, $suffixArr));
             $urlArr[] = $method->getName();
             $methodAnnotations['ApiRoute'] = [implode('/', $urlArr)];
         }
-        if (!isset($methodAnnotations['ApiSector']))
-        {
+        if (!isset($methodAnnotations['ApiSector'])) {
             $methodAnnotations['ApiSector'] = isset($classAnnotations['ApiSector']) ? $classAnnotations['ApiSector'] : [$classTitle];
         }
-        if (!isset($methodAnnotations['ApiParams']))
-        {
+        if (!isset($methodAnnotations['ApiParams'])) {
             $params = self::parseCustomAnnotations($docblockMethod, 'param');
-            foreach ($params as $k => $v)
-            {
+            foreach ($params as $k => $v) {
                 $arr = explode(' ', preg_replace("/[\s]+/", " ", $v));
                 $methodAnnotations['ApiParams'][] = [
                     'name'        => isset($arr[1]) ? str_replace('$', '', $arr[1]) : '',
@@ -299,10 +262,8 @@ class Extractor
         $annotations = array();
 
         $docblock = substr($docblock, 3, -2);
-        if (preg_match_all('/@' . $name . '(?:\s*(?:\(\s*)?(.*?)(?:\s*\))?)??\s*(?:\n|\*\/)/', $docblock, $matches))
-        {
-            foreach ($matches[1] as $k => $v)
-            {
+        if (preg_match_all('/@' . $name . '(?:\s*(?:\(\s*)?(.*?)(?:\s*\))?)??\s*(?:\n|\*\/)/', $docblock, $matches)) {
+            foreach ($matches[1] as $k => $v) {
                 $annotations[] = $v;
             }
         }
@@ -321,36 +282,31 @@ class Extractor
 
         // Strip away the docblock header and footer to ease parsing of one line annotations
         $docblock = substr($docblock, 3, -2);
-        if (preg_match_all('/@(?<name>[A-Za-z_-]+)[\s\t]*\((?<args>(?:(?!\)).)*)\)\r?/s', $docblock, $matches))
-        {
+        if (preg_match_all('/@(?<name>[A-Za-z_-]+)[\s\t]*\((?<args>(?:(?!\)).)*)\)\r?/s', $docblock, $matches)) {
             $numMatches = count($matches[0]);
-            for ($i = 0; $i < $numMatches; ++$i)
-            {
+            for ($i = 0; $i < $numMatches; ++$i) {
+                $name = $matches['name'][$i];
+                $value = '';
                 // annotations has arguments
-                if (isset($matches['args'][$i]))
-                {
+                if (isset($matches['args'][$i])) {
                     $argsParts = trim($matches['args'][$i]);
-                    $name = $matches['name'][$i];
-                    if($name == 'ApiReturn')
-                    {
+                    if ($name == 'ApiReturn') {
                         $value = $argsParts;
-                    } else {
+                    } else if ($matches['args'][$i] != '') {
                         $argsParts = preg_replace("/\{(\w+)\}/", '#$1#', $argsParts);
                         $value = self::parseArgs($argsParts);
-                        if(is_string($value))
-                        {
+                        if (is_string($value)) {
                             $value = preg_replace("/\#(\w+)\#/", '{$1}', $argsParts);
                         }
                     }
                 }
-                else
-                {
-                    $value = array();
-                }
 
                 $annotations[$name][] = $value;
             }
         }
+        if (stripos($docblock, '@ApiInternal') !== false) {
+            $annotations['ApiInternal'] = [true];
+        }
 
         return $annotations;
     }
@@ -382,39 +338,31 @@ class Extractor
         $quoted = false;
         $tokens = array('"', '"', '{', '}', ',', '=');
 
-        while ($i <= $len)
-        {
+        while ($i <= $len) {
             $prev_c = substr($content, $i - 1, 1);
             $c = substr($content, $i++, 1);
 
-            if ($c === '"' && $prev_c !== "\\")
-            {
+            if ($c === '"' && $prev_c !== "\\") {
                 $delimiter = $c;
                 //open delimiter
-                if (!$composing && empty($prevDelimiter) && empty($nextDelimiter))
-                {
+                if (!$composing && empty($prevDelimiter) && empty($nextDelimiter)) {
                     $prevDelimiter = $nextDelimiter = $delimiter;
                     $val = '';
                     $composing = true;
                     $quoted = true;
-                }
-                else
-                {
+                } else {
                     // close delimiter
-                    if ($c !== $nextDelimiter)
-                    {
+                    if ($c !== $nextDelimiter) {
                         throw new Exception(sprintf(
-                                "Parse Error: enclosing error -> expected: [%s], given: [%s]", $nextDelimiter, $c
+                            "Parse Error: enclosing error -> expected: [%s], given: [%s]", $nextDelimiter, $c
                         ));
                     }
 
                     // validating syntax
-                    if ($i < $len)
-                    {
-                        if (',' !== substr($content, $i, 1) && '\\' !== $prev_c)
-                        {
+                    if ($i < $len) {
+                        if (',' !== substr($content, $i, 1) && '\\' !== $prev_c) {
                             throw new Exception(sprintf(
-                                    "Parse Error: missing comma separator near: ...%s<--", substr($content, ($i - 10), $i)
+                                "Parse Error: missing comma separator near: ...%s<--", substr($content, ($i - 10), $i)
                             ));
                         }
                     }
@@ -423,11 +371,8 @@ class Extractor
                     $composing = false;
                     $delimiter = null;
                 }
-            }
-            elseif (!$composing && in_array($c, $tokens))
-            {
-                switch ($c)
-                {
+            } elseif (!$composing && in_array($c, $tokens)) {
+                switch ($c) {
                     case '=':
                         $prevDelimiter = $nextDelimiter = '';
                         $level = 2;
@@ -440,10 +385,9 @@ class Extractor
 
                         // If composing flag is true yet,
                         // it means that the string was not enclosed, so it is parsing error.
-                        if ($composing === true && !empty($prevDelimiter) && !empty($nextDelimiter))
-                        {
+                        if ($composing === true && !empty($prevDelimiter) && !empty($nextDelimiter)) {
                             throw new Exception(sprintf(
-                                    "Parse Error: enclosing error -> expected: [%s], given: [%s]", $nextDelimiter, $c
+                                "Parse Error: enclosing error -> expected: [%s], given: [%s]", $nextDelimiter, $c
                             ));
                         }
 
@@ -453,19 +397,16 @@ class Extractor
                         $subc = '';
                         $subComposing = true;
 
-                        while ($i <= $len)
-                        {
+                        while ($i <= $len) {
                             $c = substr($content, $i++, 1);
 
-                            if (isset($delimiter) && $c === $delimiter)
-                            {
+                            if (isset($delimiter) && $c === $delimiter) {
                                 throw new Exception(sprintf(
-                                        "Parse Error: Composite variable is not enclosed correctly."
+                                    "Parse Error: Composite variable is not enclosed correctly."
                                 ));
                             }
 
-                            if ($c === '}')
-                            {
+                            if ($c === '}') {
                                 $subComposing = false;
                                 break;
                             }
@@ -473,37 +414,27 @@ class Extractor
                         }
 
                         // if the string is composing yet means that the structure of var. never was enclosed with '}'
-                        if ($subComposing)
-                        {
+                        if ($subComposing) {
                             throw new Exception(sprintf(
-                                    "Parse Error: Composite variable is not enclosed correctly. near: ...%s'", $subc
+                                "Parse Error: Composite variable is not enclosed correctly. near: ...%s'", $subc
                             ));
                         }
 
                         $val = self::parseArgs($subc);
                         break;
                 }
-            }
-            else
-            {
-                if ($level == 1)
-                {
+            } else {
+                if ($level == 1) {
                     $var .= $c;
-                }
-                elseif ($level == 2)
-                {
+                } elseif ($level == 2) {
                     $val .= $c;
                 }
             }
 
-            if ($level === 3 || $i === $len)
-            {
-                if ($type == 'plain' && $i === $len)
-                {
+            if ($level === 3 || $i === $len) {
+                if ($type == 'plain' && $i === $len) {
                     $data = self::castValue($var);
-                }
-                else
-                {
+                } else {
                     $data[trim($var)] = self::castValue($val, !$quoted);
                 }
 
@@ -520,34 +451,26 @@ 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  string $val string containing possibles variables that can be cast to bool or int
      * @param  boolean $trim indicate if the value passed should be trimmed after to try cast
      * @return mixed   returns the value converted to original type if was possible
      */
     private static function castValue($val, $trim = false)
     {
-        if (is_array($val))
-        {
-            foreach ($val as $key => $value)
-            {
+        if (is_array($val)) {
+            foreach ($val as $key => $value) {
                 $val[$key] = self::castValue($value);
             }
-        }
-        elseif (is_string($val))
-        {
-            if ($trim)
-            {
+        } elseif (is_string($val)) {
+            if ($trim) {
                 $val = trim($val);
             }
             $val = stripslashes($val);
             $tmp = strtolower($val);
 
-            if ($tmp === 'false' || $tmp === 'true')
-            {
+            if ($tmp === 'false' || $tmp === 'true') {
                 $val = $tmp === 'true';
-            }
-            elseif (is_numeric($val))
-            {
+            } elseif (is_numeric($val)) {
                 return $val + 0;
             }
 

+ 89 - 2
application/admin/command/Api/template/index.html

@@ -8,6 +8,7 @@
         <meta name="author" content="{$config.author}">
         <title>{$config.title}</title>
         <link href="https://cdn.bootcss.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
+        <link href="https://cdn.bootcss.com/font-awesome/4.6.2/css/font-awesome.min.css" rel="stylesheet">
         <style type="text/css">
             body {
                 padding-top: 70px; margin-bottom: 15px;
@@ -28,6 +29,59 @@
             .null     { color: magenta; }
             .key      { color: red; }
             .popover  { max-width: 400px; max-height: 400px; overflow-y: auto;}
+            .list-group.panel > .list-group-item {
+            }
+            .list-group-item:last-child {
+                border-radius:0;
+            }
+            h4.panel-title a {
+                font-weight:normal;
+                font-size:14px;
+            }
+            h4.panel-title a .text-muted {
+                font-size:12px;
+                font-weight:normal;
+                font-family: 'Verdana';
+            }
+            #sidebar {
+                width: 220px;
+                position: fixed;
+                margin-left: -240px;
+                overflow-y:auto;
+            }
+            #sidebar > .list-group {
+                margin-bottom:0;
+            }
+            #sidebar > .list-group > a{
+                text-indent:0;
+            }
+            #sidebar .child {
+                border:1px solid #ddd;
+                border-bottom:none;
+            }
+            #sidebar .child > a {
+                border:0;
+            }
+            #sidebar .list-group a.current {
+                background:#f5f5f5;
+            }
+            @media (max-width: 1620px){
+                #sidebar {
+                    margin:0;
+                }
+                #accordion {
+                    padding-left:235px;
+                }
+            }
+            @media (max-width: 768px){
+                #sidebar {
+                    display: none;
+                }
+                #accordion {
+                    padding-left:0px;
+                }
+            }
+
         </style>
     </head>
     <body>
@@ -68,15 +122,29 @@
         </div>
 
         <div class="container">
+            <!-- menu -->
+            <div id="sidebar">
+                <div class="list-group panel">
+                    {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>
+                        {/foreach}
+                    </div>
+                    {/foreach}
+                </div>
+            </div>
             <div class="panel-group" id="accordion">
                 {foreach name="docslist" id="docs"}
                 <h2>{$key}</h2>
                 <hr>
                 {foreach name="docs" id="api" }
                 <div class="panel panel-default">
-                    <div class="panel-heading">
+                    <div class="panel-heading" id="heading-{$api.id}">
                         <h4 class="panel-title">
-                            <span class="label {$api.method_label}">{$api.method|strtoupper}</span> <a data-toggle="collapse" data-parent="#accordion{$api.id}" href="#collapseOne{$api.id}"> {$api.route}</a>
+                            <span class="label {$api.method_label}">{$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>
                     <div id="collapseOne{$api.id}" class="panel-collapse collapse">
@@ -198,6 +266,7 @@
                                                         {/if}
                                                         <div class="form-group">
                                                             <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>
                                                     </form>
                                                 </div>
@@ -349,6 +418,24 @@
                     placement: 'bottom'
                 });
 
+                $(window).on("resize", function(){
+                    $("#sidebar").css("max-height", $(window).height()-80);
+                });
+
+                $(window).trigger("resize");
+
+                $(document).on("click", "#sidebar .list-group > .list-group-item", function(){
+                    $("#sidebar .list-group > .list-group-item").removeClass("current");
+                    $(this).addClass("current");
+                });
+                $(document).on("click", "#sidebar .child a", function(){
+                    var heading = $("#heading-"+$(this).data("id"));
+                    if(!heading.next().hasClass("in")){
+                        $("a", heading).trigger("click");
+                    }
+                    $("html,body").animate({scrollTop:heading.offset().top-70});
+                });
+
                 $('code[id^=response]').hide();
 
                 $.each($('pre[id^=sample_response],pre[id^=sample_post_body]'), function () {

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

@@ -105,6 +105,12 @@ class Crud extends Command
     protected $sortField = 'weigh';
 
     /**
+     * 筛选字段
+     * @var string
+     */
+    protected $headingFilterField = 'status';
+
+    /**
      * 编辑器的Class
      */
     protected $editorClass = 'editor';
@@ -138,6 +144,7 @@ class Crud extends Command
             ->addOption('selectpagessuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate multiple selectpage component with suffix', null)
             ->addOption('ignorefields', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'ignore fields', null)
             ->addOption('sortfield', null, Option::VALUE_OPTIONAL, 'sort field', null)
+            ->addOption('headingfilterfield', null, Option::VALUE_OPTIONAL, 'heading filter field', null)
             ->addOption('editorclass', null, Option::VALUE_OPTIONAL, 'automatically generate editor class', null)
             ->setDescription('Build CRUD controller and model from table');
     }
@@ -198,6 +205,8 @@ class Crud extends Command
         $ignoreFields = $input->getOption('ignorefields');
         //排序字段
         $sortfield = $input->getOption('sortfield');
+        //顶部筛选过滤字段
+        $headingfilterfield = $input->getOption('headingfilterfield');
         //编辑器Class
         $editorclass = $input->getOption('editorclass');
         if ($setcheckboxsuffix)
@@ -224,6 +233,8 @@ class Crud extends Command
             $this->editorClass = $editorclass;
         if ($sortfield)
             $this->sortField = $sortfield;
+        if ($headingfilterfield)
+            $this->headingFilterField = $headingfilterfield;
 
         $dbname = Config::get('database.database');
         $prefix = Config::get('database.prefix');
@@ -466,6 +477,8 @@ class Crud extends Command
             $getEnumArr = [];
             $appendAttrList = [];
             $controllerAssignList = [];
+            $headingHtml = '{:build_heading()}';
+            $headingJs = '';
 
             //循环所有字段,开始构造视图的HTML和JS信息
             foreach ($columnList as $k => $v) {
@@ -681,6 +694,10 @@ class Crud extends Command
                         //构造JS列信息
                         $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], $inputType && in_array($inputType, ['select', 'checkbox', 'radio']) ? '_text' : '', $itemArr);
                     }
+                    if ($this->headingFilterField && $this->headingFilterField == $field && $itemArr) {
+                        $headingHtml = $this->getReplacedStub('html/heading-html', ['field' => $field]);
+                        $headingJs = $this->getReplacedStub('html/heading-js', ['field' => $field]);
+                    }
                     //排序方式,如果有指定排序字段,否则按主键排序
                     $order = $field == $this->sortField ? $this->sortField : $order;
                 }
@@ -724,6 +741,7 @@ class Crud extends Command
                 $modelInit = $this->getReplacedStub('mixins' . DS . 'modelinit', ['order' => $order]);
             }
 
+
             $data = [
                 'controllerNamespace'     => $controllerNamespace,
                 'modelNamespace'          => $modelNamespace,
@@ -753,6 +771,8 @@ class Crud extends Command
                 'relationWithList'        => '',
                 'relationMethodList'      => '',
                 'controllerIndex'         => '',
+                'headingHtml'             => $headingHtml,
+                'headingJs'               => $headingJs,
                 'visibleFieldList'        => $fields ? "\$row->visible(['" . implode("','", array_filter(explode(',', $fields))) . "']);" : '',
                 'appendAttrList'          => implode(",\n", $appendAttrList),
                 'getEnumList'             => implode("\n\n", $getEnumArr),

+ 10 - 0
application/admin/command/Crud/stubs/html/heading-html.stub

@@ -0,0 +1,10 @@
+
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs" data-field="{%field%}">
+            <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
+            {foreach name="{%field%}List" item="vo"}
+            <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
+            {/foreach}
+        </ul>
+    </div>

+ 18 - 0
application/admin/command/Crud/stubs/html/heading-js.stub

@@ -0,0 +1,18 @@
+
+            // 绑定TAB事件
+            $('.panel-heading a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+                var field = $(this).closest("ul").data("field");
+                var value = $(this).data("value");
+                var options = table.bootstrapTable('getOptions');
+                options.pageNumber = 1;
+                options.queryParams = function (params) {
+                    var filter = {};
+                    if (value !== '') {
+                        filter[field] = value;
+                    }
+                    params.filter = JSON.stringify(filter);
+                    return params;
+                };
+                table.bootstrapTable('refresh', {});
+                return false;
+            });

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

@@ -1,5 +1,5 @@
 <div class="panel panel-default panel-intro">
-    {:build_heading()}
+    {%headingHtml%}
 
     <div class="panel-body">
         <div id="myTabContent" class="tab-content">

+ 2 - 0
application/admin/command/Crud/stubs/javascript.stub

@@ -28,6 +28,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 ]
             });
 
+            {%headingJs%}
+
             // 为表格绑定事件
             Table.api.bindevent(table);
         },

+ 1 - 1
application/admin/command/Crud/stubs/mixins/checkbox.stub

@@ -1,7 +1,7 @@
 
     public function {%methodName%}($value, $data)
     {
-        $value = $value ? $value : $data['{%field%}'];
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
         $valueArr = explode(',', $value);
         $list = $this->{%listMethodName%}();
         return implode(',', array_intersect_key($list, array_flip($valueArr)));

+ 1 - 1
application/admin/command/Crud/stubs/mixins/datetime.stub

@@ -1,6 +1,6 @@
 
     public function {%methodName%}($value, $data)
     {
-        $value = $value ? $value : $data['{%field%}'];
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
         return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
     }

+ 1 - 1
application/admin/command/Crud/stubs/mixins/multiple.stub

@@ -1,7 +1,7 @@
 
     public function {%methodName%}($value, $data)
     {
-        $value = $value ? $value : $data['{%field%}'];
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
         $valueArr = explode(',', $value);
         $list = $this->{%listMethodName%}();
         return implode(',', array_intersect_key($list, array_flip($valueArr)));

+ 1 - 1
application/admin/command/Crud/stubs/mixins/radio.stub

@@ -1,7 +1,7 @@
 
     public function {%methodName%}($value, $data)
     {        
-        $value = $value ? $value : $data['{%field%}'];
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
         $list = $this->{%listMethodName%}();
         return isset($list[$value]) ? $list[$value] : '';
     }

+ 1 - 1
application/admin/command/Crud/stubs/mixins/select.stub

@@ -1,7 +1,7 @@
 
     public function {%methodName%}($value, $data)
     {        
-        $value = $value ? $value : $data['{%field%}'];
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
         $list = $this->{%listMethodName%}();
         return isset($list[$value]) ? $list[$value] : '';
     }

+ 20 - 34
application/admin/controller/Ajax.php

@@ -165,44 +165,30 @@ class Ajax extends Backend
             $ids = array_values(array_intersect($ids, $hasids));
         }
 
-        //直接修复排序
-        $one = Db::name($table)->field("{$field},COUNT(*) AS nums")->group($field)->having('nums > 1')->find();
-        if ($one) {
-            $list = Db::name($table)->field("$prikey,$field")->order($field, $orderway)->select();
-            foreach ($list as $k => $v) {
-                Db::name($table)->where($prikey, $v[$prikey])->update([$field => $k + 1]);
-            }
-            $this->success();
-        } else {
-            $list = Db::name($table)->field("$prikey,$field")->where($prikey, 'in', $ids)->order($field, $orderway)->select();
-            foreach ($list as $k => $v) {
-                $sour[] = $v[$prikey];
-                $weighdata[$v[$prikey]] = $v[$field];
-            }
-            $position = array_search($changeid, $ids);
-            $desc_id = $sour[$position];    //移动到目标的ID值,取出所处改变前位置的值
-            $sour_id = $changeid;
-            $desc_value = $weighdata[$desc_id];
-            $sour_value = $weighdata[$sour_id];
-            //echo "移动的ID:{$sour_id}\n";
-            //echo "替换的ID:{$desc_id}\n";
-            $weighids = array();
-            $temp = array_values(array_diff_assoc($ids, $sour));
-            foreach ($temp as $m => $n) {
-                if ($n == $sour_id) {
-                    $offset = $desc_id;
+        $list = Db::name($table)->field("$prikey,$field")->where($prikey, 'in', $ids)->order($field, $orderway)->select();
+        foreach ($list as $k => $v) {
+            $sour[] = $v[$prikey];
+            $weighdata[$v[$prikey]] = $v[$field];
+        }
+        $position = array_search($changeid, $ids);
+        $desc_id = $sour[$position];    //移动到目标的ID值,取出所处改变前位置的值
+        $sour_id = $changeid;
+        $weighids = array();
+        $temp = array_values(array_diff_assoc($ids, $sour));
+        foreach ($temp as $m => $n) {
+            if ($n == $sour_id) {
+                $offset = $desc_id;
+            } else {
+                if ($sour_id == $temp[0]) {
+                    $offset = isset($temp[$m + 1]) ? $temp[$m + 1] : $sour_id;
                 } else {
-                    if ($sour_id == $temp[0]) {
-                        $offset = isset($temp[$m + 1]) ? $temp[$m + 1] : $sour_id;
-                    } else {
-                        $offset = isset($temp[$m - 1]) ? $temp[$m - 1] : $sour_id;
-                    }
+                    $offset = isset($temp[$m - 1]) ? $temp[$m - 1] : $sour_id;
                 }
-                $weighids[$n] = $weighdata[$offset];
-                Db::name($table)->where($prikey, $n)->update([$field => $weighdata[$offset]]);
             }
-            $this->success();
+            $weighids[$n] = $weighdata[$offset];
+            Db::name($table)->where($prikey, $n)->update([$field => $weighdata[$offset]]);
         }
+        $this->success();
     }
 
     /**

+ 3 - 0
application/admin/controller/Category.php

@@ -15,6 +15,9 @@ use fast\Tree;
 class Category extends Backend
 {
 
+    /**
+     * @var \app\common\model\Category
+     */
     protected $model = null;
     protected $categorylist = [];
     protected $noNeedRight = ['selectpage'];

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

@@ -30,7 +30,7 @@ class Index extends Backend
     public function index()
     {
         //左侧菜单
-        list($menulist, $navlist) = $this->auth->getSidebar([
+        list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar([
             'dashboard' => 'hot',
             'addon'     => ['new', 'red', 'badge'],
             'auth/rule' => __('Menu'),
@@ -44,6 +44,8 @@ class Index extends Backend
         }
         $this->view->assign('menulist', $menulist);
         $this->view->assign('navlist', $navlist);
+        $this->view->assign('fixedmenu', $fixedmenu);
+        $this->view->assign('referermenu', $referermenu);
         $this->view->assign('title', __('Home'));
         return $this->view->fetch();
     }

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

@@ -17,6 +17,9 @@ use fast\Tree;
 class Admin extends Backend
 {
 
+    /**
+     * @var \app\admin\model\Admin
+     */
     protected $model = null;
     protected $childrenGroupIds = [];
     protected $childrenAdminIds = [];

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

@@ -14,6 +14,9 @@ use app\common\controller\Backend;
 class Adminlog extends Backend
 {
 
+    /**
+     * @var \app\admin\model\AdminLog
+     */
     protected $model = null;
     protected $childrenGroupIds = [];
     protected $childrenAdminIds = [];

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

@@ -15,6 +15,9 @@ use fast\Tree;
 class Group extends Backend
 {
 
+    /**
+     * @var \app\admin\model\AuthGroup
+     */
     protected $model = null;
     //当前登录管理员所有子组别
     protected $childrenGroupIds = [];

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

@@ -15,6 +15,9 @@ use think\Cache;
 class Rule extends Backend
 {
 
+    /**
+     * @var \app\admin\model\AuthRule
+     */
     protected $model = null;
     protected $rulelist = [];
     protected $multiFields = 'ismenu,status';

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

@@ -13,6 +13,9 @@ use app\common\controller\Backend;
 class Attachment extends Backend
 {
 
+    /**
+     * @var \app\common\model\Attachment
+     */
     protected $model = null;
 
     public function _initialize()

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

@@ -16,6 +16,9 @@ use think\Exception;
 class Config extends Backend
 {
 
+    /**
+     * @var \app\common\model\Config
+     */
     protected $model = null;
     protected $noNeedRight = ['check'];
 

+ 1 - 1
application/admin/controller/user/Group.php

@@ -13,7 +13,7 @@ class Group extends Backend
 {
 
     /**
-     * UserGroup模型对象
+     * @var \app\admin\model\UserGroup
      */
     protected $model = null;
 

+ 2 - 1
application/admin/controller/user/Rule.php

@@ -13,8 +13,9 @@ use fast\Tree;
 class Rule extends Backend
 {
 
+
     /**
-     * UserRule模型对象
+     * @var \app\admin\model\UserRule
      */
     protected $model = null;
     protected $rulelist = [];

+ 2 - 1
application/admin/controller/user/User.php

@@ -14,8 +14,9 @@ class User extends Backend
 
     protected $relationSearch = true;
 
+
     /**
-     * User模型对象
+     * @var \app\admin\model\User
      */
     protected $model = null;
 

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

@@ -17,6 +17,8 @@ return [
     'Del'                                                   => '删除',
     'Delete'                                                => '删除',
     'Import'                                                => '导入',
+    'Export'                                                => '导出',
+    'All'                                                   => '全部',
     'Detail'                                                => '详情',
     'Multi'                                                 => '批量更新',
     'Setting'                                               => '配置',

+ 15 - 3
application/admin/library/Auth.php

@@ -367,7 +367,8 @@ class Auth extends \fast\Auth
 
         // 读取管理员当前拥有的权限节点
         $userRule = $this->getRuleList();
-        $select_id = 0;
+        $selected = $referer = [];
+        $refererUrl = Session::get('referer');
         $pinyin = new \Overtrue\Pinyin\Pinyin('Overtrue\Pinyin\MemoryFileDictLoader');
         // 必须将结果集转换为数组
         $ruleList = collection(\app\admin\model\AuthRule::where('status', 'normal')->where('ismenu', 1)->order('weigh', 'desc')->cache("__menu__")->select())->toArray();
@@ -376,15 +377,20 @@ class Auth extends \fast\Auth
                 unset($ruleList[$k]);
                 continue;
             }
-            $select_id = $v['name'] == $fixedPage ? $v['id'] : $select_id;
             $v['icon'] = $v['icon'] . ' fa-fw';
             $v['url'] = '/' . $module . '/' . $v['name'];
             $v['badge'] = isset($badgeList[$v['name']]) ? $badgeList[$v['name']] : '';
             $v['py'] = $pinyin->abbr($v['title'], '');
             $v['pinyin'] = $pinyin->permalink($v['title'], '');
             $v['title'] = __($v['title']);
+            $selected = $v['name'] == $fixedPage ? $v : $selected;
+            $referer = $v['url'] == $refererUrl ? $v : $referer;
+        }
+        if ($selected == $referer) {
+            $referer = [];
         }
 
+        $select_id = $selected ? $selected['id'] : 0;
         $menu = $nav = '';
         if (Config::get('fastadmin.multiplenav')) {
             $topList = [];
@@ -412,10 +418,16 @@ class Auth extends \fast\Auth
             // 构造菜单数据
             Tree::instance()->init($ruleList);
             $menu = Tree::instance()->getTreeMenu(0, '<li class="@class"><a href="@url@addtabs" addtabs="@id" url="@url" py="@py" pinyin="@pinyin"><i class="@icon"></i> <span>@title</span> <span class="pull-right-container">@caret @badge</span></a> @childlist</li>', $select_id, '', 'ul', 'class="treeview-menu"');
+            if ($selected) {
+                $nav .= '<li role="presentation" id="tab_' . $selected['id'] . '" class="' . ($referer ? '' : 'active') . '"><a href="#con_' . $selected['id'] . '" node-id="' . $selected['id'] . '" aria-controls="' . $selected['id'] . '" role="tab" data-toggle="tab"><i class="' . $selected['icon'] . ' fa-fw"></i> <span>' . $selected['title'] . '</span> </a></li>';
+            }
+            if ($referer) {
+                $nav .= '<li role="presentation" id="tab_' . $referer['id'] . '" class="active"><a href="#con_' . $referer['id'] . '" node-id="' . $referer['id'] . '" aria-controls="' . $referer['id'] . '" role="tab" data-toggle="tab"><i class="' . $referer['icon'] . ' fa-fw"></i> <span>' . $referer['title'] . '</span> </a> <i class="close-tab fa fa-remove"></i></li>';
+            }
         }
 
 
-        return [$menu, $nav];
+        return [$menu, $nav, $selected, $referer];
     }
 
     /**

+ 6 - 0
application/admin/library/traits/Backend.php

@@ -92,6 +92,8 @@ trait Backend
                     }
                 } catch (\think\exception\PDOException $e) {
                     $this->error($e->getMessage());
+                } catch (\think\Exception $e) {
+                    $this->error($e->getMessage());
                 }
             }
             $this->error(__('Parameter %s can not be empty', ''));
@@ -131,6 +133,8 @@ trait Backend
                     }
                 } catch (\think\exception\PDOException $e) {
                     $this->error($e->getMessage());
+                } catch (\think\Exception $e) {
+                    $this->error($e->getMessage());
                 }
             }
             $this->error(__('Parameter %s can not be empty', ''));
@@ -322,6 +326,8 @@ trait Backend
             $this->model->saveAll($insert);
         } catch (\think\exception\PDOException $exception) {
             $this->error($exception->getMessage());
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
         }
 
         $this->success();

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

@@ -14,7 +14,7 @@
             <div class="tab-pane fade active in" id="one">
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
-                        {:build_toolbar()}
+                        {:build_toolbar('refresh,add,edit,del')}
                         <div class="dropdown btn-group {:$auth->check('category/multi')?'':'hide'}">
                             <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
                             <ul class="dropdown-menu text-left" role="menu">

+ 6 - 0
application/admin/view/common/header.html

@@ -141,6 +141,12 @@
     <!--第二级菜单,只有在multiplenav开启时才显示-->
     <div id="secondnav">
         <ul class="nav nav-tabs nav-addtabs disable-top-badge" role="tablist">
+            {if $fixedmenu}
+            <li role="presentation" id="tab_{$fixedmenu.id}" class="{:$referermenu?'':'active'}"><a href="#con_{$fixedmenu.id}" node-id="{$fixedmenu.id}" aria-controls="{$fixedmenu.id}" role="tab" data-toggle="tab"><i class="fa fa-dashboard fa-fw"></i> <span>{$fixedmenu.title}</span> <span class="pull-right-container"> </span></a></li>
+            {/if}
+            {if $referermenu}
+            <li role="presentation" id="tab_{$referermenu.id}" class="active"><a href="#con_{$referermenu.id}" node-id="{$referermenu.id}" aria-controls="{$referermenu.id}" role="tab" data-toggle="tab"><i class="fa fa-list fa-fw"></i> <span>{$referermenu.title}</span> <span class="pull-right-container"> </span></a> <i class="close-tab fa fa-remove"></i></li>
+            {/if}
         </ul>
     </div>
     {/if}

+ 11 - 2
application/admin/view/index/index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="{$config.language}">
     <head>
-        <!-- 加载部部样式及META信息 -->
+        <!-- 加载样式及META信息 -->
         {include file="common/meta" /}
     </head>
     <body class="hold-transition skin-green sidebar-mini fixed {if $config.fastadmin.multiplenav}multiplenav{/if}" id="tabs">
@@ -19,7 +19,16 @@
 
             <!-- 主体内容区域 -->
             <div class="content-wrapper tab-content tab-addtabs">
-
+                {if $fixedmenu}
+                <div role="tabpanel" class="tab-pane {:$referermenu?'':'active'}" id="con_{$fixedmenu.id}">
+                    <iframe src="{$fixedmenu.url}?addtabs=1" width="100%" height="100%" frameborder="no" border="0" marginwidth="0" marginheight="0" scrolling-x="no" scrolling-y="auto" allowtransparency="yes"></iframe>
+                </div>
+                {/if}
+                {if $referermenu}
+                <div role="tabpanel" class="tab-pane active" id="con_{$referermenu.id}">
+                    <iframe src="{$referermenu.url}?addtabs=1" width="100%" height="100%" frameborder="no" border="0" marginwidth="0" marginheight="0" scrolling-x="no" scrolling-y="auto" allowtransparency="yes"></iframe>
+                </div>
+                {/if}
             </div>
 
             <!-- 底部链接,默认隐藏 -->

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

@@ -6,7 +6,7 @@
             <div class="tab-pane fade active in" id="one">
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
-                        {:build_toolbar()}
+                        {:build_toolbar('refresh,add,edit,del')}
                         <div class="dropdown btn-group {:$auth->check('user/group/multi')?'':'hide'}">
                             <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
                             <ul class="dropdown-menu text-left" role="menu">

+ 28 - 4
application/api/controller/Demo.php

@@ -15,13 +15,37 @@ class Demo extends Api
     //如果接口已经设置无需登录,那也就无需鉴权了
     //
     // 无需登录的接口,*表示全部
-    protected $noNeedLogin = ['test1'];
+    protected $noNeedLogin = ['test', 'test1'];
     // 无需鉴权的接口,*表示全部
     protected $noNeedRight = ['test2'];
 
     /**
+     * 测试方法
+     *
+     * @ApiTitle    (测试名称)
+     * @ApiSummary  (测试描述信息)
+     * @ApiMethod   (POST)
+     * @ApiRoute    (/api/demo/test/id/{id}/name/{name})
+     * @ApiHeaders  (name=token, type=string, required=true, description="请求的Token")
+     * @ApiParams   (name="id", type="integer", required=true, description="会员ID")
+     * @ApiParams   (name="name", type="string", required=true, description="用户名")
+     * @ApiParams   (name="data", type="object", sample="{'user_id':'int','user_name':'string','profile':{'email':'string','age':'integer'}}", description="扩展数据")
+     * @ApiReturnParams   (name="code", type="integer", required=true, sample="0")
+     * @ApiReturnParams   (name="msg", type="string", required=true, sample="返回成功")
+     * @ApiReturnParams   (name="data", type="object", sample="{'user_id':'int','user_name':'string','profile':{'email':'string','age':'integer'}}", description="扩展数据返回")
+     * @ApiReturn   ({
+         'code':'1',
+         'msg':'返回成功'
+        })
+     */
+    public function test()
+    {
+        $this->success('返回成功', $this->request->param());
+    }
+
+    /**
      * 无需登录的接口
-     * 
+     *
      */
     public function test1()
     {
@@ -30,7 +54,7 @@ class Demo extends Api
 
     /**
      * 需要登录的接口
-     * 
+     *
      */
     public function test2()
     {
@@ -39,7 +63,7 @@ class Demo extends Api
 
     /**
      * 需要登录且需要验证有相应组的权限
-     * 
+     *
      */
     public function test3()
     {

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

@@ -303,7 +303,7 @@ class Backend extends Controller
                 case 'FINDIN':
                 case 'FINDINSET':
                 case 'FIND_IN_SET':
-                    $where[] = "FIND_IN_SET('{$v}', " . ($this->relationSearch ? $k : '`' . str_replace('.', '`.`', $k) . '`') . ")";
+                    $where[] = "FIND_IN_SET('{$v}', " . ($relationSearch ? $k : '`' . str_replace('.', '`.`', $k) . '`') . ")";
                     break;
                 case 'IN':
                 case 'IN(...)':

+ 1 - 1
bower.json

@@ -31,7 +31,7 @@
     "fastadmin-citypicker": "~1.3.0",
     "fastadmin-cxselect": "~1.4.0",
     "fastadmin-dragsort": "~1.0.0",
-    "fastadmin-addtabs": "~1.0.0",
+    "fastadmin-addtabs": "~1.0.3",
     "fastadmin-selectpage": "~1.0.0",
     "fastadmin-layer": "~3.1.2"
   }

文件差異過大導致無法顯示
+ 756 - 381
public/api.html


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

@@ -11,6 +11,10 @@
 @import url("../libs/nice-validator/dist/jquery.validator.css");
 @import url("../libs/bootstrap-select/dist/css/bootstrap-select.min.css");
 @import url("../libs/fastadmin-selectpage/selectpage.css");
+html,
+body {
+  height: 100%;
+}
 body {
   background: #f1f4f6;
   font-size: 13px;
@@ -44,18 +48,19 @@ html.ios-fix body {
   overflow: auto;
   -webkit-overflow-scrolling: touch;
 }
+.wrapper {
+  height: 100%;
+}
 #header {
   background: #fff;
 }
 .content-wrapper {
   position: relative;
+  height: 100%;
 }
 .control-relative {
   position: relative;
 }
-.tab-addtabs {
-  overflow: hidden;
-}
 .tab-addtabs .tab-pane {
   height: 100%;
   width: 100%;

文件差異過大導致無法顯示
+ 1 - 1
public/assets/css/backend.min.css


+ 68 - 73
public/assets/js/adminlte.js

@@ -148,8 +148,8 @@ $(function () {
     //Extend options if external options exist
     if (typeof AdminLTEOptions !== "undefined") {
         $.extend(true,
-                $.AdminLTE.options,
-                AdminLTEOptions);
+            $.AdminLTE.options,
+            AdminLTEOptions);
     }
 
     //Easy access to options
@@ -245,11 +245,11 @@ function _init() {
     $.AdminLTE.layout = {
         activate: function () {
             var _this = this;
-            _this.fix();
+            //_this.fix();
             _this.fixSidebar();
-            $('body, html, .wrapper').css('height', 'auto');
+            //$('body, html, .wrapper').css('height', 'auto');
             $(window, ".wrapper").resize(function () {
-                _this.fix();
+                //_this.fix();
                 _this.fixSidebar();
             });
         },
@@ -365,14 +365,14 @@ function _init() {
             //Expand sidebar on hover
             $('.main-sidebar').hover(function () {
                 if ($('body').hasClass('sidebar-mini')
-                        && $("body").hasClass('sidebar-collapse')
-                        && $(window).width() > screenWidth) {
+                    && $("body").hasClass('sidebar-collapse')
+                    && $(window).width() > screenWidth) {
                     _this.expand();
                 }
             }, function () {
                 if ($('body').hasClass('sidebar-mini')
-                        && $('body').hasClass('sidebar-expanded-on-hover')
-                        && $(window).width() > screenWidth) {
+                    && $('body').hasClass('sidebar-expanded-on-hover')
+                    && $(window).width() > screenWidth) {
                     _this.collapse();
                 }
             });
@@ -399,58 +399,58 @@ function _init() {
         var _this = this;
         var animationSpeed = $.AdminLTE.options.animationSpeed;
         $(document).off('click', menu + ' li a')
-                .on('click', menu + ' li a', function (e) {
-                    //Get the clicked link and the next element
-                    var $this = $(this);
-                    var checkElement = $this.next();
-
-                    //Check if the next element is a menu and is visible
-                    if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')) && (!$('body').hasClass('sidebar-collapse'))) {
-                        //Close the menu
-                        checkElement.slideUp(animationSpeed, function () {
-                            checkElement.removeClass('menu-open');
-                            //Fix the layout in case the sidebar stretches over the height of the window
-                            //_this.layout.fix();
-                        });
-                        checkElement.parent("li").removeClass("active");
+            .on('click', menu + ' li a', function (e) {
+                //Get the clicked link and the next element
+                var $this = $(this);
+                var checkElement = $this.next();
+
+                //Check if the next element is a menu and is visible
+                if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')) && (!$('body').hasClass('sidebar-collapse'))) {
+                    //Close the menu
+                    checkElement.slideUp(animationSpeed, function () {
+                        checkElement.removeClass('menu-open');
+                        //Fix the layout in case the sidebar stretches over the height of the window
+                        //_this.layout.fix();
+                    });
+                    checkElement.parent("li").removeClass("active");
+                }
+                //If the menu is not visible
+                else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible'))) {
+                    //Get the parent menu
+                    var parent = $this.parents('ul').first();
+                    // modified by FastAdmin
+                    if ($(".show-submenu", menu).size() == 0) {
+                        //Close all open menus within the parent
+                        var ul = parent.find('ul:visible').slideUp(animationSpeed);
+                        //Remove the menu-open class from the parent
+                        ul.removeClass('menu-open');
                     }
-                    //If the menu is not visible
-                    else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible'))) {
-                        //Get the parent menu
-                        var parent = $this.parents('ul').first();
-                        // modified by FastAdmin
-                        if ($(".show-submenu", menu).size() == 0) {
-                            //Close all open menus within the parent
-                            var ul = parent.find('ul:visible').slideUp(animationSpeed);
-                            //Remove the menu-open class from the parent
-                            ul.removeClass('menu-open');
-                        }
-                        //Get the parent li
-                        var parent_li = $this.parent("li");
-
-                        //Open the target menu and add the menu-open class
-                        checkElement.slideDown(animationSpeed, function () {
-                            //Add the class active to the parent li
-                            checkElement.addClass('menu-open');
-                            //parent.find('li.active').removeClass('active');
-                            //parent_li.addClass('active');
-                            //Fix the layout in case the sidebar stretches over the height of the window
-                            _this.layout.fix();
-                        });
-                    } else {
-                        if (!$this.parent().hasClass("active")) {
-                            $this.parent().addClass("active");
-                        }
-                        // modified by FastAdmin
-                        if ($(".show-submenu", menu).size() == 0) {
-                            $this.parent().siblings().find("ul.menu-open").slideUp();
-                        }
+                    //Get the parent li
+                    var parent_li = $this.parent("li");
+
+                    //Open the target menu and add the menu-open class
+                    checkElement.slideDown(animationSpeed, function () {
+                        //Add the class active to the parent li
+                        checkElement.addClass('menu-open');
+                        //parent.find('li.active').removeClass('active');
+                        //parent_li.addClass('active');
+                        //Fix the layout in case the sidebar stretches over the height of the window
+                        _this.layout.fix();
+                    });
+                } else {
+                    if (!$this.parent().hasClass("active")) {
+                        $this.parent().addClass("active");
                     }
-                    //if this isn't a link, prevent the page from being redirected
-                    if (checkElement.is('.treeview-menu')) {
-                        e.preventDefault();
+                    // modified by FastAdmin
+                    if ($(".show-submenu", menu).size() == 0) {
+                        $this.parent().siblings().find("ul.menu-open").slideUp();
                     }
-                });
+                }
+                //if this isn't a link, prevent the page from being redirected
+                if (checkElement.is('.treeview-menu')) {
+                    e.preventDefault();
+                }
+            });
     };
 
     /* ControlSidebar
@@ -477,7 +477,7 @@ function _init() {
                 e.preventDefault();
                 //If the sidebar is not open
                 if (!sidebar.hasClass('control-sidebar-open')
-                        && !$('body').hasClass('control-sidebar-open')) {
+                    && !$('body').hasClass('control-sidebar-open')) {
                     //Open the sidebar
                     _this.open(sidebar, o.slide);
                 } else {
@@ -588,8 +588,8 @@ function _init() {
             if (!box.hasClass("collapsed-box")) {
                 //Convert minus into plus
                 element.children(":first")
-                        .removeClass(_this.icons.collapse)
-                        .addClass(_this.icons.open);
+                    .removeClass(_this.icons.collapse)
+                    .addClass(_this.icons.open);
                 //Hide the content
                 box_content.slideUp(_this.animationSpeed, function () {
                     box.addClass("collapsed-box");
@@ -597,8 +597,8 @@ function _init() {
             } else {
                 //Convert plus into minus
                 element.children(":first")
-                        .removeClass(_this.icons.open)
-                        .addClass(_this.icons.collapse);
+                    .removeClass(_this.icons.open)
+                    .addClass(_this.icons.collapse);
                 //Show the content
                 box_content.slideDown(_this.animationSpeed, function () {
                     box.removeClass("collapsed-box");
@@ -782,16 +782,13 @@ function _init() {
     };
 
     //set/get form element value
-    $.fn.field = function (name, value)
-    {
+    $.fn.field = function (name, value) {
         if (typeof name !== "string")
             return false;
         var element = $(this).find("[name='" + name + "']");
 
-        if (typeof value === "undefined" && element.length >= 1)
-        {
-            switch (element.attr("type"))
-            {
+        if (typeof value === "undefined" && element.length >= 1) {
+            switch (element.attr("type")) {
                 case "checkbox":
                     var result = new Array();
                     element.each(function (i, val) {
@@ -814,10 +811,8 @@ function _init() {
                     return element.val();
                     break;
             }
-        } else
-        {
-            switch (element.attr("type"))
-            {
+        } else {
+            switch (element.attr("type")) {
                 case "checkbox":
                 case "radio":
                     value = $.isArray(value) ? value : [value];

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

@@ -10,7 +10,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     edit_url: 'category/edit',
                     del_url: 'category/del',
                     multi_url: 'category/multi',
-                    dragsort_url: '',
+                    dragsort_url: 'ajax/weigh',
                     table: 'category',
                 }
             });

+ 26 - 27
public/assets/js/backend/dashboard.js

@@ -29,37 +29,33 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'table', 'echarts', 'echart
                     boundaryGap: false,
                     data: Orderdata.column
                 },
-                yAxis: {
-
-                },
+                yAxis: {},
                 grid: [{
-                        left: 'left',
-                        top: 'top',
-                        right: '10',
-                        bottom: 30
-                    }],
+                    left: 'left',
+                    top: 'top',
+                    right: '10',
+                    bottom: 30
+                }],
                 series: [{
-                        name: __('Sales'),
-                        type: 'line',
-                        smooth: true,
-                        areaStyle: {
-                            normal: {
-                            }
-                        },
-                        lineStyle: {
-                            normal: {
-                                width: 1.5
-                            }
-                        },
-                        data: Orderdata.paydata
+                    name: __('Sales'),
+                    type: 'line',
+                    smooth: true,
+                    areaStyle: {
+                        normal: {}
                     },
+                    lineStyle: {
+                        normal: {
+                            width: 1.5
+                        }
+                    },
+                    data: Orderdata.paydata
+                },
                     {
                         name: __('Orders'),
                         type: 'line',
                         smooth: true,
                         areaStyle: {
-                            normal: {
-                            }
+                            normal: {}
                         },
                         lineStyle: {
                             normal: {
@@ -92,20 +88,23 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'table', 'echarts', 'echart
                         data: Orderdata.column
                     },
                     series: [{
-                            name: __('Sales'),
-                            data: Orderdata.paydata
-                        },
+                        name: __('Sales'),
+                        data: Orderdata.paydata
+                    },
                         {
                             name: __('Orders'),
                             data: Orderdata.createdata
                         }]
                 });
+                if ($("#echart").width() != $("#echart canvas").width() && $("#echart canvas").width() < $("#echart").width()) {
+                    myChart.resize();
+                }
             }, 2000);
             $(window).resize(function () {
                 myChart.resize();
             });
 
-            $(document).on("click", ".btn-checkversion", function(){
+            $(document).on("click", ".btn-checkversion", function () {
                 top.window.$("[data-toggle=checkupdate]").trigger("click");
             });
 

+ 0 - 5
public/assets/js/backend/index.js

@@ -1,11 +1,6 @@
 define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], function ($, undefined, Backend, undefined, AdminLTE, Form) {
     var Controller = {
         index: function () {
-            //窗口大小改变,修正主窗体最小高度
-            $(window).resize(function () {
-                $(".tab-addtabs").css("height", $(".content-wrapper").height() + "px");
-            });
-
             //双击重新加载页面
             $(document).on("dblclick", ".sidebar-menu li > a", function (e) {
                 $("#con_" + $(this).attr("addtabs") + " iframe").attr('src', function (i, val) {

+ 15 - 13
public/assets/js/require-backend.min.js

@@ -10517,18 +10517,21 @@ define("drop", function(){});
                 if (options.close && $("li", navobj).size() > 0) {
                     tabitem.append(' <i class="close-tab fa fa-remove"></i>');
                 }
-                //创建新TAB的内容
-                conitem = $('<div role="tabpanel" class="tab-pane" id="' + conid + '"></div>');
-                //是否指定TAB内容
-                if (opts.content) {
-                    conitem.append(opts.content);
-                } else if (options.iframeUse && !opts.ajax) {//没有内容,使用IFRAME打开链接
-                    var height = options.iframeHeight;
-                    conitem.append('<iframe src="' + url + '" width="100%" height="' + height + '" frameborder="no" border="0" marginwidth="0" marginheight="0" scrolling-x="no" scrolling-y="auto" allowtransparency="yes"></iframe></div>');
-                } else {
-                    $.get(url, function (data) {
-                        conitem.append(data);
-                    });
+                if (conitem.size() === 0) {
+                    //创建新TAB的内容
+                    conitem = $('<div role="tabpanel" class="tab-pane" id="' + conid + '"></div>');
+                    //是否指定TAB内容
+                    if (opts.content) {
+                        conitem.append(opts.content);
+                    } else if (options.iframeUse && !opts.ajax) {//没有内容,使用IFRAME打开链接
+                        var height = options.iframeHeight;
+                        conitem.append('<iframe src="' + url + '" width="100%" height="' + height + '" frameborder="no" border="0" marginwidth="0" marginheight="0" scrolling-x="no" scrolling-y="auto" allowtransparency="yes"></iframe></div>');
+                    } else {
+                        $.get(url, function (data) {
+                            conitem.append(data);
+                        });
+                    }
+                    tabobj.append(conitem);
                 }
                 //加入TABS
                 if ($('.tabdrop li', navobj).size() > 0) {
@@ -10536,7 +10539,6 @@ define("drop", function(){});
                 } else {
                     navobj.append(tabitem);
                 }
-                tabobj.append(conitem);
             } else {
                 //强制刷新iframe
                 if (options.iframeForceRefresh) {

+ 10 - 2
public/assets/less/backend.less

@@ -21,6 +21,11 @@
 @panel-intro-bg: darken(@main-bg, 3%);
 @panel-nav-bg: #fff;
 
+html,
+body {
+  height: 100%;
+}
+
 body {
   background: #f1f4f6;
   font-size: 13px;
@@ -56,7 +61,9 @@ html.ios-fix, html.ios-fix body {
   overflow: auto;
   -webkit-overflow-scrolling: touch;
 }
-
+.wrapper {
+  height: 100%;
+}
 #header {
   background: #fff;
   //box-shadow: 0 2px 2px rgba(0,0,0,.05),0 1px 0 rgba(0,0,0,.05);
@@ -64,6 +71,7 @@ html.ios-fix, html.ios-fix body {
 
 .content-wrapper {
   position: relative;
+  height:100%;
 }
 
 .control-relative {
@@ -71,7 +79,7 @@ html.ios-fix, html.ios-fix body {
 }
 
 .tab-addtabs {
-  overflow: hidden;
+  //overflow: hidden;
   .tab-pane {
     height: 100%;
     width: 100%;