Browse Source

新增CRUD多项字段和表名识别,可自动生成下拉列表、筛选框、单选框、分类列表、日期时间型等等
新增启用nice-validator表单验证功能
新增后台增改时对数组和日期字段的处理
新增test表用于开发测试
新增表单生成禁用字符编码的方法
禁用开发模式的客户端JS和CSS缓存
修复微信支付初始化变量的错误
修复php think min在Win下的错误提示
修复不同语言下HTML的语言标识错误

Karson 9 years ago
parent
commit
d1287bb05f

+ 316 - 158
application/admin/command/Crud.php

@@ -68,7 +68,7 @@ class Crud extends Command
         //非覆盖模式时如果存在控制器文件则报错
         if (is_file($controllerFile) && !$force)
         {
-            throw new Exception('controller already exists');
+            throw new Exception('controller already exists!\nIf you need to rebuild again, use the parameter --force=true ');
         }
 
         //模型默认以表名进行处理,以下划线进行分隔,如果需要自定义则需要传入model,不支持目录层级
@@ -89,9 +89,11 @@ class Crud extends Command
         //非覆盖模式时如果存在模型文件则报错
         if (is_file($modelFile) && !$force)
         {
-            throw new Exception('model already exists');
+            throw new Exception('model already exists!\nIf you need to rebuild again, use the parameter --force=true ');
         }
 
+        require $adminPath . 'common.php';
+
         //从数据库中获取表字段信息
         $columnList = Db::query("SELECT * FROM `information_schema`.`columns` WHERE TABLE_SCHEMA = ? AND table_name = ? ORDER BY ORDINAL_POSITION", [$dbname, $tableName]);
         $fields = [];
@@ -106,194 +108,256 @@ class Crud extends Command
         $langList = [];
         $field = 'id';
         $order = 'id';
+        $priDefined = FALSE;
 
-        //循环所有字段,开始构造视图的HTML和JS信息
-        foreach ($columnList as $k => $v)
+        try
         {
-            $field = $v['COLUMN_NAME'];
-            $fieldLang = ucfirst($field);
-            // 语言列表
-            if ($v['COLUMN_COMMENT'] != '')
-            {
-                $langList[] = $this->getLangItem($field, $v['COLUMN_COMMENT']);
-            }
-            if ($v['COLUMN_KEY'] != 'PRI')
+            Form::setEscapeHtml(false);
+
+            //循环所有字段,开始构造视图的HTML和JS信息
+            foreach ($columnList as $k => $v)
             {
-                $inputType = 'text';
-                $step = 0;
-                switch ($v['DATA_TYPE'])
+                $field = $v['COLUMN_NAME'];
+                $itemArr = [];
+                // 这里构建Enum和Set类型的列表数据
+                if (in_array($v['DATA_TYPE'], ['enum', 'set']))
                 {
-                    case 'bigint':
-                    case 'int':
-                    case 'mediumint':
-                    case 'smallint':
-                    case 'tinyint':
-                        $inputType = 'number';
-                        break;
-                    case 'enum':
-                    case 'set':
-                        $inputType = 'select';
-                        break;
-                    case 'decimal':
-                    case 'double':
-                    case 'float':
-                        $inputType = 'number';
-                        $step = "0." . str_repeat(0, $v['NUMERIC_SCALE'] - 1) . "1";
-                    case 'text':
-                        $inputType = 'textarea';
-                    default:
-                        break;
+                    $itemArr = substr($v['COLUMN_TYPE'], strlen($v['DATA_TYPE']) + 1, -1);
+                    $itemArr = explode(',', str_replace("'", '', $itemArr));
                 }
-                if (substr($field, -4) == 'time')
+                // 语言列表
+                if ($v['COLUMN_COMMENT'] != '')
                 {
-                    $inputType = 'datetime';
+                    $langList[] = $this->getLangItem($field, $v['COLUMN_COMMENT']);
                 }
-
-                if ($inputType == 'select')
+                //createtime和updatetime是保留字段不能修改和添加
+                if ($v['COLUMN_KEY'] != 'PRI' && !in_array($field, ['createtime', 'updatetime']))
                 {
-                    $itemlist = substr($v['COLUMN_TYPE'], strlen($v['DATA_TYPE']) + 1, -1);
-                    $itemlist = str_replace("'", '', $itemlist);
-                    $attr = "'id'=>'c-$field','class'=>'form-control selectpicker'";
-                    if ($v['DATA_TYPE'] == 'enum')
+                    $inputType = $this->getFieldType($v);
+
+                    // 如果是number类型时增加一个步长
+                    $step = $inputType == 'number' && $v['NUMERIC_SCALE'] > 0 ? "0." . str_repeat(0, $v['NUMERIC_SCALE'] - 1) . "1" : 0;
+
+                    $attrArr = ['id' => "c-{$field}"];
+                    $cssClassArr = ['form-control'];
+                    $fieldName = "row[{$field}]";
+                    $defaultValue = $v['COLUMN_DEFAULT'];
+                    $editValue = "{\$row.{$field}}";
+                    // 如果默认值为空,则是一个必选项
+                    if ($v['COLUMN_DEFAULT'] == '')
                     {
-                        $attr .= ",'multiple'=>''";
+                        $attrArr['required'] = '';
                     }
-                    if ($v['COLUMN_DEFAULT'] == '')
+                    if ($field == 'status' && in_array($inputType, ['text', 'number']))
                     {
-                        $attr .= ",'required'=>''";
+                        //如果状态类型不是enum或set
+                        $itemArr = !$itemArr ? ['normal', 'hidden'] : $itemArr;
+                        $inputType = 'radio';
                     }
-                    $formAddElement = "{:build_select('row[$field]', '{$itemlist}', null, [{$attr}])}";
-                    $formEditElement = "{:build_select('row[$field]', '{$itemlist}', \$row['$field'], [{$attr}])}";
-                }
-                else
-                {
-                    //CSS类名
-                    $cssClass = ['form-control'];
-                    $cssClass[] = substr($field, -4) == 'time' ? 'datetimepicker' : '';
-                    $cssClass[] = $v['DATA_TYPE'] == 'text' ? 'summernote' : '';
-                    $cssClass[] = substr($field, -3) == '_id' ? 'typeahead' : '';
-                    $cssClass[] = substr($field, -4) == '_ids' ? 'tagsinput' : '';
-                    $cssClass = array_filter($cssClass);
-                    //因为有自动完成可输入其它内容
-                    if (array_intersect($cssClass, ['typeahead', 'tagsinput']))
+                    if ($inputType == 'select')
                     {
-                        $inputType = 'text';
-                        $step = 0;
+                        $cssClassArr[] = 'selectpicker';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        if ($v['DATA_TYPE'] == 'set')
+                        {
+                            $attrArr['multiple'] = '';
+                            $fieldName.="[]";
+                        }
+                        $attrStr = $this->getArrayString($attrArr);
+                        $itemArr = $this->getLangArray($itemArr, FALSE);
+                        $itemString = $this->getArrayString($itemArr);
+                        $formAddElement = "{:build_select('{$fieldName}', [{$itemString}], '{$defaultValue}', [{$attrStr}])}";
+                        $formEditElement = "{:build_select('{$fieldName}', [{$itemString}], \$row.{$field}, [{$attrStr}])}";
                     }
-                    $attr = ['id' => "c-{$field}", 'class' => implode(' ', $cssClass)];
-                    if ($step)
+                    else if ($inputType == 'datetime')
                     {
-                        $attr['step'] = $step;
+                        $cssClassArr[] = 'datetimepicker';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $format = "YYYY-MM-DD HH:mm:ss";
+                        $phpFormat = "Y-m-d H:i:s";
+                        $fieldFunc = '';
+                        switch ($v['DATA_TYPE'])
+                        {
+                            case 'year';
+                                $format = "YYYY";
+                                $phpFormat = 'Y';
+                                break;
+                            case 'date';
+                                $format = "YYYY-MM-DD";
+                                $phpFormat = 'Y-m-d';
+                                break;
+                            case 'time';
+                                $format = "HH:mm:ss";
+                                $phpFormat = 'H:i:s';
+                                break;
+                            case 'timestamp';
+                                $fieldFunc = 'datetime';
+                            case 'datetime';
+                                $format = "YYYY-MM-DD HH:mm:ss";
+                                $phpFormat = 'Y-m-d H:i:s';
+                                break;
+                            default:
+                                $fieldFunc = 'datetime';
+                                break;
+                        }
+                        $defaultDateTime = "{:date('{$phpFormat}')}";
+                        $attrArr['data-date-format'] = $format;
+                        $attrArr['data-use-current'] = "true";
+                        $fieldFunc = $fieldFunc ? "|{$fieldFunc}" : "";
+                        $formAddElement = Form::text($fieldName, $defaultDateTime, $attrArr);
+                        $formEditElement = Form::text($fieldName, "{\$row.{$field}{$fieldFunc}}", $attrArr);
                     }
-                    //如果是图片则额外附加
-                    if (substr($field, -5) == 'image' || substr($field, -6) == 'avatar')
+                    else if ($inputType == 'checkbox')
                     {
-                        //$attr['data-plupload-id'] = "plupload-{$field}-text";
-                        $attr['size'] = 50;
+                        $fieldName.="[]";
+                        $itemArr = $this->getLangArray($itemArr, FALSE);
+                        $itemString = $this->getArrayString($itemArr);
+                        $formAddElement = "{:build_checkboxs('{$fieldName}', [{$itemString}], '{$defaultValue}')}";
+                        $formEditElement = "{:build_checkboxs('{$fieldName}', [{$itemString}], \$row.{$field})}";
                     }
-                    $fieldFunc = substr($field, -4) == 'time' ? "|datetime" : "";
-                    if ($inputType == 'textarea')
+                    else if ($inputType == 'radio')
                     {
-                        $formAddElement = Form::textarea("row[{$field}]", $v['COLUMN_DEFAULT'], $attr);
-                        $formEditElement = Form::textarea("row[{$field}]", "{\$row.{$field}{$fieldFunc}}", $attr);
+                        $itemArr = $this->getLangArray($itemArr, FALSE);
+                        $itemString = $this->getArrayString($itemArr);
+                        $defaultValue = $defaultValue ? $defaultValue : key($itemArr);
+                        $formAddElement = "{:build_radios('{$fieldName}', [{$itemString}], '{$defaultValue}')}";
+                        $formEditElement = "{:build_radios('{$fieldName}', [{$itemString}], \$row.{$field})}";
                     }
-                    else
+                    else if ($inputType == 'textarea' || ($inputType == 'text' && $v['CHARACTER_MAXIMUM_LENGTH'] >= 255))
                     {
-                        $formAddElement = Form::input($inputType, "row[{$field}]", $v['COLUMN_DEFAULT'], $attr);
-                        $formEditElement = Form::input($inputType, "row[{$field}]", "{\$row.{$field}{$fieldFunc}}", $attr);
+                        $cssClassArr[] = substr($field, -7) == 'content' ? 'summernote' : '';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $attrArr['rows'] = 5;
+                        $formAddElement = Form::textarea($fieldName, $defaultValue, $attrArr);
+                        $formEditElement = Form::textarea($fieldName, $editValue, $attrArr);
                     }
-
-                    if (substr($field, -5) == 'image' || substr($field, -6) == 'avatar')
+                    else if ($field == 'category_id' || $field == 'category_ids')
                     {
-                        //如果是图片或头像
-                        $formAddElement = $this->getImageUpload($field, $formAddElement);
-                        $formEditElement = $this->getImageUpload($field, $formEditElement);
+                        $type = $table;
+                        if ($field == 'category_ids')
+                        {
+                            $attrArr['multiple'] = '';
+                        }
+                        $attrStr = $this->getArrayString($attrArr);
+                        $formAddElement = "{:build_category_select('{$fieldName}', '{$type}', '{$defaultValue}', [{$attrStr}])}";
+                        $formEditElement = "{:build_category_select('{$fieldName}', '{$type}', \$row.{$field}, [{$attrStr}])}";
                     }
-                    else if ($field == 'status')
+                    else
                     {
-                        //如果是状态字段
-                        $formAddElement = "{:build_radios('row[status]', ['normal'=>__('Normal'), 'hidden'=>__('Hidden')])}";
-                        $formEditElement = "{:build_radios('row[status]', ['normal'=>__('Normal'), 'hidden'=>__('Hidden')], \$row['status'])}";
+                        //CSS类名
+                        $cssClassArr[] = substr($field, -3) == '_id' ? 'typeahead' : '';
+                        $cssClassArr[] = substr($field, -4) == '_ids' ? 'tagsinput' : '';
+                        $cssClassArr = array_filter($cssClassArr);
+                        //因为有自动完成可输入其它内容
+                        $step = array_intersect($cssClassArr, ['typeahead', 'tagsinput']) ? 0 : $step;
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+
+                        $isUpload = substr($field, -4) == 'file' || substr($field, -5) == 'image' || substr($field, -6) == 'avatar' ? TRUE : FALSE;
+                        //如果是步长则加上步长
+                        if ($step)
+                        {
+                            $attrArr['step'] = $step;
+                        }
+                        //如果是图片加上个size
+                        if ($isUpload)
+                        {
+                            $attrArr['size'] = 50;
+                        }
+
+                        $formAddElement = Form::input($inputType, $fieldName, $defaultValue, $attrArr);
+                        $formEditElement = Form::input($inputType, $fieldName, $editValue, $attrArr);
+
+                        //如果是图片或文件
+                        if ($isUpload)
+                        {
+                            $formAddElement = $this->getImageUpload($field, $formAddElement);
+                            $formEditElement = $this->getImageUpload($field, $formEditElement);
+                        }
                     }
+                    //构造添加和编辑HTML信息
+                    $addList[] = $this->getFormGroup($field, $formAddElement);
+                    $editList[] = $this->getFormGroup($field, $formEditElement);
                 }
-                //构造添加和编辑HTML信息
-                $addList[] = $this->getFormGroup($field, $formAddElement);
-                $editList[] = $this->getFormGroup($field, $formEditElement);
-            }
 
-            //过滤text类型字段
-            if ($v['DATA_TYPE'] != 'text')
-            {
-                //主键
-                if ($v['COLUMN_KEY'] == 'PRI')
+                //过滤text类型字段
+                if ($v['DATA_TYPE'] != 'text')
                 {
-                    $javascriptList[] = "{field: 'state', checkbox: true}";
+                    //主键
+                    if ($v['COLUMN_KEY'] == 'PRI' && !$priDefined)
+                    {
+                        $priDefined = TRUE;
+                        $javascriptList[] = "{field: 'state', checkbox: true}";
+                    }
+                    //构造JS列信息
+                    $javascriptList[] = $this->getJsColumn($field);
+                    //排序方式,如果有weigh则按weigh,否则按主键排序
+                    $order = $field == 'weigh' ? 'weigh' : $order;
                 }
-                //构造JS列信息
-                $javascriptList[] = $this->getJsColumn($field);
-                //排序方式,如果有weigh则按weigh,否则按主键排序
-                $order = $field == 'weigh' ? 'weigh' : $order;
+            }
+            //JS最后一列加上操作列
+            $javascriptList[] = str_repeat(" ", 24) . "{field: 'operate', title: __('Operate'), events: Table.api.events.operate, formatter: Table.api.formatter.operate}";
+            $addList = implode("\n", array_filter($addList));
+            $editList = implode("\n", array_filter($editList));
+            $javascriptList = implode(",\n", array_filter($javascriptList));
+            $langList = implode(",\n", array_filter($langList));
+
+            //表注释
+            $tableComment = $tableInfo['Comment'];
+            $tableComment = mb_substr($tableComment, -1) == '表' ? mb_substr($tableComment, 0, -1) . '管理' : $tableComment;
+
+            //最终将生成的文件路径
+            $controllerFile = $adminPath . 'controller' . DS . $controllerFile;
+            $javascriptFile = ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'backend' . DS . $controllerUrl . '.js';
+            $addFile = $adminPath . 'view' . DS . $controllerUrl . DS . 'add.html';
+            $editFile = $adminPath . 'view' . DS . $controllerUrl . DS . 'edit.html';
+            $indexFile = $adminPath . 'view' . DS . $controllerUrl . DS . 'index.html';
+            $langFile = $adminPath . 'lang' . DS . Lang::detect() . DS . $controllerUrl . '.php';
+
+            $appNamespace = Config::get('app_namespace');
+            $moduleName = 'admin';
+            $controllerNamespace = "{$appNamespace}\\{$moduleName}\\controller" . ($controllerDir ? "\\" : "") . str_replace('/', "\\", $controllerDir);
+            $modelNamespace = "{$appNamespace}\\" . ($local ? $moduleName : "common") . "\\model";
+
+            $data = [
+                'controllerNamespace'     => $controllerNamespace,
+                'modelNamespace'          => $modelNamespace,
+                'controllerUrl'           => $controllerUrl,
+                'controllerDir'           => $controllerDir,
+                'controllerName'          => $controllerName,
+                'modelName'               => $modelName,
+                'tableComment'            => $tableComment,
+                'iconName'                => $iconName,
+                'order'                   => $order,
+                'table'                   => $table,
+                'tableName'               => $tableName,
+                'addList'                 => $addList,
+                'editList'                => $editList,
+                'javascriptList'          => $javascriptList,
+                'langList'                => $langList,
+                'modelAutoWriteTimestamp' => in_array('createtime', $fields) || in_array('updatetime', $fields) ? "'int'" : 'false',
+                'createTime'              => in_array('createtime', $fields) ? "'createtime'" : 'false',
+                'updateTime'              => in_array('updatetime', $fields) ? "'updatetime'" : 'false',
+            ];
+
+            // 生成控制器文件
+            $result = $this->writeToFile('controller', $data, $controllerFile);
+            // 生成模型文件
+            $result = $this->writeToFile('model', $data, $modelFile);
+            // 生成视图文件
+            $result = $this->writeToFile('add', $data, $addFile);
+            $result = $this->writeToFile('edit', $data, $editFile);
+            $result = $this->writeToFile('index', $data, $indexFile);
+            // 生成JS文件
+            $result = $this->writeToFile('javascript', $data, $javascriptFile);
+            // 生成语言文件
+            if ($langList)
+            {
+                $result = $this->writeToFile('lang', $data, $langFile);
             }
         }
-        //JS最后一列加上操作列
-        $javascriptList[] = str_repeat(" ", 24) . "{field: 'operate', title: __('Operate'), events: Table.api.events.operate, formatter: Table.api.formatter.operate}";
-        $addList = implode("\n", array_filter($addList));
-        $editList = implode("\n", array_filter($editList));
-        $javascriptList = implode(",\n", array_filter($javascriptList));
-        $langList = implode(",\n", array_filter($langList));
-
-        //表注释
-        $tableComment = $tableInfo['Comment'];
-        $tableComment = mb_substr($tableComment, -1) == '表' ? mb_substr($tableComment, 0, -1) . '管理' : $tableComment;
-
-        //最终将生成的文件路径
-        $controllerFile = $adminPath . 'controller' . DS . $controllerFile;
-        $javascriptFile = ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'backend' . DS . $controllerUrl . '.js';
-        $addFile = $adminPath . 'view' . DS . $controllerUrl . DS . 'add.html';
-        $editFile = $adminPath . 'view' . DS . $controllerUrl . DS . 'edit.html';
-        $indexFile = $adminPath . 'view' . DS . $controllerUrl . DS . 'index.html';
-        $langFile = $adminPath . 'lang' . DS . Lang::detect() . DS . $controllerUrl . '.php';
-
-        $appNamespace = Config::get('app_namespace');
-        $moduleName = 'admin';
-        $controllerNamespace = "{$appNamespace}\\{$moduleName}\\controller" . ($controllerDir ? "\\" : "") . str_replace('/', "\\", $controllerDir);
-        $modelNamespace = "{$appNamespace}\\" . ($local ? $moduleName : "common") . "\\model";
-
-        $data = [
-            'controllerNamespace'     => $controllerNamespace,
-            'modelNamespace'          => $modelNamespace,
-            'controllerUrl'           => $controllerUrl,
-            'controllerDir'           => $controllerDir,
-            'controllerName'          => $controllerName,
-            'modelName'               => $modelName,
-            'tableComment'            => $tableComment,
-            'iconName'                => $iconName,
-            'order'                   => $order,
-            'table'                   => $table,
-            'tableName'               => $tableName,
-            'addList'                 => $addList,
-            'editList'                => $editList,
-            'javascriptList'          => $javascriptList,
-            'langList'                => $langList,
-            'modelAutoWriteTimestamp' => in_array('createtime', $fields) || in_array('updatetime', $fields) ? "'int'" : 'false',
-            'createTime'              => in_array('createtime', $fields) ? "'createtime'" : 'false',
-            'updateTime'              => in_array('updatetime', $fields) ? "'updatetime'" : 'false',
-        ];
-
-        // 生成控制器文件
-        $result = $this->writeToFile('controller', $data, $controllerFile);
-        // 生成模型文件
-        $result = $this->writeToFile('model', $data, $modelFile);
-        // 生成视图文件
-        $result = $this->writeToFile('add', $data, $addFile);
-        $result = $this->writeToFile('edit', $data, $editFile);
-        $result = $this->writeToFile('index', $data, $indexFile);
-        // 生成JS文件
-        $result = $this->writeToFile('javascript', $data, $javascriptFile);
-        // 生成语言文件
-        if ($langList)
+        catch (\think\exception\ErrorException $e)
         {
-            $result = $this->writeToFile('lang', $data, $langFile);
+            print_r($e);
         }
         $output->writeln("<info>Build Successed</info>");
     }
@@ -348,6 +412,99 @@ EOD;
     }
 
     /**
+     * 读取数据和语言数组列表
+     * @param array $arr
+     * @return array
+     */
+    protected function getLangArray($arr, $withTpl = TRUE)
+    {
+        $langArr = [];
+        foreach ($arr as $k => $v)
+        {
+            $langArr[(is_numeric($k) ? $v : $k)] = is_numeric($k) ? ($withTpl ? "{:" : "") . "__('" . ucfirst($v) . "')" . ($withTpl ? "}" : "") : $v;
+        }
+        return $langArr;
+    }
+
+    /**
+     * 将数据转换成带字符串
+     * @param array $arr
+     * @return string
+     */
+    protected function getArrayString($arr)
+    {
+        $stringArr = [];
+        foreach ($arr as $k => $v)
+        {
+            $is_var = in_array(substr($v, 0, 1), ['$', '_']);
+            if (!$is_var)
+            {
+                $v = str_replace("'", "\'", $v);
+                $k = str_replace("'", "\'", $k);
+            }
+            $stringArr[] = "'" . (is_numeric($k) ? $v : $k) . "' => " . (is_numeric($k) ? "__('" . ucfirst($k) . "')" : $is_var ? $v : "'{$v}'");
+        }
+        return implode(",", $stringArr);
+    }
+
+    protected function getFieldType(& $v)
+    {
+        $inputType = 'text';
+        switch ($v['DATA_TYPE'])
+        {
+            case 'bigint':
+            case 'int':
+            case 'mediumint':
+            case 'smallint':
+            case 'tinyint':
+                $inputType = 'number';
+                break;
+            case 'enum':
+            case 'set':
+                $inputType = 'select';
+                break;
+            case 'decimal':
+            case 'double':
+            case 'float':
+                $inputType = 'number';
+                break;
+            case 'longtext':
+            case 'text':
+            case 'mediumtext':
+            case 'smalltext':
+            case 'tinytext':
+                $inputType = 'textarea';
+                break;
+            case 'year';
+            case 'date';
+            case 'time';
+            case 'datetime';
+            case 'timestamp';
+                $inputType = 'datetime';
+                break;
+            default:
+                break;
+        }
+        $fieldsName = $v['COLUMN_NAME'];
+        // 如果后缀以time结尾说明也是个时间字段
+        if (substr($fieldsName, -4) == 'time')
+        {
+            $inputType = 'datetime';
+        }
+        // 如果后缀以data结尾且类型为enum,说明是个单选框
+        if (substr($fieldsName, -4) == 'data' && $v['DATA_TYPE'] == 'enum')
+        {
+            $inputType = "radio";
+        }
+        // 如果后缀以data结尾且类型为set,说明是个复选框
+        if (substr($fieldsName, -4) == 'data' && $v['DATA_TYPE'] == 'set')
+        {
+            $inputType = "checkbox";
+        }
+        return $inputType;
+    }
+
+    /**
      * 获取表单分组数据
      * @param string $field
      * @param string $content
@@ -374,10 +531,11 @@ EOD;
      */
     protected function getImageUpload($field, $content)
     {
+        $filter = substr($field, -4) == 'avatar' || substr($field, -5) == 'image' ? 'data-mimetype="image/*"' : "";
         return <<<EOD
 <div class="form-inline">
                 {$content}
-                <span><button id="plupload-{$field}" class="btn btn-danger plupload" data-input-id="c-{$field}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                <span><button id="plupload-{$field}" class="btn btn-danger plupload" data-input-id="c-{$field}"{$filter}><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
             </div>
 EOD;
     }

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

@@ -262,6 +262,40 @@ CREATE TABLE `fa_page` (
 ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='单页表';
 
 -- ----------------------------
+--  Table structure for `fa_test`
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_test`;
+CREATE TABLE `fa_test` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `category_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '分类ID(单选)',
+  `category_ids` varchar(100) NOT NULL DEFAULT '' COMMENT '分类ID(多选)',
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `user_ids` varchar(100) NOT NULL DEFAULT '' COMMENT '多会员ID',
+  `week` enum('monday','tuesday','wednesday') NOT NULL COMMENT '星期(单选)',
+  `flag` set('hot','index','recommend') NOT NULL DEFAULT '' COMMENT '标志(多选)',
+  `genderdata` enum('male','female') NOT NULL DEFAULT 'male' COMMENT '性别(单选)',
+  `hobbydata` set('music','reading','swimming') NOT NULL COMMENT '爱好(多选)',
+  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '标题',
+  `content` text NOT NULL COMMENT '内容',
+  `image` varchar(100) NOT NULL DEFAULT '' COMMENT '图片',
+  `attachfile` varchar(100) NOT NULL DEFAULT '' COMMENT '附件',
+  `keywords` varchar(100) NOT NULL DEFAULT '' COMMENT '关键字',
+  `description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述',
+  `price` float(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '价格',
+  `views` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '点击',
+  `startdate` date DEFAULT NULL COMMENT '开始日期',
+  `activitydate` datetime DEFAULT NULL COMMENT '活动时间(datetime)',
+  `year` year(4) DEFAULT NULL COMMENT '年',
+  `times` time DEFAULT NULL COMMENT '时间',
+  `refreshtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '刷新时间(int)',
+  `createtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
+  `updatetime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `status` varchar(30) NOT NULL DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='测试表';
+
+-- ----------------------------
 --  Table structure for `fa_user`
 -- ----------------------------
 DROP TABLE IF EXISTS `fa_user`;

+ 19 - 8
application/admin/command/Min.php

@@ -51,17 +51,28 @@ class Min extends Command
         $publicPath = ROOT_PATH . 'public' . DS;
         $tempFile = $minPath . 'temp.js';
 
-        try
+        // Winsows下请手动配置配置该值
+        $nodeExec = "";
+
+        if (!$nodeExec)
         {
-            $nodeExec = exec("which node");
-            if (!$nodeExec)
+            if (IS_WIN)
             {
-                throw new Exception("node environment not found!please install node first!");
+                throw new Exception("node environment require nodejs!please check http://doc.fastadmin.net/322813 !");
+            }
+
+            try
+            {
+                $nodeExec = exec("which node");
+                if (!$nodeExec)
+                {
+                    throw new Exception("node environment not found!please install node first!");
+                }
+            }
+            catch (Exception $e)
+            {
+                throw new Exception($e->getMessage());
             }
-        }
-        catch (Exception $e)
-        {
-            throw new Exception($e->getMessage());
         }
 
         foreach ($moduleArr as $mod)

+ 35 - 14
application/admin/common.php

@@ -1,27 +1,24 @@
 <?php
 
 use app\admin\library\Auth;
+use app\common\model\Category;
 use app\common\model\Configvalue;
 use fast\Form;
+use fast\Tree;
 use think\Db;
 
-function get_upload_multipart($savekey = '', $mimetype = '', $maxsize = '')
+/**
+ * 重新生成上传的参数配置
+ * @return type
+ */
+function get_upload_multipart($params = [])
 {
     // 加载配置
     $configvalue = new Configvalue;
     // 上传参数配置配置
-    $uploadcfg = $configvalue->upload($savekey, $mimetype, $maxsize);
+    $uploadcfg = $configvalue->upload($params);
 
-    return json_encode(['policy' => $uploadcfg['policy'], 'signature' => $uploadcfg['signature']]);
-}
-
-function get_flag_list()
-{
-    return [
-        'h' => __('Hot'),
-        'i' => __('Index'),
-        'r' => __('Recommend'),
-    ];
+    return json_encode(isset($uploadcfg['multipart']) ? $uploadcfg['multipart'] : []);
 }
 
 /**
@@ -50,9 +47,10 @@ function build_radios($name, $list = [], $selected = null)
 {
     $html = [];
     $selected = is_null($selected) ? key($list) : $selected;
+    $selected = is_array($selected) ? $selected : explode(',', $selected);
     foreach ($list as $k => $v)
     {
-        $html[] = sprintf(Form::label("{$name}-{$k}", "%s {$v}"), Form::radio($name, $k, $k == $selected, ['id' => "{$name}-{$k}"]));
+        $html[] = sprintf(Form::label("{$name}-{$k}", "%s {$v}"), Form::radio($name, $k, in_array($k, $selected), ['id' => "{$name}-{$k}"]));
     }
     return implode(' ', $html);
 }
@@ -68,14 +66,37 @@ function build_checkboxs($name, $list = [], $selected = null)
 {
     $html = [];
     $selected = is_null($selected) ? [] : $selected;
+    $selected = is_array($selected) ? $selected : explode(',', $selected);
     foreach ($list as $k => $v)
     {
-        $html[] = sprintf(Form::label("{$name}-{$k}", "%s {$v}"), Form::checkbox($name, $k, $k == $selected, ['id' => "{$name}-{$k}"]));
+        $html[] = sprintf(Form::label("{$name}-{$k}", "%s {$v}"), Form::checkbox($name, $k, in_array($k, $selected), ['id' => "{$name}-{$k}"]));
     }
     return implode(' ', $html);
 }
 
 /**
+ * 生成分类下拉列表框
+ * @param string $name
+ * @param string $type
+ * @param mixed $selected
+ * @param array $attr
+ * @return string
+ */
+function build_category_select($name, $type, $selected = null, $attr = [])
+{
+    $tree = Tree::instance();
+    $tree->init(Category::getCategoryArray($type), 'pid');
+    $categorylist = $tree->getTreeList($tree->getTreeArray(0), 'name');
+    $categorydata = [0 => __('None')];
+    foreach ($categorylist as $k => $v)
+    {
+        $categorydata[$v['id']] = $v['name'];
+    }
+    $attr = array_merge(['id' => "c-{$name}", 'class' => 'form-control selectpicker'], $attr);
+    return build_select($name, $categorydata, $selected, $attr);
+}
+
+/**
  * 生成表格操作按钮栏
  * @param array $btns
  * @return string

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

@@ -43,6 +43,11 @@ trait Backend
             $params = $this->request->post("row/a");
             if ($params)
             {
+                foreach ($params as $k => &$v)
+                {
+                    $v = is_array($v) ? implode(',', $v) : $v;
+                    $v = substr($k, -4) == 'time' && !is_numeric($v) ? strtotime($v) : $v;
+                }
                 $this->model->create($params);
                 AdminLog::record(__('Add'), $this->model->getLastInsID());
                 $this->code = 1;
@@ -67,6 +72,11 @@ trait Backend
             $params = $this->request->post("row/a");
             if ($params)
             {
+                foreach ($params as $k => &$v)
+                {
+                    $v = is_array($v) ? implode(',', $v) : $v;
+                    $v = substr($k, -4) == 'time' && !is_numeric($v) ? strtotime($v) : $v;
+                }
                 $row->save($params);
                 AdminLog::record(__('Edit'), $ids);
                 $this->code = 1;

+ 1 - 1
application/admin/view/layout/default.html

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="{$config.language}">
     <head>
         {include file="common/meta" /}
     </head>

+ 1 - 2
application/admin/view/page/add.html

@@ -1,5 +1,4 @@
 <form id="add-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action="">
-
     <div class="form-group">
         <label for="c-category_id" class="control-label col-xs-12 col-sm-2">{:__('Category_id')}:</label>
         <div class="col-xs-12 col-sm-8">
@@ -48,7 +47,7 @@
     <div class="form-group">
         <label for="c-views" class="control-label col-xs-12 col-sm-2">{:__('Views')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-views" class="form-control" name="row[views]" type="number" value="0">
+            <input id="c-views" class="form-control" name="row[views]" type="text" value="0">
         </div>
     </div>
     <div class="form-group">

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

@@ -20,6 +20,11 @@ class Common
         {
             Config::set('site.cdnurl', $cdnurl);
         }
+        // 如果是调试模式将version置为当前的时间戳可避免缓存
+        if (!Config::get('app_debug'))
+        {
+            Config::set('site.version', time());
+        }
     }
 
 }

+ 37 - 5
extend/fast/Form.php

@@ -89,6 +89,12 @@ class FormBuilder
      * @var array
      */
     protected $skipValueTypes = array('file', 'password', 'checkbox', 'radio');
+
+    /**
+     * Escape html
+     * @var boolean
+     */
+    protected $escapeHtml = true;
     protected static $instance;
 
     /**
@@ -180,6 +186,32 @@ class FormBuilder
     }
 
     /**
+     * Set the escape html mode
+     * @param boolean $escape
+     */
+    public function setEscapeHtml($escape)
+    {
+        $this->escapeHtml = $escape;
+    }
+
+    /**
+     * Escape HTML special characters in a string.
+     * @return string
+     */
+    public function escape($value)
+    {
+        if (!$this->escapeHtml)
+        {
+            return $value;
+        }
+        if (is_array($value))
+        {
+            $value = json_encode($value, JSON_UNESCAPED_UNICODE);
+        }
+        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
+    }
+
+    /**
      * Close the current form.
      *
      * @return string
@@ -222,7 +254,7 @@ class FormBuilder
 
         $options = $this->attributes($options);
 
-        $value = e($this->formatLabel($name, $value));
+        $value = $this->escape($this->formatLabel($name, $value));
 
         return '<label for="' . $name . '"' . $options . '>' . $value . '</label>';
     }
@@ -378,7 +410,7 @@ class FormBuilder
         // the element. Then we'll create the final textarea elements HTML for us.
         $options = $this->attributes($options);
 
-        return '<textarea' . $options . '>' . e($value) . '</textarea>';
+        return '<textarea' . $options . '>' . $this->escape($value) . '</textarea>';
     }
 
     /**
@@ -547,7 +579,7 @@ class FormBuilder
             $html[] = $this->option($display, $value, $selected);
         }
 
-        return '<optgroup label="' . e($label) . '">' . implode('', $html) . '</optgroup>';
+        return '<optgroup label="' . $this->escape($label) . '">' . implode('', $html) . '</optgroup>';
     }
 
     /**
@@ -562,9 +594,9 @@ class FormBuilder
     {
         $selected = $this->getSelectedValue($value, $selected);
 
-        $options = array('value' => e($value), 'selected' => $selected);
+        $options = array('value' => $this->escape($value), 'selected' => $selected);
 
-        return '<option' . $this->attributes($options) . '>' . e($display) . '</option>';
+        return '<option' . $this->attributes($options) . '>' . $this->escape($display) . '</option>';
     }
 
     /**

+ 1 - 1
extend/fast/payment/Wechat.php

@@ -50,7 +50,7 @@ class Wechat
     {
         if ($config = Config::get('payment.wechat'))
         {
-            $this->config = array_merge($this->config, $config);
+            $this->_config = array_merge($this->_config, $config);
         }
         $this->_config = array_merge($this->_config, is_array($options) ? $options : []);
     }

+ 3 - 0
public/assets/css/backend-func.css

@@ -287,6 +287,9 @@ body {
 .fixed-table-container {
   border: none!important;
 }
+.note-editor .note-editing-area .note-editable {
+  display: block !important;
+}
 .pjax-loader-bar .progress {
   position: fixed;
   top: 0;

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


+ 21 - 0
public/assets/js/backend/page.js

@@ -54,6 +54,27 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         },
         api: {
             bindevent: function () {
+                $("form[role=form]").validator({
+                    rules: {
+                        mobile: [/^1[3-9]\d{9}$/, "请填写有效的手机号"],
+                        chinese: [/^[\u0391-\uFFE5]+$/, "请填写中文字符"],
+                        // 使用函数定义规则
+                        phone: function (elem, param) {
+                            return /^1[3458]\d{9}$/.test($(elem).val()) || '请检查手机号格式';
+                        },
+                        image: function (elem, param) {
+                            return /^\/(.*)\.(jpg|jpeg|png|gif)$/.test($(elem).val()) || '请上传有并行的图片文件';
+                        }
+                    },
+                    messages: {
+                    },
+                    fields: {
+                        'row[title]': "required;length(3~16)",
+                        'row[image]': "required;image",
+                        'row[views]': "required;range[0~100]",
+                        'row[content]': "required"
+                    },
+                });
                 Form.api.bindevent($("form[role=form]"));
             }
         }

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


+ 9 - 16
public/assets/js/require-form.js

@@ -1,4 +1,4 @@
-define(['jquery', 'bootstrap', 'backend', 'config', 'toastr', 'upload', 'bootstrap-validator', 'bootstrap-checkbox', 'bootstrap-radio', 'bootstrap-switch'], function ($, undefined, Backend, Config, Toastr, Upload, undefined) {
+define(['jquery', 'bootstrap', 'backend', 'config', 'toastr', 'upload', 'validator'], function ($, undefined, Backend, Config, Toastr, Upload, Validator) {
     var Form = {
         config: {
         },
@@ -64,11 +64,14 @@ define(['jquery', 'bootstrap', 'backend', 'config', 'toastr', 'upload', 'bootstr
                 return false;
             },
             bindevent: function (form, onBeforeSubmit, onAfterSubmit) {
-                form.validator().on('submit', function (e) {
-                    if (e.isDefaultPrevented()) {
-                        //验证不通过
-                        Toastr.error("验证失败,请检查表单输入是否正确");
-                    } else {
+                form.validator({
+                    validClass: 'has-success',
+                    invalidClass: 'has-error',
+                    bindClassTo: '.form-group',
+                    formClass: 'n-default n-bootstrap',
+                    msgClass: 'n-right',
+                    stopOnError: true,
+                    valid: function (ret) {
                         //验证通过提交表单
                         Form.api.submit(form, onBeforeSubmit, function (data) {
                             if (typeof onAfterSubmit == 'function') {
@@ -86,16 +89,6 @@ define(['jquery', 'bootstrap', 'backend', 'config', 'toastr', 'upload', 'bootstr
                     }
                 });
 
-                // Activate the switches with icons
-                if ($('.switch').length != 0) {
-                    $('.switch')['bootstrapSwitch']();
-                }
-
-                // Activate regular switches
-                if ($("[data-toggle='switch']").length != 0) {
-                    $("[data-toggle='switch']").wrap('<div class="switch" />').parent().bootstrapSwitch();
-                }
-
                 //绑定select元素事件
                 if ($(".selectpicker", form).size() > 0) {
                     require(['bootstrap-select'], function () {

+ 126 - 126
public/assets/js/require-frontend.min.js

@@ -208,7 +208,7 @@ define('../libs/require-css/css.min',[],function(){if("undefined"==typeof window
  @Author:贤心
  @Site:http://layer.layui.com
  @License:LGPL
-
+    
  */
 
 ;!function(window, undefined){
@@ -244,39 +244,39 @@ var layer = {
     layer.cache = ready.config = $.extend({}, ready.config, options);
     layer.path = ready.config.path || layer.path;
     typeof options.extend === 'string' && (options.extend = [options.extend]);
-
+    
     if(ready.config.path) layer.ready();
-
+    
     if(!options.extend) return this;
-
-    isLayui
+    
+    isLayui 
       ? layui.addcss('modules/layer/' + options.extend)
     : layer.link('skin/' + options.extend);
-
+    
     return this;
   },
-
+  
   //载入CSS配件
   link: function(href, fn, cssname){
-
+    
     //未设置路径,则不主动加载css
     if(!layer.path) return;
-
+    
     var head = $('head')[0], link = document.createElement('link');
     if(typeof fn === 'string') cssname = fn;
     var app = (cssname || href).replace(/\.|\//g, '');
     var id = 'layuicss-'+app, timeout = 0;
-
+    
     link.rel = 'stylesheet';
     link.href = layer.path + href;
     link.id = id;
-
+    
     if(!$('#'+ id)[0]){
       head.appendChild(link);
     }
-
+    
     if(typeof fn !== 'function') return;
-
+    
     //轮询css是否加载完毕
     (function poll() {
       if(++timeout > 8 * 1000 / 100){
@@ -285,14 +285,14 @@ var layer = {
       parseInt($('#'+id).css('width')) === 1989 ? fn() : setTimeout(poll, 100);
     }());
   },
-
+  
   ready: function(callback){
     var cssname = 'skinlayercss', ver = '1110';
     isLayui ? layui.addcss('modules/layer/default/layer.css?v='+layer.v+ver, callback, cssname)
     : layer.link('skin/default/layer.css?v='+layer.v+ver, callback, cssname);
     return this;
   },
-
+  
   //各种快捷引用
   alert: function(content, options, yes){
     var type = typeof options === 'function';
@@ -301,9 +301,9 @@ var layer = {
       content: content,
       yes: yes
     }, type ? {} : options));
-  },
-
-  confirm: function(content, options, yes, cancel){
+  }, 
+  
+  confirm: function(content, options, yes, cancel){ 
     var type = typeof options === 'function';
     if(type){
       cancel = yes;
@@ -316,7 +316,7 @@ var layer = {
       btn2: cancel
     }, type ? {} : options));
   },
-
+  
   msg: function(content, options, end){ //最常用提示层
     var type = typeof options === 'function', rskin = ready.config.skin;
     var skin = (rskin ? rskin + ' ' + rskin + '-msg' : '')||'layui-layer-msg';
@@ -341,9 +341,9 @@ var layer = {
          options.skin = skin + ' ' + (options.skin||'layui-layer-hui');
        }
        return options;
-    }()));
+    }()));  
   },
-
+  
   load: function(icon, options){
     return layer.open($.extend({
       type: 3,
@@ -351,8 +351,8 @@ var layer = {
       resize: false,
       shade: 0.01
     }, options));
-  },
-
+  }, 
+  
   tips: function(content, follow, options){
     return layer.open($.extend({
       type: 4,
@@ -367,7 +367,7 @@ var layer = {
   }
 };
 
-var Class = function(setings){
+var Class = function(setings){  
   var that = this;
   that.index = ++layer.index;
   that.config = $.extend({}, that.config, ready.config, setings);
@@ -393,7 +393,7 @@ Class.pt.config = {
   area: 'auto',
   closeBtn: 1,
   time: 0, //0表示不自动关闭
-  zIndex: 19891014,
+  zIndex: 19891014, 
   maxWidth: 360,
   anim: 0,
   icon: -1,
@@ -408,15 +408,15 @@ Class.pt.vessel = function(conType, callback){
   var that = this, times = that.index, config = that.config;
   var zIndex = config.zIndex + times, titype = typeof config.title === 'object';
   var ismax = config.maxmin && (config.type === 1 || config.type === 2);
-  var titleHTML = (config.title ? '<div class="layui-layer-title" style="'+ (titype ? config.title[1] : '') +'">'
-    + (titype ? config.title[0] : config.title)
+  var titleHTML = (config.title ? '<div class="layui-layer-title" style="'+ (titype ? config.title[1] : '') +'">' 
+    + (titype ? config.title[0] : config.title) 
   + '</div>' : '');
-
+  
   config.zIndex = zIndex;
   callback([
     //遮罩
     config.shade ? ('<div class="layui-layer-shade" id="layui-layer-shade'+ times +'" times="'+ times +'" style="'+ ('z-index:'+ (zIndex-1) +'; background-color:'+ (config.shade[1]||'#000') +'; opacity:'+ (config.shade[0]||config.shade) +'; filter:alpha(opacity='+ (config.shade[0]*100||config.shade*100) +');') +'"></div>') : '',
-
+    
     //主体
     '<div class="'+ doms[0] + (' layui-layer-'+ready.type[config.type]) + (((config.type == 0 || config.type == 2) && !config.shade) ? ' layui-layer-border' : '') + ' ' + (config.skin||'') +'" id="'+ doms[0] + times +'" type="'+ ready.type[config.type] +'" times="'+ times +'" showtime="'+ config.time +'" conType="'+ (conType ? 'object' : 'string') +'" style="z-index: '+ zIndex +'; width:'+ config.area[0] + ';height:' + config.area[1] + (config.fixed ? '' : ';position:absolute;') +'">'
       + (conType && config.type != 2 ? '' : titleHTML)
@@ -451,22 +451,22 @@ Class.pt.creat = function(){
   ,content = config.content
   ,conType = typeof content === 'object'
   ,body = $('body');
-
+  
   if($('#'+config.id)[0])  return;
 
   if(typeof config.area === 'string'){
     config.area = config.area === 'auto' ? ['', ''] : [config.area, ''];
   }
-
+  
   //anim兼容旧版shift
   if(config.shift){
     config.anim = config.shift;
   }
-
+  
   if(layer.ie == 6){
     config.fixed = false;
   }
-
+  
   switch(config.type){
     case 0:
       config.btn = ('btn' in config) ? config.btn : ready.btn[0];
@@ -491,7 +491,7 @@ Class.pt.creat = function(){
       config.tipsMore || layer.closeAll('tips');
     break;
   }
-
+  
   //建立容器
   that.vessel(conType, function(html, titleHTML, moveElem){
     body.append(html[0]);
@@ -521,12 +521,12 @@ Class.pt.creat = function(){
       config.type == 4 && that.tips();
     });
   }
-
+  
   config.time <= 0 || setTimeout(function(){
     layer.close(that.index)
   }, config.time);
   that.move().callback();
-
+  
   //为兼容jQuery3.0的css动画影响元素尺寸计算
   if(doms.anim[config.anim]){
     that.layero.addClass(doms.anim[config.anim]).data('anim', true);
@@ -551,7 +551,7 @@ Class.pt.auto = function(index){
     elem.height(area[1] - titHeight - btnHeight - 2*(parseFloat(elem.css('padding'))|0));
   }
   switch(config.type){
-    case 2:
+    case 2: 
       setHeight('iframe');
     break;
     default:
@@ -575,12 +575,12 @@ Class.pt.offset = function(){
   var type = typeof config.offset === 'object';
   that.offsetTop = (win.height() - area[1])/2;
   that.offsetLeft = (win.width() - area[0])/2;
-
+  
   if(type){
     that.offsetTop = config.offset[0];
     that.offsetLeft = config.offset[1]||that.offsetLeft;
   } else if(config.offset !== 'auto'){
-
+    
     if(config.offset === 't'){ //上
       that.offsetTop = 0;
     } else if(config.offset === 'r'){ //右
@@ -604,20 +604,20 @@ Class.pt.offset = function(){
     } else {
       that.offsetTop = config.offset;
     }
-
+    
   }
-
+ 
   if(!config.fixed){
-    that.offsetTop = /%$/.test(that.offsetTop) ?
+    that.offsetTop = /%$/.test(that.offsetTop) ? 
       win.height()*parseFloat(that.offsetTop)/100
     : parseFloat(that.offsetTop);
-    that.offsetLeft = /%$/.test(that.offsetLeft) ?
+    that.offsetLeft = /%$/.test(that.offsetLeft) ? 
       win.width()*parseFloat(that.offsetLeft)/100
     : parseFloat(that.offsetLeft);
     that.offsetTop += win.scrollTop();
     that.offsetLeft += win.scrollLeft();
   }
-
+  
   if(layero.attr('minLeft')){
     that.offsetTop = win.height() - (layero.find(doms[1]).outerHeight() || 0);
     that.offsetLeft = layero.css('left');
@@ -637,10 +637,10 @@ Class.pt.tips = function(){
     top: follow.offset().top,
     left: follow.offset().left
   }, tipsG = layero.find('.layui-layer-TipsG');
-
+  
   var guide = config.tips[0];
   config.tips[1] || tipsG.remove();
-
+  
   goal.autoLeft = function(){
     if(goal.left + layArea[0] - win.width() > 0){
       goal.tipLeft = goal.left + goal.width - layArea[0];
@@ -649,16 +649,16 @@ Class.pt.tips = function(){
       goal.tipLeft = goal.left;
     };
   };
-
+  
   //辨别tips的方位
-  goal.where = [function(){ //上
+  goal.where = [function(){ //上        
     goal.autoLeft();
     goal.tipTop = goal.top - layArea[1] - 10;
     tipsG.removeClass('layui-layer-TipsB').addClass('layui-layer-TipsT').css('border-right-color', config.tips[1]);
   }, function(){ //右
     goal.tipLeft = goal.left + goal.width + 10;
     goal.tipTop = goal.top;
-    tipsG.removeClass('layui-layer-TipsL').addClass('layui-layer-TipsR').css('border-bottom-color', config.tips[1]);
+    tipsG.removeClass('layui-layer-TipsL').addClass('layui-layer-TipsR').css('border-bottom-color', config.tips[1]); 
   }, function(){ //下
     goal.autoLeft();
     goal.tipTop = goal.top + goal.height + 10;
@@ -669,7 +669,7 @@ Class.pt.tips = function(){
     tipsG.removeClass('layui-layer-TipsR').addClass('layui-layer-TipsL').css('border-bottom-color', config.tips[1]);
   }];
   goal.where[guide-1]();
-
+  
   /* 8*2为小三角形占据的空间 */
   if(guide === 1){
     goal.top - (win.scrollTop() + layArea[1] + 8*2) < 0 && goal.where[2]();
@@ -682,11 +682,11 @@ Class.pt.tips = function(){
   }
 
   layero.find('.'+doms[5]).css({
-    'background-color': config.tips[1],
+    'background-color': config.tips[1], 
     'padding-right': (config.closeBtn ? '30px' : '')
   });
   layero.css({
-    left: goal.tipLeft - (config.fixed ? win.scrollLeft() : 0),
+    left: goal.tipLeft - (config.fixed ? win.scrollLeft() : 0), 
     top: goal.tipTop  - (config.fixed ? win.scrollTop() : 0)
   });
 }
@@ -700,7 +700,7 @@ Class.pt.move = function(){
   ,moveElem = layero.find(config.move)
   ,resizeElem = layero.find('.layui-layer-resize')
   ,dict = {};
-
+  
   if(config.move){
     moveElem.css('cursor', 'move');
   }
@@ -716,7 +716,7 @@ Class.pt.move = function(){
       ready.moveElem.css('cursor', 'move').show();
     }
   });
-
+  
   resizeElem.on('mousedown', function(e){
     e.preventDefault();
     dict.resizeStart = true;
@@ -727,7 +727,7 @@ Class.pt.move = function(){
     ];
     ready.moveElem.css('cursor', 'se-resize').show();
   });
-
+  
   _DOC.on('mousemove', function(e){
 
     //拖拽移动
@@ -735,35 +735,35 @@ Class.pt.move = function(){
       var X = e.clientX - dict.offset[0]
       ,Y = e.clientY - dict.offset[1]
       ,fixed = layero.css('position') === 'fixed';
-
+      
       e.preventDefault();
-
+      
       dict.stX = fixed ? 0 : win.scrollLeft();
       dict.stY = fixed ? 0 : win.scrollTop();
 
       //控制元素不被拖出窗口外
       if(!config.moveOut){
         var setRig = win.width() - layero.outerWidth() + dict.stX
-        ,setBot = win.height() - layero.outerHeight() + dict.stY;
+        ,setBot = win.height() - layero.outerHeight() + dict.stY;  
         X < dict.stX && (X = dict.stX);
-        X > setRig && (X = setRig);
+        X > setRig && (X = setRig); 
         Y < dict.stY && (Y = dict.stY);
         Y > setBot && (Y = setBot);
       }
-
+      
       layero.css({
         left: X
         ,top: Y
       });
     }
-
+    
     //Resize
     if(config.resize && dict.resizeStart){
       var X = e.clientX - dict.offset[0]
       ,Y = e.clientY - dict.offset[1];
-
+      
       e.preventDefault();
-
+      
       layer.style(that.index, {
         width: dict.area[0] + X
         ,height: dict.area[1] + Y
@@ -781,7 +781,7 @@ Class.pt.move = function(){
       ready.moveElem.hide();
     }
   });
-
+  
   return that;
 };
 
@@ -798,7 +798,7 @@ Class.pt.callback = function(){
     }
   }
   layer.ie == 6 && that.IE6(layero);
-
+  
   //按钮
   layero.find('.'+ doms[6]).children('a').on('click', function(){
     var index = $(this).index();
@@ -815,29 +815,29 @@ Class.pt.callback = function(){
       close === false || layer.close(that.index);
     }
   });
-
+  
   //取消
   function cancel(){
     var close = config.cancel && config.cancel(that.index, layero);
     close === false || layer.close(that.index);
   }
-
+  
   //右上角关闭回调
   layero.find('.'+ doms[7]).on('click', cancel);
-
+  
   //点遮罩关闭
   if(config.shadeClose){
     $('#layui-layer-shade'+ that.index).on('click', function(){
       layer.close(that.index);
     });
-  }
-
+  } 
+  
   //最小化
   layero.find('.layui-layer-min').on('click', function(){
     var min = config.min && config.min(layero);
-    min === false || layer.min(that.index, config);
+    min === false || layer.min(that.index, config); 
   });
-
+  
   //全屏/还原
   layero.find('.layui-layer-max').on('click', function(){
     if($(this).hasClass('layui-layer-maxmin')){
@@ -859,11 +859,11 @@ ready.reselect = function(){
   $.each($('select'), function(index , value){
     var sthis = $(this);
     if(!sthis.parents('.'+doms[0])[0]){
-      (sthis.attr('layer') == 1 && $('.'+doms[0]).length < 1) && sthis.removeAttr('layer').show();
+      (sthis.attr('layer') == 1 && $('.'+doms[0]).length < 1) && sthis.removeAttr('layer').show(); 
     }
     sthis = null;
   });
-};
+}; 
 
 Class.pt.IE6 = function(layero){
   //隐藏select
@@ -879,7 +879,7 @@ Class.pt.IE6 = function(layero){
 //需依赖原型的对外方法
 Class.pt.openLayer = function(){
   var that = this;
-
+  
   //置顶当前窗口
   layer.zIndex = that.config.zIndex;
   layer.setTop = function(layero){
@@ -897,7 +897,7 @@ ready.record = function(layero){
   var area = [
     layero.width(),
     layero.height(),
-    layero.position().top,
+    layero.position().top, 
     layero.position().left + parseFloat(layero.css('margin-left'))
   ];
   layero.find('.layui-layer-max').addClass('layui-layer-maxmin');
@@ -922,7 +922,7 @@ window.layer = layer;
 //获取子iframe的DOM
 layer.getChildFrame = function(selector, index){
   index = index || $('.'+doms[4]).attr('times');
-  return $('#'+ doms[0] + index).find('iframe').contents().find(selector);
+  return $('#'+ doms[0] + index).find('iframe').contents().find(selector);  
 };
 
 //得到当前iframe层的索引,子iframe时使用
@@ -954,24 +954,24 @@ layer.style = function(index, options, limit){
   ,titHeight = layero.find(doms[1]).outerHeight() || 0
   ,btnHeight = layero.find('.'+doms[6]).outerHeight() || 0
   ,minLeft = layero.attr('minLeft');
-
+  
   if(type === ready.type[3] || type === ready.type[4]){
     return;
   }
-
+  
   if(!limit){
     if(parseFloat(options.width) <= 260){
       options.width = 260;
     };
-
+    
     if(parseFloat(options.height) - titHeight - btnHeight <= 64){
       options.height = 64 + titHeight + btnHeight;
     };
   }
-
+  
   layero.css(options);
   btnHeight = layero.find('.'+doms[6]).outerHeight();
-
+  
   if(type === ready.type[2]){
     layero.find('iframe').css({
       height: parseFloat(options.height) - titHeight - btnHeight
@@ -991,16 +991,16 @@ layer.min = function(index, options){
   ,titHeight = layero.find(doms[1]).outerHeight() || 0
   ,left = layero.attr('minLeft') || (181*ready.minIndex)+'px'
   ,position = layero.css('position');
-
+  
   ready.record(layero);
-
+  
   if(ready.minLeft[0]){
     left = ready.minLeft[0];
     ready.minLeft.shift();
   }
-
+  
   layero.attr('position', position);
-
+  
   layer.style(index, {
     width: 180
     ,height: titHeight
@@ -1013,7 +1013,7 @@ layer.min = function(index, options){
   layero.find('.layui-layer-min').hide();
   layero.attr('type') === 'page' && layero.find(doms[4]).hide();
   ready.rescollbar(index);
-
+  
   if(!layero.attr('minLeft')){
     ready.minIndex++;
   }
@@ -1025,9 +1025,9 @@ layer.restore = function(index){
   var layero = $('#'+ doms[0] + index), area = layero.attr('area').split(',');
   var type = layero.attr('type');
   layer.style(index, {
-    width: parseFloat(area[0]),
-    height: parseFloat(area[1]),
-    top: parseFloat(area[2]),
+    width: parseFloat(area[0]), 
+    height: parseFloat(area[1]), 
+    top: parseFloat(area[2]), 
     left: parseFloat(area[3]),
     position: layero.attr('position'),
     overflow: 'visible'
@@ -1090,16 +1090,16 @@ layer.close = function(index){
       layero.remove();
     }
   };
-
+  
   if(layero.data('anim')){
     layero.addClass(closeAnim);
   }
-
+  
   $('#layui-layer-moves, #layui-layer-shade' + index).remove();
   layer.ie == 6 && ready.reselect();
   ready.rescollbar(index);
   typeof ready.end[index] === 'function' && ready.end[index]();
-  delete ready.end[index];
+  delete ready.end[index]; 
   if(layero.attr('minLeft')){
     ready.minIndex--;
     ready.minLeft.push(layero.attr('minLeft'));
@@ -1119,7 +1119,7 @@ layer.closeAll = function(type){
   });
 };
 
-/**
+/** 
 
   拓展模块,layui开始合并在一起
 
@@ -1127,15 +1127,15 @@ layer.closeAll = function(type){
 
 var cache = layer.cache||{}, skin = function(type){
   return (cache.skin ? (' ' + cache.skin + ' ' + cache.skin + '-'+type) : '');
-};
-
+}; 
+ 
 //仿系统prompt
 layer.prompt = function(options, yes){
   var style = '';
   options = options || {};
-
+  
   if(typeof options === 'function') yes = options;
-
+  
   if(options.area){
     var area = options.area;
     style = 'style="width: '+ area[0] +'; height: '+ area[1] + ';"';
@@ -1144,7 +1144,7 @@ layer.prompt = function(options, yes){
   var prompt, content = options.formType == 2 ? '<textarea class="layui-layer-input"' + style +'>' + (options.value||'') +'</textarea>' : function(){
     return '<input type="'+ (options.formType == 1 ? 'password' : 'text') +'" class="layui-layer-input" value="'+ (options.value||'') +'">';
   }();
-
+  
   return layer.open($.extend({
     type: 1
     ,btn: ['&#x786E;&#x5B9A;','&#x53D6;&#x6D88;']
@@ -1220,7 +1220,7 @@ layer.photos = function(options, loop, key){
   var photos = type ? options.photos : {}, data = photos.data || [];
   var start = photos.start || 0;
   dict.imgIndex = (start|0) + 1;
-
+  
   options.img = options.img || 'img';
 
   if(!type){ //页面直接获取
@@ -1237,13 +1237,13 @@ layer.photos = function(options, loop, key){
         });
       })
     };
-
+    
     pushData();
-
+    
     if (data.length === 0) return;
-
+    
     loop || parent.on('click', options.img, function(){
-      var othis = $(this), index = othis.attr('layer-index');
+      var othis = $(this), index = othis.attr('layer-index'); 
       layer.photos($.extend(options, {
         photos: {
           start: index,
@@ -1254,14 +1254,14 @@ layer.photos = function(options, loop, key){
       }), true);
       pushData();
     })
-
+    
     //不直接弹出
     if(!loop) return;
-
+    
   } else if (data.length === 0){
     return layer.msg('&#x6CA1;&#x6709;&#x56FE;&#x7247;');
   }
-
+  
   //上一张
   dict.imgprev = function(key){
     dict.imgIndex--;
@@ -1270,7 +1270,7 @@ layer.photos = function(options, loop, key){
     }
     dict.tabimg(key);
   };
-
+  
   //下一张
   dict.imgnext = function(key,errorMsg){
     dict.imgIndex++;
@@ -1280,7 +1280,7 @@ layer.photos = function(options, loop, key){
     }
     dict.tabimg(key)
   };
-
+  
   //方向键
   dict.keyup = function(event){
     if(!dict.end){
@@ -1295,7 +1295,7 @@ layer.photos = function(options, loop, key){
       }
     }
   }
-
+  
   //切换
   dict.tabimg = function(key){
     if(data.length <= 1) return;
@@ -1303,7 +1303,7 @@ layer.photos = function(options, loop, key){
     layer.close(dict.index);
     layer.photos(options, true, key);
   }
-
+  
   //一些动作
   dict.event = function(){
     dict.bigimg.hover(function(){
@@ -1311,24 +1311,24 @@ layer.photos = function(options, loop, key){
     }, function(){
       dict.imgsee.hide();
     });
-
+    
     dict.bigimg.find('.layui-layer-imgprev').on('click', function(event){
       event.preventDefault();
       dict.imgprev();
-    });
-
-    dict.bigimg.find('.layui-layer-imgnext').on('click', function(event){
+    });  
+    
+    dict.bigimg.find('.layui-layer-imgnext').on('click', function(event){     
       event.preventDefault();
       dict.imgnext();
     });
-
+    
     $(document).on('keyup', dict.keyup);
   };
-
+  
   //图片预加载
-  function loadImage(url, callback, error) {
+  function loadImage(url, callback, error) {   
     var img = new Image();
-    img.src = url;
+    img.src = url; 
     if(img.complete){
       return callback(img);
     }
@@ -1339,9 +1339,9 @@ layer.photos = function(options, loop, key){
     img.onerror = function(e){
       img.onerror = null;
       error(e);
-    };
+    };  
   };
-
+  
   dict.loadi = layer.load(1, {
     shade: 'shade' in options ? false : 0.9,
     scrollbar: false
@@ -1353,7 +1353,7 @@ layer.photos = function(options, loop, key){
       area: function(){
         var imgarea = [img.width, img.height];
         var winarea = [$(window).width() - 100, $(window).height() - 100];
-
+        
         //如果 实际图片的宽或者高比 屏幕大(那么进行缩放)
         if(!options.full && (imgarea[0]>winarea[0]||imgarea[1]>winarea[1])){
           var wh = [imgarea[0]/winarea[0],imgarea[1]/winarea[1]];//取宽度缩放比例、高度缩放比例
@@ -1365,8 +1365,8 @@ layer.photos = function(options, loop, key){
             imgarea[1] = imgarea[1]/wh[1];
           }
         }
-
-        return [imgarea[0]+'px', imgarea[1]+'px'];
+        
+        return [imgarea[0]+'px', imgarea[1]+'px']; 
       }(),
       title: false,
       shade: 0.9,
@@ -1398,8 +1398,8 @@ layer.photos = function(options, loop, key){
   }, function(){
     layer.close(dict.loadi);
     layer.msg('&#x5F53;&#x524D;&#x56FE;&#x7247;&#x5730;&#x5740;&#x5F02;&#x5E38;<br>&#x662F;&#x5426;&#x7EE7;&#x7EED;&#x67E5;&#x770B;&#x4E0B;&#x4E00;&#x5F20;&#xFF1F;', {
-      time: 30000,
-      btn: ['&#x4E0B;&#x4E00;&#x5F20;', '&#x4E0D;&#x770B;&#x4E86;'],
+      time: 30000, 
+      btn: ['&#x4E0B;&#x4E00;&#x5F20;', '&#x4E0D;&#x770B;&#x4E86;'], 
       yes: function(){
         data.length > 1 && dict.imgnext(true,true);
       }

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

@@ -317,6 +317,10 @@ body {
     border:none!important;
 }
 
+.note-editor .note-editing-area .note-editable{
+    display: block !important;
+}
+
 .pjax-loader-bar .progress {
     position: fixed;
     top: 0;