Prechádzať zdrojové kódy

新增插件分类、免费插件搜索
新增全局的Template
新增自定义通用搜索表单内容
新增通用搜索按钮显示配置
新增单独清除模板、插件缓存
新增后台登录失败重试配置
优化通用搜索,搜索元素支持自动绑定元素事件
优化本地插件显示
优化前台首页和API文档字体显示
修复元素验证指定data-target不生效的BUG
修复插件命令行添加--force不生效的BUG
修复noNeedLogin和noNeedRight大小写的BUG
修复fieldlist无法挺拽的BUG
修复一键CRUD后指定字段显示后无法显示关联数据的BUG

Karson 7 rokov pred
rodič
commit
b941f0a3e4

+ 81 - 127
application/admin/command/Addon.php

@@ -19,14 +19,14 @@ class Addon extends Command
     protected function configure()
     {
         $this
-                ->setName('addon')
-                ->addOption('name', 'a', Option::VALUE_REQUIRED, 'addon name', null)
-                ->addOption('action', 'c', Option::VALUE_REQUIRED, 'action(create/enable/disable/install/uninstall/refresh/upgrade/package)', 'create')
-                ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', null)
-                ->addOption('release', 'r', Option::VALUE_OPTIONAL, 'addon release version', null)
-                ->addOption('uid', 'u', Option::VALUE_OPTIONAL, 'fastadmin uid', null)
-                ->addOption('token', 't', Option::VALUE_OPTIONAL, 'fastadmin token', null)
-                ->setDescription('Addon manager');
+            ->setName('addon')
+            ->addOption('name', 'a', Option::VALUE_REQUIRED, 'addon name', null)
+            ->addOption('action', 'c', Option::VALUE_REQUIRED, 'action(create/enable/disable/install/uninstall/refresh/upgrade/package)', 'create')
+            ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', null)
+            ->addOption('release', 'r', Option::VALUE_OPTIONAL, 'addon release version', null)
+            ->addOption('uid', 'u', Option::VALUE_OPTIONAL, 'fastadmin uid', null)
+            ->addOption('token', 't', Option::VALUE_OPTIONAL, 'fastadmin token', null)
+            ->setDescription('Addon manager');
     }
 
     protected function execute(Input $input, Output $output)
@@ -44,12 +44,10 @@ class Addon extends Command
 
         include dirname(__DIR__) . DS . 'common.php';
 
-        if (!$name)
-        {
+        if (!$name) {
             throw new Exception('Addon name could not be empty');
         }
-        if (!$action || !in_array($action, ['create', 'disable', 'enable', 'install', 'uninstall', 'refresh', 'upgrade', 'package']))
-        {
+        if (!$action || !in_array($action, ['create', 'disable', 'enable', 'install', 'uninstall', 'refresh', 'upgrade', 'package'])) {
             throw new Exception('Please input correct action name');
         }
 
@@ -57,17 +55,14 @@ class Addon extends Command
         Db::execute("SELECT 1");
 
         $addonDir = ADDON_PATH . $name . DS;
-        switch ($action)
-        {
+        switch ($action) {
             case 'create':
                 //非覆盖模式时如果存在则报错
-                if (is_dir($addonDir) && !$force)
-                {
+                if (is_dir($addonDir) && !$force) {
                     throw new Exception("addon already exists!\nIf you need to create again, use the parameter --force=true ");
                 }
                 //如果存在先移除
-                if (is_dir($addonDir))
-                {
+                if (is_dir($addonDir)) {
                     rmdirs($addonDir);
                 }
                 mkdir($addonDir);
@@ -76,16 +71,12 @@ class Addon extends Command
                 $createMenu = $this->getCreateMenu($menuList);
                 $prefix = Config::get('database.prefix');
                 $createTableSql = '';
-                try
-                {
+                try {
                     $result = Db::query("SHOW CREATE TABLE `" . $prefix . $name . "`;");
-                    if (isset($result[0]) && isset($result[0]['Create Table']))
-                    {
+                    if (isset($result[0]) && isset($result[0]['Create Table'])) {
                         $createTableSql = $result[0]['Create Table'];
                     }
-                }
-                catch (PDOException $e)
-                {
+                } catch (PDOException $e) {
 
                 }
 
@@ -102,8 +93,7 @@ class Addon extends Command
                 $this->writeToFile("config", $data, $addonDir . 'config.php');
                 $this->writeToFile("info", $data, $addonDir . 'info.ini');
                 $this->writeToFile("controller", $data, $addonDir . 'controller' . DS . 'Index.php');
-                if ($createTableSql)
-                {
+                if ($createTableSql) {
                     $createTableSql = str_replace("`" . $prefix, '`__PREFIX__', $createTableSql);
                     file_put_contents($addonDir . 'install.sql', $createTableSql);
                 }
@@ -112,75 +102,61 @@ class Addon extends Command
                 break;
             case 'disable':
             case 'enable':
-                try
-                {
+                try {
                     //调用启用、禁用的方法
                     Service::$action($name, 0);
-                }
-                catch (AddonException $e)
-                {
-                    if ($e->getCode() != -3)
-                    {
+                } catch (AddonException $e) {
+                    if ($e->getCode() != -3) {
                         throw new Exception($e->getMessage());
                     }
-                    //如果有冲突文件则提醒
-                    $data = $e->getData();
-                    foreach ($data['conflictlist'] as $k => $v)
-                    {
-                        $output->warning($v);
-                    }
-                    $output->info("Are you sure you want to " . ($action == 'enable' ? 'override' : 'delete') . " all those files?  Type 'yes' to continue: ");
-                    $line = fgets(STDIN);
-                    if (trim($line) != 'yes')
-                    {
-                        throw new Exception("Operation is aborted!");
+                    if (!$force) {
+                        //如果有冲突文件则提醒
+                        $data = $e->getData();
+                        foreach ($data['conflictlist'] as $k => $v) {
+                            $output->warning($v);
+                        }
+                        $output->info("Are you sure you want to " . ($action == 'enable' ? 'override' : 'delete') . " all those files?  Type 'yes' to continue: ");
+                        $line = fgets(STDIN);
+                        if (trim($line) != 'yes') {
+                            throw new Exception("Operation is aborted!");
+                        }
                     }
                     //调用启用、禁用的方法
                     Service::$action($name, 1);
-                }
-                catch (Exception $e)
-                {
+                } catch (Exception $e) {
                     throw new Exception($e->getMessage());
                 }
                 $output->info(ucfirst($action) . " Successed!");
                 break;
             case 'install':
                 //非覆盖模式时如果存在则报错
-                if (is_dir($addonDir) && !$force)
-                {
+                if (is_dir($addonDir) && !$force) {
                     throw new Exception("addon already exists!\nIf you need to install again, use the parameter --force=true ");
                 }
                 //如果存在先移除
-                if (is_dir($addonDir))
-                {
+                if (is_dir($addonDir)) {
                     rmdirs($addonDir);
                 }
-                try
-                {
+                try {
                     Service::install($name, 0, ['version' => $release]);
-                }
-                catch (AddonException $e)
-                {
-                    if ($e->getCode() != -3)
-                    {
+                } catch (AddonException $e) {
+                    if ($e->getCode() != -3) {
                         throw new Exception($e->getMessage());
                     }
-                    //如果有冲突文件则提醒
-                    $data = $e->getData();
-                    foreach ($data['conflictlist'] as $k => $v)
-                    {
-                        $output->warning($v);
-                    }
-                    $output->info("Are you sure you want to override all those files?  Type 'yes' to continue: ");
-                    $line = fgets(STDIN);
-                    if (trim($line) != 'yes')
-                    {
-                        throw new Exception("Operation is aborted!");
+                    if (!$force) {
+                        //如果有冲突文件则提醒
+                        $data = $e->getData();
+                        foreach ($data['conflictlist'] as $k => $v) {
+                            $output->warning($v);
+                        }
+                        $output->info("Are you sure you want to override all those files?  Type 'yes' to continue: ");
+                        $line = fgets(STDIN);
+                        if (trim($line) != 'yes') {
+                            throw new Exception("Operation is aborted!");
+                        }
                     }
                     Service::install($name, 1, ['version' => $release, 'uid' => $uid, 'token' => $token]);
-                }
-                catch (Exception $e)
-                {
+                } catch (Exception $e) {
                     throw new Exception($e->getMessage());
                 }
 
@@ -188,36 +164,29 @@ class Addon extends Command
                 break;
             case 'uninstall':
                 //非覆盖模式时如果存在则报错
-                if (!$force)
-                {
+                if (!$force) {
                     throw new Exception("If you need to uninstall addon, use the parameter --force=true ");
                 }
-                try
-                {
+                try {
                     Service::uninstall($name, 0);
-                }
-                catch (AddonException $e)
-                {
-                    if ($e->getCode() != -3)
-                    {
+                } catch (AddonException $e) {
+                    if ($e->getCode() != -3) {
                         throw new Exception($e->getMessage());
                     }
-                    //如果有冲突文件则提醒
-                    $data = $e->getData();
-                    foreach ($data['conflictlist'] as $k => $v)
-                    {
-                        $output->warning($v);
-                    }
-                    $output->info("Are you sure you want to delete all those files?  Type 'yes' to continue: ");
-                    $line = fgets(STDIN);
-                    if (trim($line) != 'yes')
-                    {
-                        throw new Exception("Operation is aborted!");
+                    if (!$force) {
+                        //如果有冲突文件则提醒
+                        $data = $e->getData();
+                        foreach ($data['conflictlist'] as $k => $v) {
+                            $output->warning($v);
+                        }
+                        $output->info("Are you sure you want to delete all those files?  Type 'yes' to continue: ");
+                        $line = fgets(STDIN);
+                        if (trim($line) != 'yes') {
+                            throw new Exception("Operation is aborted!");
+                        }
                     }
                     Service::uninstall($name, 1);
-                }
-                catch (Exception $e)
-                {
+                } catch (Exception $e) {
                     throw new Exception($e->getMessage());
                 }
 
@@ -233,53 +202,44 @@ class Addon extends Command
                 break;
             case 'package':
                 $infoFile = $addonDir . 'info.ini';
-                if (!is_file($infoFile))
-                {
+                if (!is_file($infoFile)) {
                     throw new Exception(__('Addon info file was not found'));
                 }
 
                 $info = get_addon_info($name);
-                if (!$info)
-                {
+                if (!$info) {
                     throw new Exception(__('Addon info file data incorrect'));
                 }
                 $infoname = isset($info['name']) ? $info['name'] : '';
-                if (!$infoname || !preg_match("/^[a-z]+$/i", $infoname) || $infoname != $name)
-                {
+                if (!$infoname || !preg_match("/^[a-z]+$/i", $infoname) || $infoname != $name) {
                     throw new Exception(__('Addon info name incorrect'));
                 }
 
                 $infoversion = isset($info['version']) ? $info['version'] : '';
-                if (!$infoversion || !preg_match("/^\d+\.\d+\.\d+$/i", $infoversion))
-                {
+                if (!$infoversion || !preg_match("/^\d+\.\d+\.\d+$/i", $infoversion)) {
                     throw new Exception(__('Addon info version incorrect'));
                 }
 
                 $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
-                if (!is_dir($addonTmpDir))
-                {
+                if (!is_dir($addonTmpDir)) {
                     @mkdir($addonTmpDir, 0755, true);
                 }
                 $addonFile = $addonTmpDir . $infoname . '-' . $infoversion . '.zip';
-                if (!class_exists('ZipArchive'))
-                {
+                if (!class_exists('ZipArchive')) {
                     throw new Exception(__('ZinArchive not install'));
                 }
                 $zip = new \ZipArchive;
                 $zip->open($addonFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
 
                 $files = new \RecursiveIteratorIterator(
-                        new \RecursiveDirectoryIterator($addonDir), \RecursiveIteratorIterator::LEAVES_ONLY
+                    new \RecursiveDirectoryIterator($addonDir), \RecursiveIteratorIterator::LEAVES_ONLY
                 );
 
-                foreach ($files as $name => $file)
-                {
-                    if (!$file->isDir())
-                    {
+                foreach ($files as $name => $file) {
+                    if (!$file->isDir()) {
                         $filePath = $file->getRealPath();
                         $relativePath = substr($filePath, strlen($addonDir));
-                        if (!in_array($file->getFilename(), ['.git', '.DS_Store', 'Thumbs.db']))
-                        {
+                        if (!in_array($file->getFilename(), ['.git', '.DS_Store', 'Thumbs.db'])) {
                             $zip->addFile($filePath, $relativePath);
                         }
                     }
@@ -301,22 +261,18 @@ class Addon extends Command
     protected function getCreateMenu($menu)
     {
         $result = [];
-        foreach ($menu as $k => & $v)
-        {
+        foreach ($menu as $k => & $v) {
             $arr = [
                 'name'  => $v['name'],
                 'title' => $v['title'],
             ];
-            if ($v['icon'] != 'fa fa-circle-o')
-            {
+            if ($v['icon'] != 'fa fa-circle-o') {
                 $arr['icon'] = $v['icon'];
             }
-            if ($v['ismenu'])
-            {
+            if ($v['ismenu']) {
                 $arr['ismenu'] = $v['ismenu'];
             }
-            if (isset($v['childlist']) && $v['childlist'])
-            {
+            if (isset($v['childlist']) && $v['childlist']) {
                 $arr['sublist'] = $this->getCreateMenu($v['childlist']);
             }
             $result[] = $arr;
@@ -334,16 +290,14 @@ class Addon extends Command
     protected function writeToFile($name, $data, $pathname)
     {
         $search = $replace = [];
-        foreach ($data as $k => $v)
-        {
+        foreach ($data as $k => $v) {
             $search[] = "{%{$k}%}";
             $replace[] = $v;
         }
         $stub = file_get_contents($this->getStub($name));
         $content = str_replace($search, $replace, $stub);
 
-        if (!is_dir(dirname($pathname)))
-        {
+        if (!is_dir(dirname($pathname))) {
             mkdir(strtolower(dirname($pathname)), 0755, true);
         }
         return file_put_contents($pathname, $content);

+ 9 - 1
application/admin/command/Api/template/index.html

@@ -9,7 +9,15 @@
         <title>{$config.title}</title>
         <link href="https://cdn.bootcss.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
         <style type="text/css">
-            body      { padding-top: 70px; margin-bottom: 15px; }
+            body {
+                padding-top: 70px; margin-bottom: 15px;
+                -webkit-font-smoothing: antialiased;
+                -moz-osx-font-smoothing: grayscale;
+                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; }
+            hr        { margin-top: 10px; }
             .tab-pane { padding-top: 10px; }
             .mt0      { margin-top: 0px; }
             .footer   { font-size: 12px; color: #666; }

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

@@ -654,9 +654,10 @@ class Crud extends Command
                         $priDefined = TRUE;
                         $javascriptList[] = "{checkbox: true}";
                     }
-                    //构造JS列信息
-                    $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], $inputType && in_array($inputType, ['select', 'checkbox', 'radio']) ? '_text' : '', $itemArr);
-
+                    if (!$fields || in_array($field, explode(',', $fields))) {
+                        //构造JS列信息
+                        $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], $inputType && in_array($inputType, ['select', 'checkbox', 'radio']) ? '_text' : '', $itemArr);
+                    }
                     //排序方式,如果有指定排序字段,否则按主键排序
                     $order = $field == $this->sortField ? $this->sortField : $order;
                 }
@@ -767,6 +768,11 @@ class Crud extends Command
                     //构造关联模型的方法
                     $relationMethodList[] = $this->getReplacedStub('mixins' . DS . 'modelrelationmethod', $relation);
 
+                    //如果设置了显示主表字段,则必须显式将关联表字段显示
+                    if ($fields) {
+                        $relationVisibleFieldList[] = "\$row->visible(['{$relation['relationMethod']}']);";
+                    }
+
                     //显示的字段
                     if ($relation['relationFields']) {
                         $relationVisibleFieldList[] = "\$row->getRelation('" . $relation['relationMethod'] . "')->visible(['" . implode("','", $relation['relationFields']) . "']);";

+ 76 - 140
application/admin/controller/Addon.php

@@ -3,6 +3,7 @@
 namespace app\admin\controller;
 
 use app\common\controller\Backend;
+use fast\Http;
 use think\addons\AddonException;
 use think\addons\Service;
 use think\Cache;
@@ -31,8 +32,7 @@ class Addon extends Backend
     public function index()
     {
         $addons = get_addon_list();
-        foreach ($addons as $k => &$v)
-        {
+        foreach ($addons as $k => &$v) {
             $config = get_addon_config($v['name']);
             $v['config'] = $config ? 1 : 0;
         }
@@ -46,60 +46,31 @@ class Addon extends Backend
     public function config($ids = NULL)
     {
         $name = $this->request->get("name");
-        if (!$name)
-        {
+        if (!$name) {
             $this->error(__('Parameter %s can not be empty', $ids ? 'id' : 'name'));
         }
-        if (!is_dir(ADDON_PATH . $name))
-        {
+        if (!is_dir(ADDON_PATH . $name)) {
             $this->error(__('Directory not found'));
         }
         $info = get_addon_info($name);
         $config = get_addon_fullconfig($name);
         if (!$info)
             $this->error(__('No Results were found'));
-        if ($this->request->isPost())
-        {
+        if ($this->request->isPost()) {
             $params = $this->request->post("row/a");
-            if ($params)
-            {
-                foreach ($config as $k => &$v)
-                {
-                    if (isset($params[$v['name']]))
-                    {
-                        if ($v['type'] == 'array')
-                        {
-                            $fieldarr = $valuearr = [];
-                            $field = $params[$v['name']]['field'];
-                            $value = $params[$v['name']]['value'];
-
-                            foreach ($field as $m => $n)
-                            {
-                                if ($n != '')
-                                {
-                                    $fieldarr[] = $field[$m];
-                                    $valuearr[] = $value[$m];
-                                }
-                            }
-                            $params[$v['name']] = array_combine($fieldarr, $valuearr);
-                            $value = $params[$v['name']];
-                        }
-                        else
-                        {
-                            $value = is_array($params[$v['name']]) ? implode(',', $params[$v['name']]) : $params[$v['name']];
-                        }
-
+            if ($params) {
+                foreach ($config as $k => &$v) {
+                    if (isset($params[$v['name']])) {
+                        $value = is_array($params[$v['name']]) ? implode(',', $params[$v['name']]) : $params[$v['name']];
                         $v['value'] = $value;
                     }
                 }
-                try
-                {
+                try {
                     //更新配置文件
                     set_addon_fullconfig($name, $config);
+                    Service::refresh();
                     $this->success();
-                }
-                catch (Exception $e)
-                {
+                } catch (Exception $e) {
                     $this->error($e->getMessage());
                 }
             }
@@ -115,13 +86,11 @@ class Addon extends Backend
     public function install()
     {
         $name = $this->request->post("name");
-        $force = (int) $this->request->post("force");
-        if (!$name)
-        {
+        $force = (int)$this->request->post("force");
+        if (!$name) {
             $this->error(__('Parameter %s can not be empty', 'name'));
         }
-        try
-        {
+        try {
             $uid = $this->request->post("uid");
             $token = $this->request->post("token");
             $version = $this->request->post("version");
@@ -137,13 +106,9 @@ class Addon extends Backend
             $info['config'] = get_addon_config($name) ? 1 : 0;
             $info['state'] = 1;
             $this->success(__('Install successful'), null, ['addon' => $info]);
-        }
-        catch (AddonException $e)
-        {
+        } catch (AddonException $e) {
             $this->result($e->getData(), $e->getCode(), $e->getMessage());
-        }
-        catch (Exception $e)
-        {
+        } catch (Exception $e) {
             $this->error($e->getMessage(), $e->getCode());
         }
     }
@@ -154,22 +119,16 @@ class Addon extends Backend
     public function uninstall()
     {
         $name = $this->request->post("name");
-        $force = (int) $this->request->post("force");
-        if (!$name)
-        {
+        $force = (int)$this->request->post("force");
+        if (!$name) {
             $this->error(__('Parameter %s can not be empty', 'name'));
         }
-        try
-        {
+        try {
             Service::uninstall($name, $force);
             $this->success(__('Uninstall successful'));
-        }
-        catch (AddonException $e)
-        {
+        } catch (AddonException $e) {
             $this->result($e->getData(), $e->getCode(), $e->getMessage());
-        }
-        catch (Exception $e)
-        {
+        } catch (Exception $e) {
             $this->error($e->getMessage());
         }
     }
@@ -181,25 +140,19 @@ class Addon extends Backend
     {
         $name = $this->request->post("name");
         $action = $this->request->post("action");
-        $force = (int) $this->request->post("force");
-        if (!$name)
-        {
+        $force = (int)$this->request->post("force");
+        if (!$name) {
             $this->error(__('Parameter %s can not be empty', 'name'));
         }
-        try
-        {
+        try {
             $action = $action == 'enable' ? $action : 'disable';
             //调用启用、禁用的方法
             Service::$action($name, $force);
             Cache::rm('__menu__');
             $this->success(__('Operate successful'));
-        }
-        catch (AddonException $e)
-        {
+        } catch (AddonException $e) {
             $this->result($e->getData(), $e->getCode(), $e->getMessage());
-        }
-        catch (Exception $e)
-        {
+        } catch (Exception $e) {
             $this->error($e->getMessage());
         }
     }
@@ -213,55 +166,46 @@ class Addon extends Backend
 
         $file = $this->request->file('file');
         $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
-        if (!is_dir($addonTmpDir))
-        {
+        if (!is_dir($addonTmpDir)) {
             @mkdir($addonTmpDir, 0755, true);
         }
         $info = $file->rule('uniqid')->validate(['size' => 10240000, 'ext' => 'zip'])->move($addonTmpDir);
-        if ($info)
-        {
+        if ($info) {
             $tmpName = substr($info->getFilename(), 0, stripos($info->getFilename(), '.'));
             $tmpAddonDir = ADDON_PATH . $tmpName . DS;
             $tmpFile = $addonTmpDir . $info->getSaveName();
-            try
-            {
+            try {
                 Service::unzip($tmpName);
                 @unlink($tmpFile);
                 $infoFile = $tmpAddonDir . 'info.ini';
-                if (!is_file($infoFile))
-                {
+                if (!is_file($infoFile)) {
                     throw new Exception(__('Addon info file was not found'));
                 }
 
                 $config = Config::parse($infoFile, '', $tmpName);
                 $name = isset($config['name']) ? $config['name'] : '';
-                if (!$name)
-                {
+                if (!$name) {
                     throw new Exception(__('Addon info file data incorrect'));
                 }
 
                 $newAddonDir = ADDON_PATH . $name . DS;
-                if (is_dir($newAddonDir))
-                {
+                if (is_dir($newAddonDir)) {
                     throw new Exception(__('Addon already exists'));
                 }
 
                 //重命名插件文件夹
                 rename($tmpAddonDir, $newAddonDir);
-                try
-                {
+                try {
                     //默认禁用该插件
                     $info = get_addon_info($name);
-                    if ($info['state'])
-                    {
+                    if ($info['state']) {
                         $info['state'] = 0;
                         set_addon_info($name, $info);
                     }
 
                     //执行插件的安装方法
                     $class = get_addon_class($name);
-                    if (class_exists($class))
-                    {
+                    if (class_exists($class)) {
                         $addon = new $class();
                         $addon->install();
                     }
@@ -271,22 +215,16 @@ class Addon extends Backend
 
                     $info['config'] = get_addon_config($name) ? 1 : 0;
                     $this->success(__('Offline installed tips'), null, ['addon' => $info]);
-                }
-                catch (Exception $e)
-                {
+                } catch (Exception $e) {
                     @rmdirs($newAddonDir);
                     throw new Exception($e->getMessage());
                 }
-            }
-            catch (Exception $e)
-            {
+            } catch (Exception $e) {
                 @unlink($tmpFile);
                 @rmdirs($tmpAddonDir);
                 $this->error($e->getMessage());
             }
-        }
-        else
-        {
+        } else {
             // 上传失败获取错误信息
             $this->error($file->getError());
         }
@@ -298,12 +236,10 @@ class Addon extends Backend
     public function upgrade()
     {
         $name = $this->request->post("name");
-        if (!$name)
-        {
+        if (!$name) {
             $this->error(__('Parameter %s can not be empty', 'name'));
         }
-        try
-        {
+        try {
             $uid = $this->request->post("uid");
             $token = $this->request->post("token");
             $version = $this->request->post("version");
@@ -318,29 +254,9 @@ class Addon extends Backend
             Service::upgrade($name, $extend);
             Cache::rm('__menu__');
             $this->success(__('Operate successful'));
-        }
-        catch (AddonException $e)
-        {
+        } catch (AddonException $e) {
             $this->result($e->getData(), $e->getCode(), $e->getMessage());
-        }
-        catch (Exception $e)
-        {
-            $this->error($e->getMessage());
-        }
-    }
-
-    /**
-     * 刷新缓存
-     */
-    public function refresh()
-    {
-        try
-        {
-            Service::refresh();
-            $this->success(__('Operate successful'));
-        }
-        catch (Exception $e)
-        {
+        } catch (Exception $e) {
             $this->error($e->getMessage());
         }
     }
@@ -350,31 +266,51 @@ class Addon extends Backend
      */
     public function downloaded()
     {
-        $offset = (int) $this->request->get("offset");
-        $limit = (int) $this->request->get("limit");
+        $offset = (int)$this->request->get("offset");
+        $limit = (int)$this->request->get("limit");
+        $filter = $this->request->get("filter");
         $search = $this->request->get("search");
         $search = htmlspecialchars(strip_tags($search));
-
+        $onlineaddons = Cache::get("onlineaddons");
+        if (!is_array($onlineaddons)) {
+            $onlineaddons = [];
+            $result = Http::sendRequest(config('fastadmin.api_url') . '/addon/index');
+            if ($result['ret']) {
+                $json = json_decode($result['msg'], TRUE);
+                $rows = isset($json['rows']) ? $json['rows'] : [];
+                foreach ($rows as $index => $row) {
+                    $onlineaddons[$row['name']] = $row;
+                }
+            }
+            Cache::set("onlineaddons", $onlineaddons, 600);
+        }
+        $filter = (array)json_decode($filter, true);
         $addons = get_addon_list();
         $list = [];
-        foreach ($addons as $k => $v)
-        {
+        foreach ($addons as $k => $v) {
             if ($search && stripos($v['name'], $search) === FALSE && stripos($v['intro'], $search) === FALSE)
                 continue;
 
-            $v['flag'] = '';
-            $v['banner'] = '';
-            $v['image'] = '';
-            $v['donateimage'] = '';
-            $v['demourl'] = '';
-            $v['price'] = '0.00';
+            if (isset($onlineaddons[$v['name']])) {
+                $v = array_merge($onlineaddons[$v['name']], $v);
+            } else {
+                $v['category_id'] = 0;
+                $v['flag'] = '';
+                $v['banner'] = '';
+                $v['image'] = '';
+                $v['donateimage'] = '';
+                $v['demourl'] = '';
+                $v['price'] = '0.00';
+            }
             $v['url'] = addon_url($v['name']);
             $v['createtime'] = filemtime(ADDON_PATH . $v['name']);
+            if ($filter && isset($filter['category_id']) && is_numeric($filter['category_id']) && $filter['category_id'] != $v['category_id']) {
+                continue;
+            }
             $list[] = $v;
         }
         $total = count($list);
-        if ($limit)
-        {
+        if ($limit) {
             $list = array_slice($list, $offset, $limit);
         }
         $result = array("total" => $total, "rows" => $list);

+ 41 - 59
application/admin/controller/Ajax.php

@@ -4,6 +4,7 @@ namespace app\admin\controller;
 
 use app\common\controller\Backend;
 use fast\Random;
+use think\addons\Service;
 use think\Cache;
 use think\Config;
 use think\Db;
@@ -47,8 +48,7 @@ class Ajax extends Backend
     {
         Config::set('default_return_type', 'json');
         $file = $this->request->file('file');
-        if (empty($file))
-        {
+        if (empty($file)) {
             $this->error(__('No file upload or server upload limit exceeded'));
         }
 
@@ -60,7 +60,7 @@ class Ajax extends Backend
         preg_match('/(\d+)(\w+)/', $upload['maxsize'], $matches);
         $type = strtolower($matches[2]);
         $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
-        $size = (int) $upload['maxsize'] * pow(1024, isset($typeDict[$type]) ? $typeDict[$type] : 0);
+        $size = (int)$upload['maxsize'] * pow(1024, isset($typeDict[$type]) ? $typeDict[$type] : 0);
         $fileInfo = $file->getInfo();
         $suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
         $suffix = $suffix ? $suffix : 'file';
@@ -68,8 +68,7 @@ class Ajax extends Backend
         $mimetypeArr = explode(',', $upload['mimetype']);
         $typeArr = explode('/', $fileInfo['type']);
         //验证文件后缀
-        if ($upload['mimetype'] !== '*' && !in_array($suffix, $mimetypeArr) && !in_array($fileInfo['type'], $mimetypeArr) && !in_array($typeArr[0] . '/*', $mimetypeArr))
-        {
+        if ($upload['mimetype'] !== '*' && !in_array($suffix, $mimetypeArr) && !in_array($fileInfo['type'], $mimetypeArr) && !in_array($typeArr[0] . '/*', $mimetypeArr)) {
             $this->error(__('Uploaded file format is limited'));
         }
         $replaceArr = [
@@ -93,11 +92,9 @@ class Ajax extends Backend
         $fileName = substr($savekey, strripos($savekey, '/') + 1);
         //
         $splInfo = $file->validate(['size' => $size])->move(ROOT_PATH . '/public' . $uploadDir, $fileName);
-        if ($splInfo)
-        {
+        if ($splInfo) {
             $imagewidth = $imageheight = 0;
-            if (in_array($suffix, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']))
-            {
+            if (in_array($suffix, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf'])) {
                 $imgInfo = getimagesize($splInfo->getPathname());
                 $imagewidth = isset($imgInfo[0]) ? $imgInfo[0] : $imagewidth;
                 $imageheight = isset($imgInfo[1]) ? $imgInfo[1] : $imageheight;
@@ -121,9 +118,7 @@ class Ajax extends Backend
             $this->success(__('Upload successful'), null, [
                 'url' => $uploadDir . $splInfo->getSaveName()
             ]);
-        }
-        else
-        {
+        } else {
             // 上传失败获取错误信息
             $this->error($file->getError());
         }
@@ -153,12 +148,10 @@ class Ajax extends Backend
         $field = in_array($field, ['weigh']) ? $field : 'weigh';
 
         // 如果设定了pid的值,此时只匹配满足条件的ID,其它忽略
-        if ($pid !== '')
-        {
+        if ($pid !== '') {
             $hasids = [];
             $list = Db::name($table)->where($prikey, 'in', $ids)->where('pid', 'in', $pid)->field('id,pid')->select();
-            foreach ($list as $k => $v)
-            {
+            foreach ($list as $k => $v) {
                 $hasids[] = $v['id'];
             }
             $ids = array_values(array_intersect($ids, $hasids));
@@ -166,20 +159,15 @@ class Ajax extends Backend
 
         //直接修复排序
         $one = Db::name($table)->field("{$field},COUNT(*) AS nums")->group($field)->having('nums > 1')->find();
-        if ($one)
-        {
+        if ($one) {
             $list = Db::name($table)->field("$prikey,$field")->order($field, $orderway)->select();
-            foreach ($list as $k => $v)
-            {
+            foreach ($list as $k => $v) {
                 Db::name($table)->where($prikey, $v[$prikey])->update([$field => $k + 1]);
             }
             $this->success();
-        }
-        else
-        {
+        } else {
             $list = Db::name($table)->field("$prikey,$field")->where($prikey, 'in', $ids)->order($field, $orderway)->select();
-            foreach ($list as $k => $v)
-            {
+            foreach ($list as $k => $v) {
                 $sour[] = $v[$prikey];
                 $weighdata[$v[$prikey]] = $v[$field];
             }
@@ -192,20 +180,13 @@ class Ajax extends Backend
             //echo "替换的ID:{$desc_id}\n";
             $weighids = array();
             $temp = array_values(array_diff_assoc($ids, $sour));
-            foreach ($temp as $m => $n)
-            {
-                if ($n == $sour_id)
-                {
+            foreach ($temp as $m => $n) {
+                if ($n == $sour_id) {
                     $offset = $desc_id;
-                }
-                else
-                {
-                    if ($sour_id == $temp[0])
-                    {
+                } else {
+                    if ($sour_id == $temp[0]) {
                         $offset = isset($temp[$m + 1]) ? $temp[$m + 1] : $sour_id;
-                    }
-                    else
-                    {
+                    } else {
                         $offset = isset($temp[$m - 1]) ? $temp[$m - 1] : $sour_id;
                     }
                 }
@@ -221,15 +202,23 @@ class Ajax extends Backend
      */
     public function wipecache()
     {
-        $wipe_cache_type = ['TEMP_PATH', 'LOG_PATH', 'CACHE_PATH'];
-        foreach ($wipe_cache_type as $item)
-        {
-            $dir = constant($item);
-            if (!is_dir($dir))
-                continue;
-            rmdirs($dir);
+        $type = $this->request->request("type");
+        switch ($type) {
+            case 'content' || 'all':
+                rmdirs(CACHE_PATH, false);
+                Cache::clear();
+                if ($type == 'content')
+                    break;
+            case 'template' || 'all':
+                rmdirs(TEMP_PATH, false);
+                if ($type == 'template')
+                    break;
+            case 'addons' || 'all':
+                Service::refresh();
+                if ($type == 'addons')
+                    break;
         }
-        Cache::clear();
+
         \think\Hook::listen("wipecache_after");
         $this->success();
     }
@@ -243,14 +232,11 @@ class Ajax extends Backend
         $pid = $this->request->get('pid');
         $where = ['status' => 'normal'];
         $categorylist = null;
-        if ($pid !== '')
-        {
-            if ($type)
-            {
+        if ($pid !== '') {
+            if ($type) {
                 $where['type'] = $type;
             }
-            if ($pid)
-            {
+            if ($pid) {
                 $where['pid'] = $pid;
             }
 
@@ -268,17 +254,13 @@ class Ajax extends Backend
         $city = $this->request->get('city');
         $where = ['pid' => 0, 'level' => 1];
         $provincelist = null;
-        if ($province !== '')
-        {
-            if ($province)
-            {
+        if ($province !== '') {
+            if ($province) {
                 $where['pid'] = $province;
                 $where['level'] = 2;
             }
-            if ($city !== '')
-            {
-                if ($city)
-                {
+            if ($city !== '') {
+                if ($city) {
                     $where['pid'] = $city;
                     $where['level'] = 3;
                 }

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

@@ -30,6 +30,8 @@ return [
     'Please disable addon first'     => '请先禁用插件再进行升级',
     'Login now'                      => '立即登录',
     'Continue install'               => '不登录,继续安装',
+    'All'                            => '全部',
+    'Uncategoried'                   => '未归类',
     'Recommend'                      => '推荐',
     'Hot'                            => '热门',
     'New'                            => '新',

+ 4 - 0
application/admin/lang/zh-cn/index.php

@@ -33,6 +33,10 @@ return [
     'Wipe cache completed'                                       => '清除缓存成功',
     'Wipe cache failed'                                          => '清除缓存失败',
     'Wipe cache'                                                 => '清空缓存',
+    'Wipe all cache'                                             => '一键清除缓存',
+    'Wipe content cache'                                         => '清空内容缓存',
+    'Wipe template cache'                                        => '清除模板缓存',
+    'Wipe addons cache'                                          => '清除插件缓存',
     'Check for updates'                                          => '检测更新',
     'Latest news'                                                => '最新消息',
     'View more'                                                  => '查看更多',

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

@@ -30,7 +30,7 @@ class Auth extends \fast\Auth
 
     /**
      * 管理员登录
-     * 
+     *
      * @param   string  $username   用户名
      * @param   string  $password   密码
      * @param   int     $keeptime   有效时长
@@ -44,7 +44,7 @@ class Auth extends \fast\Auth
             $this->setError('Username is incorrect');
             return false;
         }
-        if ($admin->loginfailure >= 3 && time() - $admin->updatetime < 86400)
+        if (Config::get('fastadmin.login_failure_retry') && $admin->loginfailure >= 10 && time() - $admin->updatetime < 86400)
         {
             $this->setError('Please try again after 1 day');
             return false;
@@ -119,7 +119,7 @@ class Auth extends \fast\Auth
 
     /**
      * 刷新保持登录的Cookie
-     * 
+     *
      * @param   int     $keeptime
      * @return  boolean
      */
@@ -155,6 +155,7 @@ class Auth extends \fast\Auth
             return FALSE;
         }
 
+        $arr = array_map('strtolower', $arr);
         // 是否存在
         if (in_array(strtolower($request->action()), $arr) || in_array('*', $arr))
         {

+ 3 - 10
application/admin/view/addon/config.html

@@ -21,20 +21,13 @@
                             <textarea name="row[{$item.name}]" class="form-control" data-rule="{$item.rule}" rows="5" data-tip="{$item.tip}" {$item.extend}>{$item.value}</textarea>
                             {/case}
                             {case array}
-                            <dl class="fieldlist" rel="{$item.value|count}" data-name="row[{$item.name}]">
+                            <dl class="fieldlist" data-name="row[{$item.name}]">
                                 <dd>
                                     <ins>{:__('Array key')}</ins>
                                     <ins>{:__('Array value')}</ins>
                                 </dd>
-                                {foreach $item.value as $key => $vo}
-                                <dd class="form-inline">
-                                    <input type="text" name="row[{$item.name}][field][{$key}]" class="form-control" value="{$key}" size="10" />
-                                    <input type="text" name="row[{$item.name}][value][{$key}]" class="form-control" value="{$vo}" size="30" />
-                                    <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span>
-                                    <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span>
-                                </dd>
-                                {/foreach}
-                                <dd><a href="javascript:;" class="append btn btn-sm btn-success"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                                <dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                                <textarea name="row[{$item.name}]" cols="30" rows="5" class="hide">{$item.value|json_encode}</textarea>
                             </dl>
                             {/case}
                             {case datetime}

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

@@ -1,7 +1,7 @@
 <style type="text/css">
     .noimage {width:100%;text-align: center;background:#18bc9c;color:#fff;padding-bottom:66.66%;position:relative;}
     .noimage > div {position: absolute;top:48%;width:100%;text-align:center;}
-    .addon {position: relative;}
+    .addon {position: relative;border-color:#e4e4e4;}
     .addon > span {position:absolute;left:15px;top:15px;z-index:9;}
     .addon > span a{opacity: 0.5;border:none;color:#fff;}
     .layui-layer-pay .layui-layer-content {padding:0;height:600px!important;}
@@ -16,9 +16,34 @@
     .status-disabled .noimage {
         background:#d2d6de;
     }
+    @media (min-width: 992px) {
+        .addon {
+            -webkit-transition:all 0.3s ease;
+            -moz-transition: all 0.3s ease;
+            -o-transition: all 0.3s ease;
+            transition: all 0.3s ease;
+        }
+        .addon:hover {
+            border-color: #dddddd;
+            -webkit-box-shadow: 0 26px 40px -24px rgba(0,36,100,0.3);
+            -moz-box-shadow: 0 26px 40px -24px rgba(0,36,100,0.3);
+            box-shadow: 0 26px 40px -24px rgba(0,36,100,0.3);
+            -webkit-transition: all 0.3s ease;
+            -moz-transition: all 0.3s ease;
+            -o-transition: all 0.3s ease;
+            transition: all 0.3s ease;
+        }
+    }
 </style>
 <div class="panel panel-default panel-intro">
-    {:build_heading()}
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs nav-category">
+            <li class="active"><a href="javascript:;" data-id="">{:__('All')}</a></li>
+            <li><a href="javascript:;" data-id="0">{:__('Uncategoried')}</a></li>
+        </ul>
+
+    </div>
 
     <div class="panel-body">
         <div id="myTabContent" class="tab-content">
@@ -27,10 +52,11 @@
                     <div id="toolbar" class="toolbar">
                         {:build_toolbar('refresh')}
                         <button type="button" id="plupload-addon" class="btn btn-danger plupload" data-url="addon/local" data-mimetype="application/zip" data-multiple="false"><i class="fa fa-upload"></i> {:__('Offline install')}</button>
-                        <a class="btn btn-success btn-ajax" href="addon/refresh"><i class="fa fa-refresh"></i> {:__('Refresh addon cache')}</a>
                         <div class="btn-group">
-                            <a href="#" class="btn btn-info btn-switch active" data-url="{$config.fastadmin.api_url}/addon/index"><i class="fa fa-cloud"></i> {:__('Online store')}</a>
-                            <a href="#" class="btn btn-info btn-switch" data-url="addon/downloaded"><i class="fa fa-laptop"></i> {:__('Local addon')}</a>
+                            <a href="#" class="btn btn-info btn-switch active" data-type="all" data-url="{$config.fastadmin.api_url}/addon/index"><i class="fa fa-list"></i> {:__('全部')}</a>
+                            <a href="#" class="btn btn-info btn-switch" data-type="free" data-url="{$config.fastadmin.api_url}/addon/index"><i class="fa fa-gift"></i> {:__('免费')}</a>
+                            <a href="#" class="btn btn-info btn-switch" data-type="price" data-url="{$config.fastadmin.api_url}/addon/index"><i class="fa fa-rmb"></i> {:__('付费')}</a>
+                            <a href="#" class="btn btn-info btn-switch" data-type="local" data-url="addon/downloaded"><i class="fa fa-laptop"></i> {:__('Local addon')}</a>
                         </div>
                         <a class="btn btn-primary btn-userinfo" href="javascript:;"><i class="fa fa-user"></i> {:__('Userinfo')}</a>
                     </div>
@@ -44,6 +70,55 @@
         </div>
     </div>
 </div>
+<script id="searchformtpl" type="text/html">
+    <form action="" class="form-commonsearch hide">
+        <div class="well" style="box-shadow:none;border-radius:2px;margin-bottom:10px;">
+            <div class="row">
+                <div class="col-xs-12 col-sm-6 col-md-3">
+                    <div class="form-group">
+                        <label class="control-label">标题</label>
+                        <input class="operate" type="hidden" data-name="title" value="like"/>
+                        <input class="form-control" type="text" name="title" placeholder="请输入查找的标题" value=""/>
+                    </div>
+                </div>
+                <div class="col-xs-12 col-sm-6 col-md-3">
+                    <div class="form-group">
+                        <label class="control-label">类型</label>
+                        <input class="operate" type="hidden" data-name="type" value="="/>
+                        <input class="form-control" type="text" name="type" placeholder="all" value=""/>
+                    </div>
+                </div>
+                <div class="col-xs-12 col-sm-6 col-md-3">
+                    <div class="form-group">
+                        <label class="control-label">分类</label>
+                        <input type="hidden" class="operate" data-name="category_id" value="="/>
+                        <input class="form-control" name="category_id" type="text" value="">
+                    </div>
+                </div>
+                <div class="col-xs-12 col-sm-6 col-md-3">
+                    <div class="form-group">
+                        <label class="control-label">版本号</label>
+                        <input type="hidden" class="operate" data-name="faversion" value="="/>
+                        <input class="form-control" name="faversion" type="text" value="{$config.fastadmin.version}">
+                    </div>
+                </div>
+                <div class="col-xs-12 col-sm-6 col-md-3">
+                    <div class="form-group">
+                        <label class="control-label"></label>
+                        <div class="row">
+                            <div class="col-xs-6">
+                                <input type="submit" class="btn btn-success btn-block" value="提交"/>
+                            </div>
+                            <div class="col-xs-6">
+                                <input type="reset" class="btn btn-primary btn-block" value="重置"/>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </form>
+</script>
 <script id="logintpl" type="text/html">
     <div>
         <form class="form-horizontal">
@@ -180,7 +255,7 @@
                 <%}%>
             </a>
             <div class="caption">
-                <h4><%=item.title?item.title:'{:__('None')}'%> 
+                <h4><%=item.title?item.title:'{:__('None')}'%>
                     <% if(item.flag.indexOf("recommend")>-1){%>
                     <span class="label label-success">{:__('Recommend')}</span>
                     <% } %>
@@ -203,7 +278,7 @@
                     <% if(!addon){ %>
                     <% if(typeof item.releaselist !="undefined" && item.releaselist.length>1){%>
                     <span class="btn-group">
-                        <a href="javascript:;" class="btn btn-primary btn-success btn-install" data-type="<%=item.price<=0?'free':'price';%>" data-donateimage="<%=item.donateimage%>" data-version="<%=item.version%>"><i class="fa fa-cloud-download"></i> {:__('Install')}</a> 
+                        <a href="javascript:;" class="btn btn-primary btn-success btn-install" data-type="<%=item.price<=0?'free':'price';%>" data-donateimage="<%=item.donateimage%>" data-version="<%=item.version%>"><i class="fa fa-cloud-download"></i> {:__('Install')}</a>
                         <a class="btn btn-success dropdown-toggle" data-toggle="dropdown" href="javascript:;">
                             <span class="fa fa-caret-down"></span>
                         </a>
@@ -214,10 +289,10 @@
                         </ul>
                     </span>
                     <% }else{%>
-                    <a href="javascript:;" class="btn btn-primary btn-success btn-install" data-type="<%=item.price<=0?'free':'price';%>" data-donateimage="<%=item.donateimage%>" data-version="<%=item.version%>"><i class="fa fa-cloud-download"></i> {:__('Install')}</a> 
+                    <a href="javascript:;" class="btn btn-primary btn-success btn-install" data-type="<%=item.price<=0?'free':'price';%>" data-donateimage="<%=item.donateimage%>" data-version="<%=item.version%>"><i class="fa fa-cloud-download"></i> {:__('Install')}</a>
                     <% } %>
                     <% if(item.demourl){ %>
-                    <a href="<%=item.demourl%>" class="btn btn-primary btn-info btn-demo" target="_blank"><i class="fa fa-flash"></i> {:__('Demo')}</a> 
+                    <a href="<%=item.demourl%>" class="btn btn-primary btn-info btn-demo" target="_blank"><i class="fa fa-flash"></i> {:__('Demo')}</a>
                     <% } %>
                     <% } %>
 
@@ -235,7 +310,7 @@
                     <% if(addon && item && addon.version!=item.version){%>
                     <% if(typeof item.releaselist !="undefined" && item.releaselist.length>1){%>
                     <span class="btn-group">
-                        <a href="javascript:;" class="btn btn-info btn-success btn-upgrade" data-version="<%=item.version%>"><i class="fa fa-cloud"></i> {:__('Upgrade')}</a> 
+                        <a href="javascript:;" class="btn btn-info btn-success btn-upgrade" data-version="<%=item.version%>"><i class="fa fa-cloud"></i> {:__('Upgrade')}</a>
                         <a class="btn btn-info dropdown-toggle" data-toggle="dropdown" href="javascript:;">
                             <span class="fa fa-caret-down"></span>
                         </a>

+ 18 - 8
application/admin/view/common/header.html

@@ -25,7 +25,7 @@
                 <a href="__PUBLIC__" target="_blank"><i class="fa fa-home" style="font-size:14px;"></i></a>
             </li>
 
-            <li class="dropdown notifications-menu">
+            <li class="dropdown notifications-menu hidden-xs">
                 <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                     <i class="fa fa-bell-o"></i>
                     <span class="label label-warning"></span>
@@ -41,19 +41,26 @@
                     <li class="footer"><a href="#" target="_blank">{:__('View more')}</a></li>
                 </ul>
             </li>
-            
-            <li>
+
+            <li class="hidden-xs">
                 <a href="javascript:;" data-toggle="checkupdate" title="{:__('Check for updates')}">
                     <i class="fa fa-refresh"></i>
                 </a>
             </li>
-            
+
             <li>
-                <a href="javascript:;" data-toggle="wipecache" title="{:__('Wipe cache')}">
+                <a href="javascript:;" data-toggle="dropdown" title="{:__('Wipe cache')}">
                     <i class="fa fa-trash"></i>
                 </a>
+                <ul class="dropdown-menu wipecache">
+                    <li><a href="javascript:;" data-type="all"><i class="fa fa-trash"></i> {:__('Wipe all cache')}</a></li>
+                    <li><a href="javascript:;" data-type="content"><i class="fa fa-file-text"></i> {:__('Wipe content cache')}</a></li>
+                    <li><a href="javascript:;" data-type="template"><i class="fa fa-file-image-o"></i> {:__('Wipe template cache')}</a></li>
+                    <li><a href="javascript:;" data-type="addons"><i class="fa fa-rocket"></i> {:__('Wipe addons cache')}</a></li>
+                </ul>
             </li>
 
+            {if $Think.config.lang_switch_on}
             <li class="hidden-xs">
                 <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-language"></i></a>
                 <ul class="dropdown-menu">
@@ -65,6 +72,7 @@
                     </li>
                 </ul>
             </li>
+            {/if}
 
             <li class="hidden-xs">
                 <a href="#" data-toggle="fullscreen"><i class="fa fa-arrows-alt"></i></a>
@@ -103,16 +111,18 @@
                     <!-- Menu Footer-->
                     <li class="user-footer">
                         <div class="pull-left">
-                            <a href="general/profile" class="btn btn-primary addtabsit"><i class="fa fa-user"></i> {:__('Profile')}</a>
+                            <a href="general/profile" class="btn btn-primary addtabsit"><i class="fa fa-user"></i>
+                                {:__('Profile')}</a>
                         </div>
                         <div class="pull-right">
-                            <a href="{:url('index/logout')}" class="btn btn-danger"><i class="fa fa-sign-out"></i> {:__('Logout')}</a>
+                            <a href="{:url('index/logout')}" class="btn btn-danger"><i class="fa fa-sign-out"></i>
+                                {:__('Logout')}</a>
                         </div>
                     </li>
                 </ul>
             </li>
             <!-- 控制栏切换按钮 -->
-            <li>
+            <li class="hidden-xs">
                 <a href="javascript:;" data-toggle="control-sidebar"><i class="fa fa-gears"></i></a>
             </li>
         </ul>

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

@@ -80,7 +80,7 @@
                                 <div class="input-group">
                                     <div class="input-group-addon"><span class="glyphicon glyphicon-option-horizontal" aria-hidden="true"></span></div>
                                     <input type="text" name="captcha" class="form-control" placeholder="{:__('Captcha')}" data-rule="{:__('Captcha')}:required;length(4)" />
-                                    <span class="input-group-addon" style="padding:0;border:none;">
+                                    <span class="input-group-addon" style="padding:0;border:none;cursor:pointer;">
                                         <img src="{:captcha_src()}" width="100" height="30" onclick="this.src = '{:captcha_src()}?r=' + Math.random();"/>
                                     </span>
                                 </div>
@@ -97,7 +97,7 @@
                             </form>
                         </div>
                     </div>
-                    <p class="copyright"><a href="https://www.fastadmin.net?ref=demo">Powered By FastAdmin</a></p>
+                    <p class="copyright"><a href="https://www.fastadmin.net">Powered By FastAdmin</a></p>
                 </div>
             </div>
         </div>

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

@@ -279,6 +279,7 @@ class Backend extends Controller
             {
                 $k = $tableName . $k;
             }
+            $v = !is_array($v) ? trim($v) : $v;
             $sym = strtoupper(isset($op[$k]) ? $op[$k] : $sym);
             switch ($sym)
             {
@@ -306,7 +307,7 @@ class Backend extends Controller
                 case 'IN(...)':
                 case 'NOT IN':
                 case 'NOT IN(...)':
-                    $where[] = [$k, str_replace('(...)', '', $sym), explode(',', $v)];
+                    $where[] = [$k, str_replace('(...)', '', $sym), is_array($v) ? $v : explode(',', $v)];
                     break;
                 case 'BETWEEN':
                 case 'NOT BETWEEN':

+ 2 - 0
application/common/library/Auth.php

@@ -382,6 +382,7 @@ class Auth
             $rules[] = $v['name'];
         }
         $url = ($module ? $module : request()->module()) . '/' . (is_null($path) ? $this->getRequestUri() : $path);
+        $url = strtolower(str_replace('.', '/', $url));
         return in_array($url, $rules) ? TRUE : FALSE;
     }
 
@@ -539,6 +540,7 @@ class Auth
         {
             return FALSE;
         }
+        $arr = array_map('strtolower', $arr);
         // 是否存在
         if (in_array(strtolower($request->action()), $arr) || in_array('*', $arr))
         {

+ 7 - 0
application/common/model/Category.php

@@ -21,6 +21,13 @@ class Category Extends Model
         'flag_text',
     ];
 
+    protected static function init()
+    {
+        self::afterInsert(function ($row) {
+            $row->save(['weigh' => $row['id']]);
+        });
+    }
+
     public function setFlagAttr($value, $data)
     {
         return is_array($value) ? implode(',', $value) : $value;

+ 9 - 7
application/config.php

@@ -258,18 +258,20 @@ return [
     //FastAdmin配置
     'fastadmin'              => [
         //是否开启前台会员中心
-        'usercenter'       => true,
+        'usercenter'          => true,
         //登录验证码
-        'login_captcha'    => false,
+        'login_captcha'       => true,
+        //登录失败超过10则1天后重试
+        'login_failure_retry' => true,
         //是否同一账号同一时间只能在一个地方登录
-        'login_unique'     => false,
+        'login_unique'        => false,
         //登录页默认背景图
-        'login_background' => "/assets/img/loginbg.jpg",
+        'login_background'    => "/assets/img/loginbg.jpg",
         //自动检测更新
-        'checkupdate'      => false,
+        'checkupdate'         => false,
         //版本号
-        'version'          => '1.0.0.20180401_beta',
+        'version'             => '1.0.0.20180406_beta',
         //API接口地址
-        'api_url'          => 'https://api.fastadmin.net',
+        'api_url'             => 'https://api.fastadmin.net',
     ],
 ];

+ 10 - 2
public/api.html

@@ -9,7 +9,15 @@
         <title>FastAdmin</title>
         <link href="https://cdn.bootcss.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
         <style type="text/css">
-            body      { padding-top: 70px; margin-bottom: 15px; }
+            body {
+                padding-top: 70px; margin-bottom: 15px;
+                -webkit-font-smoothing: antialiased;
+                -moz-osx-font-smoothing: grayscale;
+                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; }
+            hr        { margin-top: 10px; }
             .tab-pane { padding-top: 10px; }
             .mt0      { margin-top: 0px; }
             .footer   { font-size: 12px; color: #666; }
@@ -3351,7 +3359,7 @@
 
             <div class="row mt0 footer">
                 <div class="col-md-6" align="left">
-                    Generated on 2018-04-01 15:11:14                </div>
+                    Generated on 2018-04-06 19:15:01                </div>
                 <div class="col-md-6" align="right">
                     <a href="https://www.fastadmin.net" target="_blank">FastAdmin</a>
                 </div>

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

@@ -807,4 +807,6 @@ form.form-horizontal .control-label {
 .checkbox > label > input {
   margin: 2px 0 0;
 }
-/*# sourceMappingURL=backend.css.map */
+.wipecache li a {
+  color: #444444!important;
+}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 1
public/assets/css/backend.min.css


+ 4 - 1
public/assets/css/index.css

@@ -4,7 +4,10 @@ body {
     width: 100%;
 }
 body {
-    font-family: 'Muli', 'Helvetica', 'Arial', 'sans-serif';
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    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;
 }
 a {
     -webkit-transition: all 0.35s;

+ 9 - 6
public/assets/js/backend.js

@@ -1,4 +1,4 @@
-define(['fast', 'moment'], function (Fast, Moment) {
+define(['fast', 'template', 'moment'], function (Fast, Template, Moment) {
     var Backend = {
         api: {
             sidebar: function (params) {
@@ -8,13 +8,11 @@ define(['fast', 'moment'], function (Fast, Moment) {
                 $.each(params, function (k, v) {
                     $url = Fast.api.fixurl(k);
 
-                    if ($.isArray(v))
-                    {
+                    if ($.isArray(v)) {
                         $nums = typeof v[0] !== 'undefined' ? v[0] : 0;
                         $color = typeof v[1] !== 'undefined' ? v[1] : colorArr[(!isNaN($nums) ? $nums : $nums.length) % $colorNums];
                         $class = typeof v[2] !== 'undefined' ? v[2] : 'label';
-                    } else
-                    {
+                    } else {
                         $nums = v;
                         $color = colorArr[(!isNaN($nums) ? $nums : $nums.length) % $colorNums];
                         $class = 'label';
@@ -58,7 +56,10 @@ define(['fast', 'moment'], function (Fast, Moment) {
                             var id = Math.floor(new Date().valueOf() * Math.random());
                             icon = typeof icon !== 'undefined' ? icon : 'fa fa-circle-o';
                             title = typeof title !== 'undefined' ? title : '';
-                            top.window.$("<a />").append('<i class="' + icon + '"></i> <span>' + title + '</span>').prop("href", url).attr({url: url, addtabs: id}).addClass("hide").appendTo(top.window.document.body).trigger("click");
+                            top.window.$("<a />").append('<i class="' + icon + '"></i> <span>' + title + '</span>').prop("href", url).attr({
+                                url: url,
+                                addtabs: id
+                            }).addClass("hide").appendTo(top.window.document.body).trigger("click");
                         }
                     }
                 }
@@ -227,6 +228,8 @@ define(['fast', 'moment'], function (Fast, Moment) {
         }
     };
     Backend.api = $.extend(Fast.api, Backend.api);
+    //将Template渲染至全局,以便于在子框架中调用
+    window.Template = Template;
     //将Moment渲染至全局,以便于在子框架中调用
     window.Moment = Moment;
     //将Backend渲染至全局,以便于在子框架中调用

+ 36 - 16
public/assets/js/backend/addon.js

@@ -14,6 +14,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
 
             var table = $("#table");
 
+            table.on('load-success.bs.table', function (e, json) {
+                if (json && typeof json.category != 'undefined' && $(".nav-category li").size() == 2) {
+                    $.each(json.category, function (i, j) {
+                        $("<li><a href='javascript:;' data-id='" + j.id + "'>" + j.name + "</a></li>").insertBefore($(".nav-category li:last"));
+                    });
+                }
+            });
             table.on('post-body.bs.table', function (e, settings, json, xhr) {
                 var parenttable = table.closest('.bootstrap-table');
                 var d = $(".fixed-table-toolbar", parenttable).find(".search input");
@@ -53,19 +60,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 showColumns: false,
                 showToggle: false,
                 showExport: false,
-                commonSearch: false,
-                searchFormVisible: false,
+                showSearch: false,
+                commonSearch: true,
+                searchFormVisible: true,
+                searchFormTemplate: 'searchformtpl',
                 pageSize: 12,
                 pagination: false,
-                queryParams: function (params) {
-                    var filter = params.filter ? JSON.parse(params.filter) : {};
-                    var op = params.op ? JSON.parse(params.op) : {};
-                    filter.faversion = Config.fastadmin.version;
-                    op.faversion = "=";
-                    params.filter = JSON.stringify(filter);
-                    params.op = JSON.stringify(op);
-                    return params;
-                }
             });
 
             // 为表格绑定事件
@@ -124,14 +124,23 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     return false;
                 }
             });
-            
-            // 切换URL
+
+            // 切换
             $(document).on("click", ".btn-switch", function () {
                 $(".btn-switch").removeClass("active");
                 $(this).addClass("active");
+                $("form.form-commonsearch input[name='type']").val($(this).data("type"));
                 table.bootstrapTable('refresh', {url: $(this).data("url"), pageNumber: 1});
+                return false;
             });
-            
+            $(document).on("click", ".nav-category li a", function () {
+                $(".nav-category li").removeClass("active");
+                $(this).parent().addClass("active");
+                $("form.form-commonsearch input[name='category_id']").val($(this).data("id"));
+                table.bootstrapTable('refresh', {url: $(this).data("url"), pageNumber: 1});
+                return false;
+            });
+
             // 会员信息
             $(document).on("click", ".btn-userinfo", function () {
                 var userinfo = Controller.api.userinfo.get();
@@ -146,7 +155,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                             Fast.api.ajax({
                                 url: Config.fastadmin.api_url + '/user/login',
                                 dataType: 'jsonp',
-                                data: {account: $("#inputAccount", layero).val(), password: $("#inputPassword", layero).val(), _method: 'POST'}
+                                data: {
+                                    account: $("#inputAccount", layero).val(),
+                                    password: $("#inputPassword", layero).val(),
+                                    _method: 'POST'
+                                }
                             }, function (data, ret) {
                                 Controller.api.userinfo.set(data);
                                 Layer.closeAll();
@@ -199,7 +212,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 var token = userinfo ? userinfo.token : '';
                 Fast.api.ajax({
                     url: 'addon/install',
-                    data: {name: name, force: force ? 1 : 0, uid: uid, token: token, version: version, faversion: Config.fastadmin.version}
+                    data: {
+                        name: name,
+                        force: force ? 1 : 0,
+                        uid: uid,
+                        token: token,
+                        version: version,
+                        faversion: Config.fastadmin.version
+                    }
                 }, function (data, ret) {
                     Layer.closeAll();
                     Config['addons'][data.addon.name] = ret.data.addon;

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

@@ -180,10 +180,11 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
             });
 
             //清除缓存
-            $(document).on('click', "[data-toggle='wipecache']", function () {
+            $(document).on('click', "ul.wipecache li a", function () {
                 $.ajax({
                     url: 'ajax/wipecache',
                     dataType: 'json',
+                    data: {type: $(this).data("type")},
                     cache: false,
                     success: function (ret) {
                         if (ret.hasOwnProperty("code")) {

+ 56 - 103
public/assets/js/bootstrap-table-commonsearch.js

@@ -1,9 +1,11 @@
 /**
+ * FastAdmin通用搜索
+ *
  * @author: pppscn <35696959@qq.com>
- * @version: v0.0.1
+ * @update 2017-05-07 <https://gitee.com/pp/fastadmin>
  *
- * @update 2017-05-07 <http://git.oschina.net/pp/fastadmin>
- * @update 2017-09-17 <http://git.oschina.net/karson/fastadmin>
+ * @author: Karson <karsonzhang@163.com>
+ * @update 2018-04-05 <https://gitee.com/karson/fastadmin>
  */
 
 !function ($) {
@@ -17,75 +19,16 @@
         var vFormCommon = createFormCommon(pColumns, that);
 
         var vModal = sprintf("<div class=\"commonsearch-table %s\">", that.options.searchFormVisible ? "" : "hidden");
-        vModal += vFormCommon.join('');
+        vModal += vFormCommon;
         vModal += "</div>";
         that.$container.prepend($(vModal));
         that.$commonsearch = $(".commonsearch-table", that.$container);
         var form = $("form.form-commonsearch", that.$commonsearch);
 
-        //绑定日期时间元素事件
-        if ($(".datetimepicker", form).size() > 0) {
-
-            require(['bootstrap-datetimepicker'], function () {
-                $('.datetimepicker', form).parent().css('position', 'relative');
-                $('.datetimepicker', form).datetimepicker({
-                    //format: 'YYYY-MM-DD',
-                    icons: {
-                        time: 'fa fa-clock-o',
-                        date: 'fa fa-calendar',
-                        up: 'fa fa-chevron-up',
-                        down: 'fa fa-chevron-down',
-                        previous: 'fa fa-chevron-left',
-                        next: 'fa fa-chevron-right',
-                        today: 'fa fa-history',
-                        clear: 'fa fa-trash',
-                        close: 'fa fa-remove'
-                    },
-                    showTodayButton: true,
-                    showClose: true
-                });
-            });
-        }
-        if ($(".datetimerange", form).size() > 0) {
-            var ranges = {};
-            ranges[__('Today')] = [Moment().startOf('day'), Moment().endOf('day')];
-            ranges[__('Yesterday')] = [Moment().subtract(1, 'days').startOf('day'), Moment().subtract(1, 'days').endOf('day')];
-            ranges[__('Last 7 Days')] = [Moment().subtract(6, 'days').startOf('day'), Moment().endOf('day')];
-            ranges[__('Last 30 Days')] = [Moment().subtract(29, 'days').startOf('day'), Moment().endOf('day')];
-            ranges[__('This Month')] = [Moment().startOf('month'), Moment().endOf('month')];
-            ranges[__('Last Month')] = [Moment().subtract(1, 'month').startOf('month'), Moment().subtract(1, 'month').endOf('month')];
-            var options = {
-                timePicker: false,
-                autoUpdateInput: false,
-                timePickerSeconds: true,
-                timePicker24Hour: true,
-                autoApply: true,
-                locale: {
-                    format: 'YYYY-MM-DD HH:mm:ss',
-                    customRangeLabel: __("Custom Range"),
-                    applyLabel: __("Apply"),
-                    cancelLabel: __("Clear"),
-                },
-                ranges: ranges,
-            };
-            var callback = function (start, end) {
-                $(this.element).val(start.format(options.locale.format) + " - " + end.format(options.locale.format));
-            };
-            var column, index;
-            require(['bootstrap-daterangepicker'], function () {
-                $(".datetimerange", form).each(function () {
-                    $(this).on('apply.daterangepicker', function (ev, picker) {
-                        callback.call(picker, picker.startDate, picker.endDate);
-                    });
-                    $(this).on('cancel.daterangepicker', function (ev, picker) {
-                        $(this).val('');
-                    });
-                    index = $(this).data("index");
-                    column = pColumns[index];
-                    $(this).daterangepicker($.extend({}, options, column.options || {}), callback);
-                });
-            });
-        }
+        require(['form'], function (Form) {
+            Form.api.bindevent(form);
+            form.validator("destroy");
+        });
 
         // 表单提交
         form.on("submit", function (event) {
@@ -103,9 +46,12 @@
     };
 
     var createFormCommon = function (pColumns, that) {
+        // 如果有使用模板则直接返回模板的内容
+        if (that.options.searchFormTemplate) {
+            return Template(that.options.searchFormTemplate, {columns: pColumns, table: that});
+        }
         var htmlForm = [];
-        var opList = ['=', '>', '>=', '<', '<=', '!=', 'FIND_IN_SET', 'LIKE', 'LIKE %...%', 'NOT LIKE', 'IN', 'NOT IN', 'IN(...)', 'NOT IN(...)', 'BETWEEN', 'NOT BETWEEN', 'RANGE', 'NOT RANGE', 'IS NULL', 'IS NOT NULL'];
-        htmlForm.push(sprintf('<form class="form-horizontal form-commonsearch" action="%s" >', that.options.actionForm));
+        htmlForm.push(sprintf('<form class="form-horizontal form-commonsearch" novalidate method="post" action="%s" >', that.options.actionForm));
         htmlForm.push('<fieldset>');
         if (that.options.titleForm.length > 0)
             htmlForm.push(sprintf("<legend>%s</legend>", that.options.titleForm));
@@ -114,24 +60,28 @@
             var vObjCol = pColumns[i];
             if (!vObjCol.checkbox && vObjCol.field !== 'operate' && vObjCol.searchable && vObjCol.operate !== false) {
                 var query = Backend.api.query(vObjCol.field);
-                query = query ? query : '';
-                vObjCol.defaultValue = that.options.renderDefault && query != '' ? query : (typeof vObjCol.defaultValue === 'undefined' ? '' : vObjCol.defaultValue);
+                var operate = Backend.api.query(vObjCol.field + "-operate");
+
+                vObjCol.defaultValue = that.options.renderDefault && query ? query : (typeof vObjCol.defaultValue === 'undefined' ? '' : vObjCol.defaultValue);
+                vObjCol.operate = that.options.renderDefault && operate ? operate : (typeof vObjCol.operate === 'undefined' ? '=' : vObjCol.operate);
                 ColumnsForSearch.push(vObjCol);
 
                 htmlForm.push('<div class="form-group col-xs-12 col-sm-6 col-md-4 col-lg-3">');
                 htmlForm.push(sprintf('<label for="%s" class="control-label col-xs-4">%s</label>', vObjCol.field, vObjCol.title));
                 htmlForm.push('<div class="col-xs-8">');
 
-                vObjCol.operate = (typeof vObjCol.operate === 'undefined' || $.inArray(vObjCol.operate.toUpperCase(), opList) === -1) ? '=' : vObjCol.operate.toUpperCase();
-                htmlForm.push(sprintf('<input type="hidden" class="form-control operate" name="field-%s" data-name="%s" value="%s" readonly>', vObjCol.field, vObjCol.field, vObjCol.operate));
+                vObjCol.operate = vObjCol.operate ? vObjCol.operate.toUpperCase() : '=';
+                htmlForm.push(sprintf('<input type="hidden" class="form-control operate" name="%s-operate" data-name="%s" value="%s" readonly>', vObjCol.field, vObjCol.field, vObjCol.operate));
 
+                var addClass = typeof vObjCol.addClass === 'undefined' ? (typeof vObjCol.addclass === 'undefined' ? 'form-control' : 'form-control ' + vObjCol.addclass) : 'form-control ' + vObjCol.addClass;
+                var extend = typeof vObjCol.extend === 'undefined' ? '' : vObjCol.extend;
                 var style = typeof vObjCol.style === 'undefined' ? '' : sprintf('style="%s"', vObjCol.style);
+                extend = typeof vObjCol.data !== 'undefined' && extend == '' ? vObjCol.data : extend;
                 if (vObjCol.searchList) {
                     if (typeof vObjCol.searchList === 'object' && typeof vObjCol.searchList.then === 'function') {
-                        htmlForm.push(sprintf('<select class="form-control" name="%s" %s>%s</select>', vObjCol.field, style, sprintf('<option value="">%s</option>', that.options.formatCommonChoose())));
+                        htmlForm.push(sprintf('<select class="%s" name="%s" %s %s>%s</select>', addClass, vObjCol.field, style, extend, sprintf('<option value="">%s</option>', that.options.formatCommonChoose())));
                         (function (vObjCol, that) {
                             $.when(vObjCol.searchList).done(function (ret) {
-
                                 var isArray = false;
                                 if (ret.data && ret.data.searchlist && $.isArray(ret.data.searchlist)) {
                                     var resultlist = {};
@@ -160,23 +110,21 @@
                             var isSelect = (isArray ? value : key) == vObjCol.defaultValue ? 'selected' : '';
                             searchList.push(sprintf("<option value='" + (isArray ? value : key) + "' %s>" + value + "</option>", isSelect));
                         });
-                        htmlForm.push(sprintf('<select class="form-control" name="%s" %s>%s</select>', vObjCol.field, style, searchList.join('')));
+                        htmlForm.push(sprintf('<select class="%s" name="%s" %s %s>%s</select>', addClass, vObjCol.field, style, extend, searchList.join('')));
                     }
                 } else {
                     var placeholder = typeof vObjCol.placeholder === 'undefined' ? vObjCol.title : vObjCol.placeholder;
                     var type = typeof vObjCol.type === 'undefined' ? 'text' : vObjCol.type;
-                    var addclass = typeof vObjCol.addclass === 'undefined' ? 'form-control' : 'form-control ' + vObjCol.addclass;
-                    var data = typeof vObjCol.data === 'undefined' ? '' : vObjCol.data;
                     var defaultValue = typeof vObjCol.defaultValue === 'undefined' ? '' : vObjCol.defaultValue;
                     if (/BETWEEN$/.test(vObjCol.operate)) {
                         var defaultValueArr = defaultValue.toString().match(/\|/) ? defaultValue.split('|') : ['', ''];
                         var placeholderArr = placeholder.toString().match(/\|/) ? placeholder.split('|') : [placeholder, placeholder];
                         htmlForm.push('<div class="row row-between">');
-                        htmlForm.push(sprintf('<div class="col-xs-6 11"><input type="%s" class="%s" name="%s" value="%s" placeholder="%s" id="%s" data-index="%s" %s %s></div>', type, addclass, vObjCol.field, defaultValueArr[0], placeholderArr[0], vObjCol.field, i, style, data));
-                        htmlForm.push(sprintf('<div class="col-xs-6 22"><input type="%s" class="%s" name="%s" value="%s" placeholder="%s" id="%s" data-index="%s" %s %s></div>', type, addclass, vObjCol.field, defaultValueArr[1], placeholderArr[1], vObjCol.field, i, style, data));
+                        htmlForm.push(sprintf('<div class="col-xs-6"><input type="%s" class="%s" name="%s" value="%s" placeholder="%s" id="%s" data-index="%s" %s %s></div>', type, addClass, vObjCol.field, defaultValueArr[0], placeholderArr[0], vObjCol.field, i, style, extend));
+                        htmlForm.push(sprintf('<div class="col-xs-6"><input type="%s" class="%s" name="%s" value="%s" placeholder="%s" id="%s" data-index="%s" %s %s></div>', type, addClass, vObjCol.field, defaultValueArr[1], placeholderArr[1], vObjCol.field, i, style, extend));
                         htmlForm.push('</div>');
                     } else {
-                        htmlForm.push(sprintf('<input type="%s" class="%s" name="%s" value="%s" placeholder="%s" id="%s" data-index="%s" %s %s>', type, addclass, vObjCol.field, defaultValue, placeholder, vObjCol.field, i, style, data));
+                        htmlForm.push(sprintf('<input type="%s" class="%s" name="%s" value="%s" placeholder="%s" id="%s" data-index="%s" %s %s>', type, addClass, vObjCol.field, defaultValue, placeholder, vObjCol.field, i, style, extend));
                     }
                 }
 
@@ -191,7 +139,7 @@
         htmlForm.push('</fieldset>');
         htmlForm.push('</form>');
 
-        return htmlForm;
+        return htmlForm.join('');
     };
 
     var createFormBtn = function (that) {
@@ -199,7 +147,7 @@
         var searchSubmit = that.options.formatCommonSubmitButton();
         var searchReset = that.options.formatCommonResetButton();
         htmlBtn.push('<div class="col-sm-8 col-xs-offset-4">');
-        htmlBtn.push(sprintf('<button type="submit" class="btn btn-success" >%s</button> ', searchSubmit));
+        htmlBtn.push(sprintf('<button type="submit" class="btn btn-success" formnovalidate>%s</button> ', searchSubmit));
         htmlBtn.push(sprintf('<button type="reset" class="btn btn-default" >%s</button> ', searchReset));
         htmlBtn.push('</div>');
         return htmlBtn;
@@ -219,16 +167,17 @@
         var op = {};
         var filter = {};
         var value = '';
-        $("form.form-commonsearch input.operate", that.$commonsearch).each(function (i) {
+        $("form.form-commonsearch .operate", that.$commonsearch).each(function (i) {
             var name = $(this).data("name");
-            var sym = $(this).val().toUpperCase();
+            var sym = $(this).is("select") ? $("option:selected", this).val() : $(this).val().toUpperCase();
             var obj = $("[name='" + name + "']", that.$commonsearch);
             if (obj.size() == 0)
                 return true;
             var vObjCol = ColumnsForSearch[i];
             if (obj.size() > 1) {
                 if (/BETWEEN$/.test(sym)) {
-                    var value_begin = $.trim($("[name='" + name + "']:first", that.$commonsearch).val()), value_end = $.trim($("[name='" + name + "']:last", that.$commonsearch).val());
+                    var value_begin = $.trim($("[name='" + name + "']:first", that.$commonsearch).val()),
+                        value_end = $.trim($("[name='" + name + "']:last", that.$commonsearch).val());
                     if (value_begin.length || value_end.length) {
                         if (typeof vObjCol.process === 'function') {
                             value_begin = vObjCol.process(value_begin, 'begin');
@@ -244,11 +193,12 @@
                     }
                 } else {
                     value = $("[name='" + name + "']:checked", that.$commonsearch).val();
+                    value = (vObjCol && typeof vObjCol.process === 'function') ? vObjCol.process(obj.val()) : obj.val();
                 }
             } else {
-                value = (vObjCol && typeof vObjCol.process === 'function') ? vObjCol.process(obj.val()) : (sym == 'LIKE %...%' ? obj.val().replace(/\*/g, '%') : obj.val());
+                value = (vObjCol && typeof vObjCol.process === 'function') ? vObjCol.process(obj.val()) : obj.val();
             }
-            if (removeempty && value == '' && sym.indexOf("NULL") == -1) {
+            if (removeempty && (value == '' || value == null || ($.isArray(value) && value.length == 0)) && !sym.match(/null/i)) {
                 return true;
             }
 
@@ -267,7 +217,7 @@
         //移除empty的值
         if (removeempty) {
             $.each(params.filter, function (i, j) {
-                if (j === '') {
+                if ((j == '' || j == null || ($.isArray(j) && j.length == 0)) && !params.op[i].match(/null/i)) {
                     delete params.filter[i];
                     delete params.op[i];
                 }
@@ -282,8 +232,10 @@
         commonSearch: false,
         titleForm: "Common search",
         actionForm: "",
+        searchFormTemplate: "",
         searchFormVisible: true,
         searchClass: 'searchit',
+        showSearch: true,
         renderDefault: true,
         onCommonSearch: function (field, text) {
             return false;
@@ -322,10 +274,10 @@
     $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales);
 
     var BootstrapTable = $.fn.bootstrapTable.Constructor,
-            _initHeader = BootstrapTable.prototype.initHeader,
-            _initToolbar = BootstrapTable.prototype.initToolbar,
-            _load = BootstrapTable.prototype.load,
-            _initSearch = BootstrapTable.prototype.initSearch;
+        _initHeader = BootstrapTable.prototype.initHeader,
+        _initToolbar = BootstrapTable.prototype.initToolbar,
+        _load = BootstrapTable.prototype.load,
+        _initSearch = BootstrapTable.prototype.initSearch;
 
     BootstrapTable.prototype.initHeader = function () {
         _initHeader.apply(this, Array.prototype.slice.apply(arguments));
@@ -344,12 +296,13 @@
         }
 
         var that = this,
-                html = [];
-        html.push(sprintf('<div class="columns-%s pull-%s" style="margin-top:10px;margin-bottom:10px;">', this.options.buttonsAlign, this.options.buttonsAlign));
-        html.push(sprintf('<button class="btn btn-default%s' + '" type="button" name="commonSearch" title="%s">', that.options.iconSize === undefined ? '' : ' btn-' + that.options.iconSize, that.options.formatCommonSearch()));
-        html.push(sprintf('<i class="%s %s"></i>', that.options.iconsPrefix, that.options.icons.commonSearchIcon))
-        html.push('</button></div>');
-
+            html = [];
+        if(that.options.showSearch){
+            html.push(sprintf('<div class="columns-%s pull-%s" style="margin-top:10px;margin-bottom:10px;">', this.options.buttonsAlign, this.options.buttonsAlign));
+            html.push(sprintf('<button class="btn btn-default%s' + '" type="button" name="commonSearch" title="%s">', that.options.iconSize === undefined ? '' : ' btn-' + that.options.iconSize, that.options.formatCommonSearch()));
+            html.push(sprintf('<i class="%s %s"></i>', that.options.iconsPrefix, that.options.icons.commonSearchIcon))
+            html.push('</button></div>');
+        }
         if (that.$toolbar.find(".pull-right").size() > 0) {
             $(html.join('')).insertBefore(that.$toolbar.find(".pull-right:first"));
         } else {
@@ -359,7 +312,7 @@
         initCommonSearch(that.columns, that);
 
         that.$toolbar.find('button[name="commonSearch"]')
-                .off('click').on('click', function () {
+            .off('click').on('click', function () {
             that.$commonsearch.toggleClass("hidden");
             return;
         });
@@ -374,7 +327,7 @@
         var queryParams = that.options.queryParams;
         //匹配默认搜索值
         this.options.queryParams = function (params) {
-            return queryParams(getQueryParams(params, getSearchQuery(this, true)));
+            return queryParams(getQueryParams(params, getSearchQuery(that, true)));
         };
         this.trigger('post-common-search', that);
 
@@ -409,8 +362,8 @@
                 var fval = fp[key].toLowerCase();
                 var value = item[key];
                 value = $.fn.bootstrapTable.utils.calculateObjectValue(that.header,
-                        that.header.formatters[$.inArray(key, that.header.fields)],
-                        [value, item, i], value);
+                    that.header.formatters[$.inArray(key, that.header.fields)],
+                    [value, item, i], value);
 
                 if (!($.inArray(key, that.header.fields) !== -1 &&
                         (typeof value === 'string' || typeof value === 'number') &&

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

@@ -1,4 +1,4 @@
-define(['fast'], function (Fast) {
+define(['fast', 'template'], function (Fast, Template) {
     var Frontend = {
         api: Fast.api,
         init: function () {
@@ -53,6 +53,8 @@ define(['fast'], function (Fast) {
         }
     };
     Frontend.api = $.extend(Fast.api, Frontend.api);
+    //将Template渲染至全局,以便于在子框架中调用
+    window.Template = Template;
     //将Frontend渲染至全局,以便于在子框架中调用
     window.Frontend = Frontend;
 

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 3649 - 5340
public/assets/js/require-backend.min.js


+ 14 - 5
public/assets/js/require-form.js

@@ -1,7 +1,7 @@
 define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, Upload, Validator) {
     var Form = {
         config: {
-            fieldlisttpl: '<dd class="form-inline"><input type="text" name="<%=name%>[<%=index%>][key]" class="form-control" value="<%=row.key%>" size="10" /> <input type="text" name="<%=name%>[<%=index%>][value]" class="form-control" value="<%=row.value%>" size="40" /> <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span></dd>'
+            fieldlisttpl: '<dd class="form-inline"><input type="text" name="<%=name%>[<%=index%>][key]" class="form-control" value="<%=row.key%>" size="10" /> <input type="text" name="<%=name%>[<%=index%>][value]" class="form-control" value="<%=row.value%>" size="30" /> <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span></dd>'
         },
         events: {
             validator: function (form, success, error, submit) {
@@ -26,6 +26,10 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                         }
                     },
                     target: function (input) {
+                        var target = $(input).data("target");
+                        if (target && $(target).size() > 0) {
+                            return $(target);
+                        }
                         var $formitem = $(input).closest('.form-group'),
                             $msgbox = $formitem.find('span.msg-box');
                         if (!$msgbox.length) {
@@ -282,7 +286,7 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                             refresh($(this).closest("dl").data("name"));
                         });
                         //追加控制
-                        $(".fieldlist", form).on("click", ".btn-append", function (e, row) {
+                        $(".fieldlist", form).on("click", ".btn-append,.append", function (e, row) {
                             var container = $(this).closest("dl");
                             var index = container.data("index");
                             var name = container.data("name");
@@ -305,7 +309,7 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                         //拖拽排序
                         $("dl.fieldlist", form).dragsort({
                             itemSelector: 'dd',
-                            dragSelector: ".btn-fdragsort",
+                            dragSelector: ".btn-dragsort",
                             dragEnd: function () {
                                 refresh($(this).closest("dl").data("name"));
                             },
@@ -319,8 +323,13 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                                 return true;
                             }
                             var template = $(this).data("template");
-                            $.each(JSON.parse(textarea.val()), function (i, j) {
-                                $(".btn-append", container).trigger('click', template ? j : {
+                            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
                                 });

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 5 - 1
public/assets/js/require-frontend.min.js


+ 4 - 0
public/assets/less/backend.less

@@ -847,4 +847,8 @@ form.form-horizontal .control-label {
             margin: 2px 0 0;
         }
     }
+}
+
+.wipecache li a {
+    color:#444444!important;
 }