Browse Source

新增菜单类upgrade方法
新增上传存储文件名功能
新增文件分片上传功能
优化上传组件及方法
优化系统配置中冗余的JS代码
优化下拉框显示样式
优化大数据列表时表格列表渲染速度
修复在PHP高版本下Token验证错误的BUG
修复在PHP高版本下会员登录页错误的BUG
修复Token::get默认值不生效的BUG

Karson 4 years ago
parent
commit
ed20e5ac6f
63 changed files with 5294 additions and 947 deletions
  1. 7 3
      README.md
  2. 2 2
      application/admin/command/Crud.php
  3. 2 2
      application/admin/command/Install/fastadmin.sql
  4. 54 90
      application/admin/controller/Ajax.php
  5. 5 3
      application/admin/controller/general/Attachment.php
  6. 16 0
      application/admin/lang/zh-cn.php
  7. 1 6
      application/admin/lang/zh-cn/ajax.php
  8. 1 0
      application/admin/lang/zh-cn/general/attachment.php
  9. 2 0
      application/admin/library/traits/Backend.php
  10. 4 4
      application/admin/view/addon/config.html
  11. 2 2
      application/admin/view/addon/index.html
  12. 2 2
      application/admin/view/category/add.html
  13. 2 2
      application/admin/view/category/edit.html
  14. 2 2
      application/admin/view/general/attachment/add.html
  15. 6 0
      application/admin/view/general/attachment/edit.html
  16. 1 1
      application/admin/view/general/attachment/select.html
  17. 3 3
      application/admin/view/general/config/index.html
  18. 2 2
      application/admin/view/general/profile/index.html
  19. 2 2
      application/admin/view/index/login.html
  20. 2 2
      application/admin/view/user/user/edit.html
  21. 55 88
      application/api/controller/Common.php
  22. 98 80
      application/api/lang/zh-cn.php
  23. 1 6
      application/api/lang/zh-cn/common.php
  24. 7 4
      application/common/controller/Backend.php
  25. 1 1
      application/common/controller/Frontend.php
  26. 17 0
      application/common/exception/UploadException.php
  27. 53 0
      application/common/library/Menu.php
  28. 323 0
      application/common/library/Upload.php
  29. 1 1
      application/common/model/Attachment.php
  30. 21 18
      application/common/model/Config.php
  31. 1 1
      application/config.php
  32. 4 0
      application/extra/upload.php
  33. 18 0
      application/index/lang/zh-cn.php
  34. 1 6
      application/index/lang/zh-cn/ajax.php
  35. 0 1
      application/index/lang/zh-cn/user.php
  36. 14 10
      application/index/view/user/login.html
  37. 3 3
      application/index/view/user/profile.html
  38. 0 1
      bower.json
  39. 3 3
      composer.json
  40. 165 44
      public/api.html
  41. 34 7
      public/assets/css/backend.css
  42. 1 1
      public/assets/css/backend.min.css
  43. 1 0
      public/assets/css/dropzone.min.css
  44. 2 3
      public/assets/css/fastadmin.css
  45. 25 7
      public/assets/css/frontend.css
  46. 1 1
      public/assets/css/frontend.min.css
  47. 2 2
      public/assets/js/backend/addon.js
  48. 9 7
      public/assets/js/backend/general/attachment.js
  49. 1 42
      public/assets/js/backend/general/config.js
  50. 3 3
      public/assets/js/backend/general/profile.js
  51. 3851 0
      public/assets/js/dropzone.js
  52. 1 0
      public/assets/js/dropzone.min.js
  53. 2 2
      public/assets/js/frontend/user.js
  54. 3 8
      public/assets/js/require-backend.js
  55. 198 236
      public/assets/js/require-backend.min.js
  56. 16 4
      public/assets/js/require-form.js
  57. 6 11
      public/assets/js/require-frontend.js
  58. 6 11
      public/assets/js/require-frontend.min.js
  59. 8 8
      public/assets/js/require-table.js
  60. 167 185
      public/assets/js/require-upload.js
  61. 34 9
      public/assets/less/backend.less
  62. 1 2
      public/assets/less/fastadmin/dropdown.less
  63. 18 3
      public/assets/less/frontend.less

+ 7 - 3
README.md

@@ -17,17 +17,17 @@ FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架。
     * 基于`Bootstrap`开发,自适应手机、平板、PC
     * 基于`RequireJS`进行JS模块管理,按需加载
     * 基于`Less`进行样式开发
-    * 基于`Bower`进行前端组件包管理
 * 强大的插件扩展功能,在线安装卸载升级插件
 * 通用的会员模块和API模块
 * 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
 * 二级域名部署支持,同时域名支持绑定到插件
 * 多语言支持,服务端及客户端支持
-* 强大的第三方模块支持([CMS](https://www.fastadmin.net/store/cms.html)、[博客](https://www.fastadmin.net/store/blog.html)、[知识付费问答](https://www.fastadmin.net/store/ask.html)、[在线投票系统](https://www.fastadmin.net/store/vote.html))
+* 支持大文件分片上传、剪切板粘贴上传、拖拽上传,进度条显示,图片上传前压缩
+* 强大的第三方应用模块支持([CMS](https://www.fastadmin.net/store/cms.html)、[博客](https://www.fastadmin.net/store/blog.html)、[知识付费问答](https://www.fastadmin.net/store/ask.html)、[在线投票系统](https://www.fastadmin.net/store/vote.html)、[商城系统](https://www.fastadmin.net/store/shopro.html))
 * 支持CMS、博客、知识付费问答无缝整合[Xunsearch全文搜索](https://www.fastadmin.net/store/xunsearch.html)
 * 第三方小程序支持([预订小程序](https://www.fastadmin.net/store/ball.html)、[问答小程序](https://www.fastadmin.net/store/questions.html)、[活动报名小程序](https://www.fastadmin.net/store/huodong.html)、[商城小程序](https://www.fastadmin.net/store/xshop.html)、[博客小程序](https://www.fastadmin.net/store/blog.html))
 * 整合第三方短信接口(阿里云、腾讯云短信)
-* 无缝整合第三方云存储(七牛、阿里云OSS、又拍云)功能
+* 无缝整合第三方云存储(七牛、阿里云OSS、又拍云)功能
 * 第三方富文本编辑器支持(Summernote、Kindeditor、百度编辑器)
 * 第三方登录(QQ、微信、微博)整合
 * 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
@@ -80,6 +80,10 @@ Nice-validator: https://validator.niceue.com
 
 SelectPage: https://github.com/TerryZ/SelectPage
 
+Layer: https://layer.layui.com
+
+DropzoneJS: https://www.dropzonejs.com
+
 
 ## 版权信息
 

+ 2 - 2
application/admin/command/Crud.php

@@ -1394,12 +1394,12 @@ EOD;
         }
         $multiple = substr($field, -1) == 's' ? ' data-multiple="true"' : ' data-multiple="false"';
         $preview = ' data-preview-id="p-' . $field . '"';
-        $previewcontainer = $preview ? '<ul class="row list-inline plupload-preview" id="p-' . $field . '"></ul>' : '';
+        $previewcontainer = $preview ? '<ul class="row list-inline faupload-preview" id="p-' . $field . '"></ul>' : '';
         return <<<EOD
 <div class="input-group">
                 {$content}
                 <div class="input-group-addon no-border no-padding">
-                    <span><button type="button" id="plupload-{$field}" class="btn btn-danger plupload" data-input-id="c-{$field}"{$uploadfilter}{$multiple}{$preview}><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                    <span><button type="button" id="faupload-{$field}" class="btn btn-danger faupload" data-input-id="c-{$field}"{$uploadfilter}{$multiple}{$preview}><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                     <span><button type="button" id="fachoose-{$field}" class="btn btn-primary fachoose" data-input-id="c-{$field}"{$selectfilter}{$multiple}><i class="fa fa-list"></i> {:__('Choose')}</button></span>
                 </div>
                 <span class="msg-box n-right" for="c-{$field}"></span>

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

@@ -441,8 +441,8 @@ CREATE TABLE `fa_user` (
   `gender` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '性别',
   `birthday` date DEFAULT NULL COMMENT '生日',
   `bio` varchar(100) NOT NULL DEFAULT '' COMMENT '格言',
-  `money` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '余额',
-  `score` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '积分',
+  `money` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '余额',
+  `score` int(10) NOT NULL DEFAULT '0' COMMENT '积分',
   `successions` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '连续登录天数',
   `maxsuccessions` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '最大连续登录天数',
   `prevtime` int(10) DEFAULT NULL COMMENT '上次登录时间',

+ 54 - 90
application/admin/controller/Ajax.php

@@ -3,6 +3,8 @@
 namespace app\admin\controller;
 
 use app\common\controller\Backend;
+use app\common\exception\UploadException;
+use app\common\library\Upload;
 use fast\Random;
 use think\addons\Service;
 use think\Cache;
@@ -54,99 +56,61 @@ class Ajax extends Backend
     public function upload()
     {
         Config::set('default_return_type', 'json');
-        $file = $this->request->file('file');
-        if (empty($file)) {
-            $this->error(__('No file upload or server upload limit exceeded'));
-        }
-
-        //判断是否已经存在附件
-        $sha1 = $file->hash();
-        $extparam = $this->request->post();
-
-        $upload = Config::get('upload');
-
-        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);
-        $fileInfo = $file->getInfo();
-        $suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
-        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
-
-        $mimetypeArr = explode(',', strtolower($upload['mimetype']));
-        $typeArr = explode('/', $fileInfo['type']);
-
-        //禁止上传PHP和HTML文件
-        if (in_array($fileInfo['type'], ['text/x-php', 'text/html']) || in_array($suffix, ['php', 'html', 'htm'])) {
-            $this->error(__('Uploaded file format is limited'));
-        }
-        //验证文件后缀
-        if ($upload['mimetype'] !== '*' &&
-            (
-                !in_array($suffix, $mimetypeArr)
-                || (stripos($typeArr[0] . '/', $upload['mimetype']) !== false && (!in_array($fileInfo['type'], $mimetypeArr) && !in_array($typeArr[0] . '/*', $mimetypeArr)))
-            )
-        ) {
-            $this->error(__('Uploaded file format is limited'));
-        }
-        //验证是否为图片文件
-        $imagewidth = $imageheight = 0;
-        if (in_array($fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($suffix, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
-            $imgInfo = getimagesize($fileInfo['tmp_name']);
-            if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
-                $this->error(__('Uploaded file is not a valid image'));
+        $chunkid = $this->request->post("chunkid");
+        if ($chunkid) {
+            if (!Config::get('upload.chunking')) {
+                $this->error(__('Chunk file disabled'));
+            }
+            $action = $this->request->post("action");
+            $chunkindex = $this->request->post("chunkindex/d");
+            $chunkcount = $this->request->post("chunkcount/d");
+            $filename = $this->request->post("filename");
+            $method = $this->request->method(true);
+            if ($action == 'merge') {
+                $attachment = null;
+                //合并分片文件
+                try {
+                    $upload = new Upload();
+                    $attachment = $upload->merge($chunkid, $chunkcount, $filename);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success(__('Uploaded successful'), '', ['url' => $attachment->url]);
+            } elseif ($method == 'clean') {
+                //删除冗余的分片文件
+                try {
+                    $upload = new Upload();
+                    $upload->clean($chunkid);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success();
+            } else {
+                //上传分片文件
+                //默认普通上传文件
+                $file = $this->request->file('file');
+                try {
+                    $upload = new Upload($file);
+                    $upload->chunk($chunkid, $chunkindex, $chunkcount);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success();
             }
-            $imagewidth = isset($imgInfo[0]) ? $imgInfo[0] : $imagewidth;
-            $imageheight = isset($imgInfo[1]) ? $imgInfo[1] : $imageheight;
-        }
-        $replaceArr = [
-            '{year}'     => date("Y"),
-            '{mon}'      => date("m"),
-            '{day}'      => date("d"),
-            '{hour}'     => date("H"),
-            '{min}'      => date("i"),
-            '{sec}'      => date("s"),
-            '{random}'   => Random::alnum(16),
-            '{random32}' => Random::alnum(32),
-            '{filename}' => $suffix ? substr($fileInfo['name'], 0, strripos($fileInfo['name'], '.')) : $fileInfo['name'],
-            '{suffix}'   => $suffix,
-            '{.suffix}'  => $suffix ? '.' . $suffix : '',
-            '{filemd5}'  => md5_file($fileInfo['tmp_name']),
-        ];
-        $savekey = $upload['savekey'];
-        $savekey = str_replace(array_keys($replaceArr), array_values($replaceArr), $savekey);
-
-        $uploadDir = substr($savekey, 0, strripos($savekey, '/') + 1);
-        $fileName = substr($savekey, strripos($savekey, '/') + 1);
-        //
-        $splInfo = $file->validate(['size' => $size])->move(ROOT_PATH . '/public' . $uploadDir, $fileName);
-        if ($splInfo) {
-            $params = array(
-                'admin_id'    => (int)$this->auth->id,
-                'user_id'     => 0,
-                'filesize'    => $fileInfo['size'],
-                'imagewidth'  => $imagewidth,
-                'imageheight' => $imageheight,
-                'imagetype'   => $suffix,
-                'imageframes' => 0,
-                'mimetype'    => $fileInfo['type'],
-                'url'         => $uploadDir . $splInfo->getSaveName(),
-                'uploadtime'  => time(),
-                'storage'     => 'local',
-                'sha1'        => $sha1,
-                'extparam'    => json_encode($extparam),
-            );
-            $attachment = model("attachment");
-            $attachment->data(array_filter($params));
-            $attachment->save();
-            \think\Hook::listen("upload_after", $attachment);
-            $this->success(__('Upload successful'), null, [
-                'url' => $uploadDir . $splInfo->getSaveName()
-            ]);
         } else {
-            // 上传失败获取错误信息
-            $this->error($file->getError());
+            $attachment = null;
+            //默认普通上传文件
+            $file = $this->request->file('file');
+            try {
+                $upload = new Upload($file);
+                $attachment = $upload->upload();
+            } catch (UploadException $e) {
+                $this->error($e->getMessage());
+            }
+
+            $this->success(__('Uploaded successful'), '', ['url' => $attachment->url]);
         }
+
     }
 
     /**

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

@@ -105,9 +105,11 @@ class Attachment extends Backend
     {
         if ($ids) {
             \think\Hook::add('upload_delete', function ($params) {
-                $attachmentFile = ROOT_PATH . '/public' . $params['url'];
-                if (is_file($attachmentFile)) {
-                    @unlink($attachmentFile);
+                if ($params['storage'] == 'local') {
+                    $attachmentFile = ROOT_PATH . '/public' . $params['url'];
+                    if (is_file($attachmentFile)) {
+                        @unlink($attachmentFile);
+                    }
                 }
             });
             $attachmentlist = $this->model->where('id', 'in', $ids)->select();

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

@@ -143,8 +143,24 @@ return [
     'Please enter your username'                            => '请输入你的用户名',
     'Please enter your password'                            => '请输入你的密码',
     'Please login first'                                    => '请登录后操作',
+    'Uploaded successful'                                   => '上传成功',
     'You can upload up to %d file%s'                        => '你最多还可以上传%d个文件',
     'You can choose up to %d file%s'                        => '你最多还可以选择%d个文件',
+    'Chunk file write error'                                => '分片写入失败',
+    'Chunk file info error'                                 => '分片文件错误',
+    'Chunk file merge error'                                => '分片合并错误',
+    'Chunk file disabled'                                   => '未开启分片上传功能',
+    'Cancel upload'                                         => '取消上传',
+    'Upload canceled'                                       => '上传已取消',
+    'No file upload or server upload limit exceeded'        => '未上传文件或超出服务器上传限制',
+    'Uploaded file format is limited'                       => '上传文件格式受限制',
+    'Uploaded file is not a valid image'                    => '上传文件不是有效的图片文件',
+    'Are you sure you want to cancel this upload?'          => '确定取消上传?',
+    'Remove file'                                           => '移除文件',
+    'You can only upload a maximum of %s files'             => '你最多允许上传 %s 个文件',
+    'You can\'t upload files of this type'                  => '不允许上传的文件类型',
+    'Server responded with %s code'                         => '服务端响应(Code:%s)',
+    'File is too big (%sMiB), Max filesize: %sMiB'          => '当前上传(%sM),最大允许上传文件大小:%sM',
     'An unexpected error occurred'                          => '发生了一个意外错误,程序猿正在紧急处理中',
     'This page will be re-directed in %s seconds'           => '页面将在 %s 秒后自动跳转',
     //菜单

+ 1 - 6
application/admin/lang/zh-cn/ajax.php

@@ -1,8 +1,3 @@
 <?php
 
-return [
-    'No file upload or server upload limit exceeded' => '未上传文件或超出服务器上传限制',
-    'Uploaded file format is limited'                => '上传文件格式受限制',
-    'Uploaded file is not a valid image'             => '上传文件不是有效的图片文件',
-    'Upload successful'                              => '上传成功',
-];
+return [];

+ 1 - 0
application/admin/lang/zh-cn/general/attachment.php

@@ -10,6 +10,7 @@ return [
     'Imagetype'          => '图片类型',
     'Imageframes'        => '图片帧数',
     'Preview'            => '预览',
+    'Filename'           => '文件名',
     'Filesize'           => '文件大小',
     'Mimetype'           => 'Mime类型',
     'Extparam'           => '透传数据',

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

@@ -200,6 +200,7 @@ trait Backend
      */
     public function del($ids = "")
     {
+        $ids = $this->request->post("ids");
         if ($ids) {
             $pk = $this->model->getPk();
             $adminIds = $this->getDataLimitAdminIds();
@@ -307,6 +308,7 @@ trait Backend
     public function multi($ids = "")
     {
         $ids = $ids ? $ids : $this->request->param("ids");
+        $ids = $this->request->param("ids");
         if ($ids) {
             if ($this->request->has('params')) {
                 parse_str($this->request->post("params"), $values);

+ 4 - 4
application/admin/view/addon/config.html

@@ -70,16 +70,16 @@
                         {case value="images"}
                         <div class="form-inline">
                             <input id="c-{$item.name}" class="form-control" size="35" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
-                            <span><button type="button" id="plupload-{$item.name}" class="btn btn-danger plupload" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}" data-preview-id="p-{$item.name}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                            <span><button type="button" id="faupload-{$item.name}" class="btn btn-danger faupload" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}" data-preview-id="p-{$item.name}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                             <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
-                            <ul class="row list-inline plupload-preview" id="p-{$item.name}"></ul>
+                            <ul class="row list-inline faupload-preview" id="p-{$item.name}"></ul>
                         </div>
                         {/case}
                         {case value="file" break="0"}{/case}
                         {case value="files"}
                         <div class="form-inline">
                             <input id="c-{$item.name}" class="form-control" size="35" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
-                            <span><button type="button" id="plupload-{$item.name}" class="btn btn-danger plupload" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                            <span><button type="button" id="faupload-{$item.name}" class="btn btn-danger faupload" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                             <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
                         </div>
                         {/case}
@@ -105,4 +105,4 @@
             <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
         </div>
     </div>
-</form>
+</form>

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

@@ -77,7 +77,7 @@
                 <div class="widget-body no-padding">
                     <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>
+                        <button type="button" id="faupload-addon" class="btn btn-danger faupload" data-url="addon/local" data-mimetype="application/zip" data-multiple="false"><i class="fa fa-upload"></i>
                             {:__('Offline install')}
                         </button>
                         {if $Think.config.fastadmin.api_url}
@@ -293,4 +293,4 @@
         <% } %>
     </div>
 </script>
-<!--@formatter:on-->
+<!--@formatter:on-->

+ 2 - 2
application/admin/view/category/add.html

@@ -58,12 +58,12 @@
             <div class="input-group">
                 <input id="c-image" class="form-control" size="35" name="row[image]" type="text" value="">
                 <div class="input-group-addon no-border no-padding">
-                    <span><button type="button" id="plupload-image" class="btn btn-danger plupload" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                    <span><button type="button" id="faupload-image" class="btn btn-danger faupload" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                     <span><button type="button" id="fachoose-image" class="btn btn-primary fachoose" data-input-id="c-image" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
                 </div>
                 <span class="msg-box n-right"></span>
             </div>
-            <ul class="row list-inline plupload-preview" id="p-image"></ul>
+            <ul class="row list-inline faupload-preview" id="p-image"></ul>
         </div>
     </div>
     <div class="form-group">

+ 2 - 2
application/admin/view/category/edit.html

@@ -54,12 +54,12 @@
             <div class="input-group">
                 <input id="c-image" class="form-control" size="35" name="row[image]" type="text" value="{$row.image}">
                 <div class="input-group-addon no-border no-padding">
-                    <span><button type="button" id="plupload-image" class="btn btn-danger plupload" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                    <span><button type="button" id="faupload-image" class="btn btn-danger faupload" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                     <span><button type="button" id="fachoose-image" class="btn btn-primary fachoose" data-input-id="c-image" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
                 </div>
                 <span class="msg-box n-right"></span>
             </div>
-            <ul class="row list-inline plupload-preview" id="p-image"></ul>
+            <ul class="row list-inline faupload-preview" id="p-image"></ul>
         </div>
     </div>
     <div class="form-group">

+ 2 - 2
application/admin/view/general/attachment/add.html

@@ -10,7 +10,7 @@
     <div class="form-group">
         <label for="c-third" class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button id="plupload-third" class="btn btn-danger plupload" data-multiple="true" data-input-id="c-third" ><i class="fa fa-upload"></i> {:__("Upload to third")}</button>
+            <button type="button" id="faupload-third" class="btn btn-danger faupload" data-multiple="true" data-input-id="c-third" ><i class="fa fa-upload"></i> {:__("Upload to third")}</button>
         </div>
     </div>
     {/if}
@@ -25,7 +25,7 @@
     <div class="form-group">
         <label for="c-local" class="control-label col-xs-12 col-sm-2"></label>
         <div class="col-xs-12 col-sm-8">
-            <button id="plupload-local" class="btn btn-primary plupload" data-input-id="c-local" data-url="{:url('ajax/upload')}"><i class="fa fa-upload"></i> {:__("Upload to local")}</button>
+            <button type="button" id="faupload-local" class="btn btn-primary faupload" data-input-id="c-local" data-url="{:url('ajax/upload')}"><i class="fa fa-upload"></i> {:__("Upload to local")}</button>
         </div>
     </div>
 

+ 6 - 0
application/admin/view/general/attachment/edit.html

@@ -31,6 +31,12 @@
         </div>
     </div>
     <div class="form-group">
+        <label for="c-filename" class="control-label col-xs-12 col-sm-2">{:__('Filename')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" name="row[filename]" value="{$row.filename|htmlentities}"  id="c-filename" class="form-control" />
+        </div>
+    </div>
+    <div class="form-group">
         <label for="c-filesize" class="control-label col-xs-12 col-sm-2">{:__('Filesize')}:</label>
         <div class="col-xs-12 col-sm-8">
             <input type="number" name="row[filesize]" value="{$row.filesize}"  id="c-filesize" class="form-control" />

+ 1 - 1
application/admin/view/general/attachment/select.html

@@ -17,7 +17,7 @@
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
                         {:build_toolbar('refresh')}
-                        <span><button type="button" id="plupload-image" class="btn btn-success plupload" data-mimetype="{$Think.get.mimetype|default=''}" data-multiple="true"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                        <span><button type="button" id="faupload-image" class="btn btn-success faupload" data-mimetype="{$Think.get.mimetype|default=''}" data-multiple="true"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                         {if request()->get('multiple') == 'true'}
                         <a class="btn btn-danger btn-choose-multi"><i class="fa fa-check"></i> {:__('Choose')}</a>
                         {/if}

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

@@ -111,17 +111,17 @@
                                             {case value="images"}
                                             <div class="form-inline">
                                                 <input id="c-{$item.name}" class="form-control" size="50" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
-                                                <span><button type="button" id="plupload-{$item.name}" class="btn btn-danger plupload" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}" data-preview-id="p-{$item.name}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                <span><button type="button" id="faupload-{$item.name}" class="btn btn-danger faupload" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}" data-preview-id="p-{$item.name}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                                                 <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
                                                 <span class="msg-box n-right" for="c-{$item.name}"></span>
-                                                <ul class="row list-inline plupload-preview" id="p-{$item.name}"></ul>
+                                                <ul class="row list-inline faupload-preview" id="p-{$item.name}"></ul>
                                             </div>
                                             {/case}
                                             {case value="file" break="0"}{/case}
                                             {case value="files"}
                                             <div class="form-inline">
                                                 <input id="c-{$item.name}" class="form-control" size="50" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
-                                                <span><button type="button" id="plupload-{$item.name}" class="btn btn-danger plupload" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                <span><button type="button" id="faupload-{$item.name}" class="btn btn-danger faupload" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                                                 <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
                                                 <span class="msg-box n-right" for="c-{$item.name}"></span>
                                             </div>

+ 2 - 2
application/admin/view/general/profile/index.html

@@ -51,9 +51,9 @@
                     <div class="box-body box-profile">
 
                         <div class="profile-avatar-container">
-                            <img class="profile-user-img img-responsive img-circle plupload" src="{$admin.avatar|cdnurl|htmlentities}" alt="">
+                            <img class="profile-user-img img-responsive img-circle faupload" src="{$admin.avatar|cdnurl|htmlentities}" alt="">
                             <div class="profile-avatar-text img-circle">{:__('Click to edit')}</div>
-                            <button id="plupload-avatar" class="plupload" data-input-id="c-avatar"><i class="fa fa-upload"></i> {:__('Upload')}</button>
+                            <button id="faupload-avatar" class="faupload" data-input-id="c-avatar"><i class="fa fa-upload"></i> {:__('Upload')}</button>
                         </div>
 
                         <h3 class="profile-username text-center">{$admin.username|htmlentities}</h3>

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

@@ -85,7 +85,7 @@
                                     </span>
                                 </div>
                                 {/if}
-                                <div class="form-group">
+                                <div class="form-group checkbox">
                                     <label class="inline" for="keeplogin">
                                         <input type="checkbox" name="keeplogin" id="keeplogin" value="1" />
                                         {:__('Keep login')}
@@ -102,4 +102,4 @@
         </div>
         {include file="common/script" /}
     </body>
-</html>
+</html>

+ 2 - 2
application/admin/view/user/user/edit.html

@@ -43,12 +43,12 @@
             <div class="input-group">
                 <input id="c-avatar" data-rule="" class="form-control" size="50" name="row[avatar]" type="text" value="{$row.avatar}">
                 <div class="input-group-addon no-border no-padding">
-                    <span><button type="button" id="plupload-avatar" class="btn btn-danger plupload" data-input-id="c-avatar" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-avatar"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                    <span><button type="button" id="faupload-avatar" class="btn btn-danger faupload" data-input-id="c-avatar" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-avatar"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
                     <span><button type="button" id="fachoose-avatar" class="btn btn-primary fachoose" data-input-id="c-avatar" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
                 </div>
                 <span class="msg-box n-right" for="c-avatar"></span>
             </div>
-            <ul class="row list-inline plupload-preview" id="p-avatar"></ul>
+            <ul class="row list-inline faupload-preview" id="p-avatar"></ul>
         </div>
     </div>
     <div class="form-group">

+ 55 - 88
application/api/controller/Common.php

@@ -3,6 +3,8 @@
 namespace app\api\controller;
 
 use app\common\controller\Api;
+use app\common\exception\UploadException;
+use app\common\library\Upload;
 use app\common\model\Area;
 use app\common\model\Version;
 use fast\Random;
@@ -47,96 +49,61 @@ class Common extends Api
      */
     public function upload()
     {
-        $file = $this->request->file('file');
-        if (empty($file)) {
-            $this->error(__('No file upload or server upload limit exceeded'));
-        }
-
-        //判断是否已经存在附件
-        $sha1 = $file->hash();
-
-        $upload = Config::get('upload');
-
-        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);
-        $fileInfo = $file->getInfo();
-        $suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
-        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
-
-        $mimetypeArr = explode(',', strtolower($upload['mimetype']));
-        $typeArr = explode('/', $fileInfo['type']);
-
-        //禁止上传PHP和HTML文件
-        if (in_array($fileInfo['type'], ['text/x-php', 'text/html']) || in_array($suffix, ['php', 'html', 'htm'])) {
-            $this->error(__('Uploaded file format is limited'));
-        }
-        //验证文件后缀
-        if ($upload['mimetype'] !== '*' &&
-            (
-                !in_array($suffix, $mimetypeArr)
-                || (stripos($typeArr[0] . '/', $upload['mimetype']) !== false && (!in_array($fileInfo['type'], $mimetypeArr) && !in_array($typeArr[0] . '/*', $mimetypeArr)))
-            )
-        ) {
-            $this->error(__('Uploaded file format is limited'));
-        }
-        //验证是否为图片文件
-        $imagewidth = $imageheight = 0;
-        if (in_array($fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($suffix, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
-            $imgInfo = getimagesize($fileInfo['tmp_name']);
-            if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
-                $this->error(__('Uploaded file is not a valid image'));
+        Config::set('default_return_type', 'json');
+        $chunkid = $this->request->post("chunkid");
+        if ($chunkid) {
+            if (!Config::get('upload.chunking')) {
+                $this->error(__('Chunk file disabled'));
+            }
+            $action = $this->request->post("action");
+            $chunkindex = $this->request->post("chunkindex/d");
+            $chunkcount = $this->request->post("chunkcount/d");
+            $filename = $this->request->post("filename");
+            $method = $this->request->method(true);
+            if ($action == 'merge') {
+                $attachment = null;
+                //合并分片文件
+                try {
+                    $upload = new Upload();
+                    $attachment = $upload->merge($chunkid, $chunkcount, $filename);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success(__('Uploaded successful'), '', ['url' => $attachment->url]);
+            } elseif ($method == 'clean') {
+                //删除冗余的分片文件
+                try {
+                    $upload = new Upload();
+                    $upload->clean($chunkid);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success();
+            } else {
+                //上传分片文件
+                //默认普通上传文件
+                $file = $this->request->file('file');
+                try {
+                    $upload = new Upload($file);
+                    $upload->chunk($chunkid, $chunkindex, $chunkcount);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success();
             }
-            $imagewidth = isset($imgInfo[0]) ? $imgInfo[0] : $imagewidth;
-            $imageheight = isset($imgInfo[1]) ? $imgInfo[1] : $imageheight;
-        }
-        $replaceArr = [
-            '{year}'     => date("Y"),
-            '{mon}'      => date("m"),
-            '{day}'      => date("d"),
-            '{hour}'     => date("H"),
-            '{min}'      => date("i"),
-            '{sec}'      => date("s"),
-            '{random}'   => Random::alnum(16),
-            '{random32}' => Random::alnum(32),
-            '{filename}' => $suffix ? substr($fileInfo['name'], 0, strripos($fileInfo['name'], '.')) : $fileInfo['name'],
-            '{suffix}'   => $suffix,
-            '{.suffix}'  => $suffix ? '.' . $suffix : '',
-            '{filemd5}'  => md5_file($fileInfo['tmp_name']),
-        ];
-        $savekey = $upload['savekey'];
-        $savekey = str_replace(array_keys($replaceArr), array_values($replaceArr), $savekey);
-
-        $uploadDir = substr($savekey, 0, strripos($savekey, '/') + 1);
-        $fileName = substr($savekey, strripos($savekey, '/') + 1);
-        //
-        $splInfo = $file->validate(['size' => $size])->move(ROOT_PATH . '/public' . $uploadDir, $fileName);
-        if ($splInfo) {
-            $params = array(
-                'admin_id'    => 0,
-                'user_id'     => (int)$this->auth->id,
-                'filesize'    => $fileInfo['size'],
-                'imagewidth'  => $imagewidth,
-                'imageheight' => $imageheight,
-                'imagetype'   => $suffix,
-                'imageframes' => 0,
-                'mimetype'    => $fileInfo['type'],
-                'url'         => $uploadDir . $splInfo->getSaveName(),
-                'uploadtime'  => time(),
-                'storage'     => 'local',
-                'sha1'        => $sha1,
-            );
-            $attachment = model("attachment");
-            $attachment->data(array_filter($params));
-            $attachment->save();
-            \think\Hook::listen("upload_after", $attachment);
-            $this->success(__('Upload successful'), [
-                'url' => $uploadDir . $splInfo->getSaveName()
-            ]);
         } else {
-            // 上传失败获取错误信息
-            $this->error($file->getError());
+            $attachment = null;
+            //默认普通上传文件
+            $file = $this->request->file('file');
+            try {
+                $upload = new Upload($file);
+                $attachment = $upload->upload();
+            } catch (UploadException $e) {
+                $this->error($e->getMessage());
+            }
+
+            $this->success(__('Uploaded successful'), '', ['url' => $attachment->url]);
         }
+
     }
 }

+ 98 - 80
application/api/lang/zh-cn.php

@@ -1,84 +1,102 @@
 <?php
 
 return [
-    'Keep login'                                  => '保持会话',
-    'Username'                                    => '用户名',
-    'User id'                                     => '会员ID',
-    'Nickname'                                    => '昵称',
-    'Password'                                    => '密码',
-    'Sign up'                                     => '注 册',
-    'Sign in'                                     => '登 录',
-    'Sign out'                                    => '注 销',
-    'Guest'                                       => '游客',
-    'Welcome'                                     => '%s,你好!',
-    'Add'                                         => '添加',
-    'Edit'                                        => '编辑',
-    'Delete'                                      => '删除',
-    'Move'                                        => '移动',
-    'Name'                                        => '名称',
-    'Status'                                      => '状态',
-    'Weigh'                                       => '权重',
-    'Operate'                                     => '操作',
-    'Warning'                                     => '温馨提示',
-    'Default'                                     => '默认',
-    'Article'                                     => '文章',
-    'Page'                                        => '单页',
-    'OK'                                          => '确定',
-    'Cancel'                                      => '取消',
-    'Loading'                                     => '加载中',
-    'More'                                        => '更多',
-    'Normal'                                      => '正常',
-    'Hidden'                                      => '隐藏',
-    'Submit'                                      => '提交',
-    'Reset'                                       => '重置',
-    'Execute'                                     => '执行',
-    'Close'                                       => '关闭',
-    'Search'                                      => '搜索',
-    'Refresh'                                     => '刷新',
-    'First'                                       => '首页',
-    'Previous'                                    => '上一页',
-    'Next'                                        => '下一页',
-    'Last'                                        => '末页',
-    'None'                                        => '无',
-    'Home'                                        => '主页',
-    'Online'                                      => '在线',
-    'Logout'                                      => '注销',
-    'Profile'                                     => '个人资料',
-    'Index'                                       => '首页',
-    'Hot'                                         => '热门',
-    'Recommend'                                   => '推荐',
-    'Dashboard'                                   => '控制台',
-    'Code'                                        => '编号',
-    'Message'                                     => '内容',
-    'Line'                                        => '行号',
-    'File'                                        => '文件',
-    'Menu'                                        => '菜单',
-    'Type'                                        => '类型',
-    'Title'                                       => '标题',
-    'Content'                                     => '内容',
-    'Append'                                      => '追加',
-    'Memo'                                        => '备注',
-    'Parent'                                      => '父级',
-    'Params'                                      => '参数',
-    'Permission'                                  => '权限',
-    'Advance search'                              => '高级搜索',
-    'Check all'                                   => '选中全部',
-    'Expand all'                                  => '展开全部',
-    'Begin time'                                  => '开始时间',
-    'End time'                                    => '结束时间',
-    'Create time'                                 => '创建时间',
-    'Flag'                                        => '标志',
-    'Please login first'                          => '请登录后操作',
-    'Redirect now'                                => '立即跳转',
-    'Operation completed'                         => '操作成功!',
-    'Operation failed'                            => '操作失败!',
-    'Unknown data format'                         => '未知的数据格式!',
-    'Network error'                               => '网络错误!',
-    'Advanced search'                             => '高级搜索',
-    'Invalid parameters'                          => '未知参数',
-    'No results were found'                       => '记录未找到',
-    'Parameter %s can not be empty'               => '参数%s不能为空',
-    'You have no permission'                      => '你没有权限访问',
-    'An unexpected error occurred'                => '发生了一个意外错误,程序猿正在紧急处理中',
-    'This page will be re-directed in %s seconds' => '页面将在 %s 秒后自动跳转',
+    'Keep login'                                     => '保持会话',
+    'Username'                                       => '用户名',
+    'User id'                                        => '会员ID',
+    'Nickname'                                       => '昵称',
+    'Password'                                       => '密码',
+    'Sign up'                                        => '注 册',
+    'Sign in'                                        => '登 录',
+    'Sign out'                                       => '注 销',
+    'Guest'                                          => '游客',
+    'Welcome'                                        => '%s,你好!',
+    'Add'                                            => '添加',
+    'Edit'                                           => '编辑',
+    'Delete'                                         => '删除',
+    'Move'                                           => '移动',
+    'Name'                                           => '名称',
+    'Status'                                         => '状态',
+    'Weigh'                                          => '权重',
+    'Operate'                                        => '操作',
+    'Warning'                                        => '温馨提示',
+    'Default'                                        => '默认',
+    'Article'                                        => '文章',
+    'Page'                                           => '单页',
+    'OK'                                             => '确定',
+    'Cancel'                                         => '取消',
+    'Loading'                                        => '加载中',
+    'More'                                           => '更多',
+    'Normal'                                         => '正常',
+    'Hidden'                                         => '隐藏',
+    'Submit'                                         => '提交',
+    'Reset'                                          => '重置',
+    'Execute'                                        => '执行',
+    'Close'                                          => '关闭',
+    'Search'                                         => '搜索',
+    'Refresh'                                        => '刷新',
+    'First'                                          => '首页',
+    'Previous'                                       => '上一页',
+    'Next'                                           => '下一页',
+    'Last'                                           => '末页',
+    'None'                                           => '无',
+    'Home'                                           => '主页',
+    'Online'                                         => '在线',
+    'Logout'                                         => '注销',
+    'Profile'                                        => '个人资料',
+    'Index'                                          => '首页',
+    'Hot'                                            => '热门',
+    'Recommend'                                      => '推荐',
+    'Dashboard'                                      => '控制台',
+    'Code'                                           => '编号',
+    'Message'                                        => '内容',
+    'Line'                                           => '行号',
+    'File'                                           => '文件',
+    'Menu'                                           => '菜单',
+    'Type'                                           => '类型',
+    'Title'                                          => '标题',
+    'Content'                                        => '内容',
+    'Append'                                         => '追加',
+    'Memo'                                           => '备注',
+    'Parent'                                         => '父级',
+    'Params'                                         => '参数',
+    'Permission'                                     => '权限',
+    'Advance search'                                 => '高级搜索',
+    'Check all'                                      => '选中全部',
+    'Expand all'                                     => '展开全部',
+    'Begin time'                                     => '开始时间',
+    'End time'                                       => '结束时间',
+    'Create time'                                    => '创建时间',
+    'Flag'                                           => '标志',
+    'Please login first'                             => '请登录后操作',
+    'Uploaded successful'                            => '上传成功',
+    'You can upload up to %d file%s'                 => '你最多还可以上传%d个文件',
+    'You can choose up to %d file%s'                 => '你最多还可以选择%d个文件',
+    'Chunk file write error'                         => '分片写入失败',
+    'Chunk file info error'                          => '分片文件错误',
+    'Chunk file merge error'                         => '分片合并错误',
+    'Chunk file disabled'                            => '未开启分片上传功能',
+    'Cancel upload'                                  => '取消上传',
+    'Upload canceled'                                => '上传已取消',
+    'No file upload or server upload limit exceeded' => '未上传文件或超出服务器上传限制',
+    'Uploaded file format is limited'                => '上传文件格式受限制',
+    'Uploaded file is not a valid image'             => '上传文件不是有效的图片文件',
+    'Are you sure you want to cancel this upload?'   => '确定取消上传?',
+    'Remove file'                                    => '移除文件',
+    'You can only upload a maximum of %s files'      => '你最多允许上传 %s 个文件',
+    'You can\'t upload files of this type'           => '不允许上传的文件类型',
+    'Server responded with %s code'                  => '服务端响应(Code:%s)',
+    'File is too big (%sMiB), Max filesize: %sMiB'   => '当前上传(%sM),最大允许上传文件大小:%sM',
+    'Redirect now'                                   => '立即跳转',
+    'Operation completed'                            => '操作成功!',
+    'Operation failed'                               => '操作失败!',
+    'Unknown data format'                            => '未知的数据格式!',
+    'Network error'                                  => '网络错误!',
+    'Advanced search'                                => '高级搜索',
+    'Invalid parameters'                             => '未知参数',
+    'No results were found'                          => '记录未找到',
+    'Parameter %s can not be empty'                  => '参数%s不能为空',
+    'You have no permission'                         => '你没有权限访问',
+    'An unexpected error occurred'                   => '发生了一个意外错误,程序猿正在紧急处理中',
+    'This page will be re-directed in %s seconds'    => '页面将在 %s 秒后自动跳转',
 ];

+ 1 - 6
application/api/lang/zh-cn/common.php

@@ -1,8 +1,3 @@
 <?php
 
-return [
-    'No file upload or server upload limit exceeded' => '未上传文件或超出服务器上传限制',
-    'Uploaded file format is limited'                => '上传文件格式受限制',
-    'Uploaded file is not a valid image'             => '上传文件不是有效的图片文件',
-    'Upload successful'                              => '上传成功',
-];
+return [];

+ 7 - 4
application/common/controller/Backend.php

@@ -228,7 +228,7 @@ class Backend extends Controller
      */
     protected function loadlang($name)
     {
-        $name =  Loader::parseName($name);
+        $name = Loader::parseName($name);
         Lang::load(APP_PATH . $this->request->module() . '/lang/' . $this->request->langset() . '/' . str_replace('.', '/', $name) . '.php');
     }
 
@@ -456,8 +456,11 @@ class Backend extends Controller
             $where = function ($query) use ($word, $andor, $field, $searchfield, $custom) {
                 $logic = $andor == 'AND' ? '&' : '|';
                 $searchfield = is_array($searchfield) ? implode($logic, $searchfield) : $searchfield;
-                foreach ($word as $k => $v) {
-                    $query->where(str_replace(',', $logic, $searchfield), "like", "%{$v}%");
+                $word = array_filter($word);
+                if ($word) {
+                    foreach ($word as $k => $v) {
+                        $query->where(str_replace(',', $logic, $searchfield), "like", "%{$v}%");
+                    }
                 }
                 if ($custom && is_array($custom)) {
                     foreach ($custom as $k => $v) {
@@ -517,7 +520,7 @@ class Backend extends Controller
         $token = $this->request->post('__token__');
 
         //验证Token
-        if (!Validate::is($token, "token", ['__token__' => $token])) {
+        if (!Validate::make()->check(['__token__' => $token], ['__token__' => 'require|token'])) {
             $this->error(__('Token verification error'), '', ['__token__' => $this->request->token()]);
         }
 

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

@@ -145,7 +145,7 @@ class Frontend extends Controller
         $token = $this->request->post('__token__');
 
         //验证Token
-        if (!Validate::is($token, "token", ['__token__' => $token])) {
+        if (!Validate::make()->check(['__token__' => $token], ['__token__' => 'require|token'])) {
             $this->error(__('Token verification error'), '', ['__token__' => $this->request->token()]);
         }
 

+ 17 - 0
application/common/exception/UploadException.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace app\common\exception;
+
+use think\Exception;
+use Throwable;
+
+class UploadException extends Exception
+{
+    public function __construct($message = "", $code = 0, $data = [])
+    {
+        $this->message = $message;
+        $this->code = $code;
+        $this->data = $data;
+    }
+
+}

+ 53 - 0
application/common/library/Menu.php

@@ -90,6 +90,21 @@ class Menu
     }
 
     /**
+     * 升级菜单
+     * @param string $name 插件名称
+     * @param array  $menu 新菜单
+     * @return bool
+     */
+    public static function upgrade($name, $menu)
+    {
+        $old = AuthRule::where('name', 'like', "{$name}%")->select();
+        $old = collection($old)->toArray();
+        $old = array_column($old, null, 'name');
+        self::menuUpdate($menu, $old);
+        return true;
+    }
+
+    /**
      * 导出指定名称的菜单规则
      * @param string $name
      * @return array
@@ -110,6 +125,44 @@ class Menu
     }
 
     /**
+     * 菜单升级
+     * @param array $newMenu
+     * @param array $oldMenu
+     * @param int   $parent
+     * @throws Exception
+     */
+    private static function menuUpdate($newMenu, $oldMenu, $parent = 0)
+    {
+        if (!is_numeric($parent)) {
+            $parentRule = AuthRule::getByName($parent);
+            $pid = $parentRule ? $parentRule['id'] : 0;
+        } else {
+            $pid = $parent;
+        }
+        $allow = array_flip(['file', 'name', 'title', 'icon', 'condition', 'remark', 'ismenu', 'weigh']);
+        foreach ($newMenu as $k => $v) {
+            $hasChild = isset($v['sublist']) && $v['sublist'] ? true : false;
+            $data = array_intersect_key($v, $allow);
+            $data['ismenu'] = isset($data['ismenu']) ? $data['ismenu'] : ($hasChild ? 1 : 0);
+            $data['icon'] = isset($data['icon']) ? $data['icon'] : ($hasChild ? 'fa fa-list' : 'fa fa-circle-o');
+            $data['pid'] = $pid;
+            $data['status'] = 'normal';
+            try {
+                if (!isset($oldMenu[$data['name']])) {
+                    $menu = AuthRule::create($data);
+                } else {
+                    $menu = $oldMenu[$data['name']];
+                }
+                if ($hasChild) {
+                    self::menuUpdate($v['sublist'], $oldMenu, $menu['id']);
+                }
+            } catch (PDOException $e) {
+                throw new Exception($e->getMessage());
+            }
+        }
+    }
+
+    /**
      * 根据名称获取规则IDS
      * @param string $name
      * @return array

+ 323 - 0
application/common/library/Upload.php

@@ -0,0 +1,323 @@
+<?php
+
+namespace app\common\library;
+
+use app\common\exception\UploadException;
+use app\common\model\Attachment;
+use fast\Random;
+use FilesystemIterator;
+use think\Config;
+use think\File;
+use think\Hook;
+
+/**
+ * 文件上传类
+ */
+class Upload
+{
+
+    /**
+     * 验证码有效时长
+     * @var int
+     */
+    protected static $expire = 120;
+
+    /**
+     * 最大允许检测的次数
+     * @var int
+     */
+    protected static $maxCheckNums = 10;
+
+    protected $chunkDir = null;
+
+    protected $config = [];
+
+    protected $error = '';
+
+    /**
+     * @var \think\File
+     */
+    protected $file = null;
+    protected $fileInfo = null;
+
+    public function __construct($file = null)
+    {
+        $this->config = Config::get('upload');
+        $this->chunkDir = RUNTIME_PATH . 'chunks';
+        if ($file) {
+            $this->setFile($file);
+        }
+    }
+
+    public function setChunkDir($dir)
+    {
+        $this->chunkDir = $dir;
+    }
+
+    public function getFile()
+    {
+        return $this->file;
+    }
+
+    public function setFile($file)
+    {
+        if (empty($file)) {
+            throw new UploadException(__('No file upload or server upload limit exceeded'));
+        }
+
+        $fileInfo = $file->getInfo();
+        $suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
+        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
+        $fileInfo['suffix'] = $suffix;
+        $fileInfo['imagewidth'] = 0;
+        $fileInfo['imageheight'] = 0;
+
+        $this->file = $file;
+        $this->fileInfo = $fileInfo;
+    }
+
+    protected function checkExecutable()
+    {
+        //禁止上传PHP和HTML文件
+        if (in_array($this->fileInfo['type'], ['text/x-php', 'text/html']) || in_array($this->fileInfo['suffix'], ['php', 'html', 'htm'])) {
+            throw new UploadException(__('Uploaded file format is limited'));
+        }
+        return true;
+    }
+
+    protected function checkMimetype()
+    {
+        $mimetypeArr = explode(',', strtolower($this->config['mimetype']));
+        $typeArr = explode('/', $this->fileInfo['type']);
+        //验证文件后缀
+        if ($this->config['mimetype'] !== '*' &&
+            (!in_array($this->fileInfo['suffix'], $mimetypeArr) || (stripos($typeArr[0] . '/', $this->config['mimetype']) !== false && (!in_array($this->fileInfo['type'], $mimetypeArr) && !in_array($typeArr[0] . '/*', $mimetypeArr))))
+        ) {
+            throw new UploadException(__('Uploaded file format is limited'));
+            return false;
+        }
+        return true;
+    }
+
+    protected function checkImage($force = false)
+    {
+        //验证是否为图片文件
+        if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
+            $imgInfo = getimagesize($this->fileInfo['tmp_name']);
+            if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
+                throw new UploadException(__('Uploaded file is not a valid image'));
+            }
+            $this->fileInfo['imagewidth'] = isset($imgInfo[0]) ? $imgInfo[0] : 0;
+            $this->fileInfo['imageheight'] = isset($imgInfo[1]) ? $imgInfo[1] : 0;
+            return true;
+        } else {
+            return !$force;
+        }
+    }
+
+    protected function checkSize()
+    {
+        preg_match('/([0-9\.]+)(\w+)/', $this->config['maxsize'], $matches);
+        $size = $matches ? $matches[1] : $this->config['maxsize'];
+        $type = $matches ? strtolower($matches[2]) : 'b';
+        $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
+        $size = (int)($size * pow(1024, isset($typeDict[$type]) ? $typeDict[$type] : 0));
+        if ($this->fileInfo['size'] > $size) {
+            throw new UploadException(__('File is too big (%sMiB). Max filesize: %sMiB.',
+                round($this->fileInfo['size'] / pow(1024, 2), 2),
+                round($size / pow(1024, 2), 2)));
+        }
+    }
+
+    public function getSuffix()
+    {
+        return $this->fileInfo['suffix'] ?: 'file';
+    }
+
+    public function getSavekey($savekey = null, $filename = null, $md5 = null)
+    {
+        if ($filename) {
+            $suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+            $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
+        } else {
+            $suffix = $this->fileInfo['suffix'];
+        }
+        $filename = $filename ? $filename : ($suffix ? substr($this->fileInfo['name'], 0, strripos($this->fileInfo['name'], '.')) : $this->fileInfo['name']);
+        $md5 = $md5 ? $md5 : md5_file($this->fileInfo['tmp_name']);
+        $replaceArr = [
+            '{year}'     => date("Y"),
+            '{mon}'      => date("m"),
+            '{day}'      => date("d"),
+            '{hour}'     => date("H"),
+            '{min}'      => date("i"),
+            '{sec}'      => date("s"),
+            '{random}'   => Random::alnum(16),
+            '{random32}' => Random::alnum(32),
+            '{filename}' => $filename,
+            '{suffix}'   => $suffix,
+            '{.suffix}'  => $suffix ? '.' . $suffix : '',
+            '{filemd5}'  => $md5,
+        ];
+        $savekey = $savekey ? $savekey : $this->config['savekey'];
+        $savekey = str_replace(array_keys($replaceArr), array_values($replaceArr), $savekey);
+
+        return $savekey;
+    }
+
+    /**
+     * 清理分片文件
+     * @param $chunkid
+     */
+    public function clean($chunkid)
+    {
+        $iterator = new \GlobIterator($this->chunkDir . DS . $chunkid . '-*', FilesystemIterator::KEY_AS_FILENAME);
+        $array = iterator_to_array($iterator);
+        var_dump($array);
+    }
+
+    public function merge($chunkid, $chunkcount, $filename)
+    {
+        $filePath = $this->chunkDir . DS . $chunkid;
+
+        $completed = true;
+        //检查所有分片是否都存在
+        for ($i = 0; $i < $chunkcount; $i++) {
+            if (!file_exists("{$filePath}-{$i}.part")) {
+                $completed = false;
+                break;
+            }
+        }
+        if (!$completed) {
+            throw new UploadException(__('Chunk file info error'));
+        }
+
+        //如果所有文件分片都上传完毕,开始合并
+        $uploadPath = $filePath;
+
+        if (!$destFile = @fopen($uploadPath, "wb")) {
+            throw new UploadException(__('Chunk file merge error'));
+        }
+        if (flock($destFile, LOCK_EX)) { // 进行排他型锁定
+            for ($i = 0; $i < $chunkcount; $i++) {
+                $partFile = "{$filePath}-{$i}.part";
+                if (!$handle = @fopen($partFile, "rb")) {
+                    break;
+                }
+                while ($buff = fread($handle, filesize($partFile))) {
+                    fwrite($destFile, $buff);
+                }
+                @fclose($handle);
+                @unlink($partFile); //删除分片
+            }
+
+            flock($destFile, LOCK_UN);
+        }
+        @fclose($destFile);
+
+        $file = new File($uploadPath);
+        $info = [
+            'name'     => $filename,
+            'type'     => $file->getMime(),
+            'tmp_name' => $uploadPath,
+            'error'    => 0,
+            'size'     => $file->getSize()
+        ];
+        $file->setUploadInfo($info);
+        $file->isTest(true);
+
+        //重新设置文件
+        $this->setFile($file);
+
+        //允许大文件
+        $this->config['maxsize'] = "1024G";
+
+        return $this->upload();
+    }
+
+    /**
+     * 分片上传
+     * @throws UploadException
+     */
+    public function chunk($chunkid, $chunkindex, $chunkcount, $chunkfilesize = null, $chunkfilename = null, $direct = false)
+    {
+
+        if ($this->fileInfo['type'] != 'application/octet-stream') {
+            throw new UploadException(__('Uploaded file format is limited'));
+        }
+
+        $destDir = RUNTIME_PATH . 'chunks';
+        $fileName = $chunkid . "-" . $chunkindex . '.part';
+        $destFile = $destDir . DS . $fileName;
+        if (!move_uploaded_file($this->file->getPathname(), $destFile)) {
+            throw new UploadException(__('Chunk file write error'));
+        }
+        $file = new File($destFile);
+        $this->setFile($file);
+        return $file;
+    }
+
+    /**
+     * 普通上传
+     * @return \app\common\model\attachment|\think\Model
+     * @throws UploadException
+     */
+    public function upload($savekey = null)
+    {
+        if (empty($this->file)) {
+            throw new UploadException(__('No file upload or server upload limit exceeded'));
+        }
+
+        $this->checkSize();
+        $this->checkExecutable();
+        $this->checkMimetype();
+        $this->checkImage();
+
+        $savekey = $savekey ? $savekey : $this->getSavekey();
+        $savekey = '/' . ltrim($savekey, '/');
+        $uploadDir = substr($savekey, 0, strripos($savekey, '/') + 1);
+        $fileName = substr($savekey, strripos($savekey, '/') + 1);
+
+        $destDir = ROOT_PATH . 'public' . $uploadDir;
+
+        $sha1 = $this->file->hash();
+
+        $file = $this->file->move($destDir, $fileName);
+        if (!$file) {
+            // 上传失败获取错误信息
+            throw new UploadException($this->file->getError());
+        }
+        $this->file = $file;
+        $params = array(
+            'admin_id'    => (int)session('admin.id'),
+            'user_id'     => (int)cookie('uid'),
+            'filename'    => htmlspecialchars(strip_tags($this->fileInfo['name'])),
+            'filesize'    => $this->fileInfo['size'],
+            'imagewidth'  => $this->fileInfo['imagewidth'],
+            'imageheight' => $this->fileInfo['imageheight'],
+            'imagetype'   => $this->fileInfo['suffix'],
+            'imageframes' => 0,
+            'mimetype'    => $this->fileInfo['type'],
+            'url'         => $uploadDir . $file->getSaveName(),
+            'uploadtime'  => time(),
+            'storage'     => 'local',
+            'sha1'        => $sha1,
+            'extparam'    => '',
+        );
+        $attachment = new Attachment();
+        $attachment->data(array_filter($params));
+        $attachment->save();
+
+        \think\Hook::listen("upload_after", $attachment);
+        return $attachment;
+    }
+
+    public function setError($msg)
+    {
+        $this->error = $msg;
+    }
+
+    public function getError()
+    {
+        return $this->error;
+    }
+}

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

@@ -38,7 +38,7 @@ class Attachment extends Model
     {
         // 如果已经上传该资源,则不再记录
         self::beforeInsert(function ($model) {
-            if (self::where('url', '=', $model['url'])->find()) {
+            if (self::where('url', '=', $model['url'])->where('storage', $model['storage'])->find()) {
                 return false;
             }
         });

+ 21 - 18
application/common/model/Config.php

@@ -29,24 +29,27 @@ class Config extends Model
     public static function getTypeList()
     {
         $typeList = [
-            'string'   => __('String'),
-            'text'     => __('Text'),
-            'editor'   => __('Editor'),
-            'number'   => __('Number'),
-            'date'     => __('Date'),
-            'time'     => __('Time'),
-            'datetime' => __('Datetime'),
-            'select'   => __('Select'),
-            'selects'  => __('Selects'),
-            'image'    => __('Image'),
-            'images'   => __('Images'),
-            'file'     => __('File'),
-            'files'    => __('Files'),
-            'switch'   => __('Switch'),
-            'checkbox' => __('Checkbox'),
-            'radio'    => __('Radio'),
-            'array'    => __('Array'),
-            'custom'   => __('Custom'),
+            'string'      => __('String'),
+            'text'        => __('Text'),
+            'editor'      => __('Editor'),
+            'number'      => __('Number'),
+            'date'        => __('Date'),
+            'time'        => __('Time'),
+            'datetime'    => __('Datetime'),
+            'select'      => __('Select'),
+            'selects'     => __('Selects'),
+            'image'       => __('Image'),
+            'images'      => __('Images'),
+            'file'        => __('File'),
+            'files'       => __('Files'),
+            'switch'      => __('Switch'),
+            'checkbox'    => __('Checkbox'),
+            'radio'       => __('Radio'),
+            'city'        => __('City'),
+            'selectpage'  => __('Selectpage'),
+            'selectpages' => __('Selectpages'),
+            'array'       => __('Array'),
+            'custom'      => __('Custom'),
         ];
         return $typeList;
     }

+ 1 - 1
application/config.php

@@ -276,7 +276,7 @@ return [
         //自动检测更新
         'checkupdate'           => false,
         //版本号
-        'version'               => '1.1.0.20200612_beta',
+        'version'               => '1.2.0',
         //API接口地址
         'api_url'               => 'https://api.fastadmin.net',
     ],

+ 4 - 0
application/extra/upload.php

@@ -26,4 +26,8 @@ return [
      * 是否支持批量上传
      */
     'multiple'  => false,
+    /**
+     * 是否支持分片上传
+     */
+    'chunking'  => false,
 ];

+ 18 - 0
application/index/lang/zh-cn.php

@@ -107,6 +107,24 @@ return [
     'User center'                                            => '会员中心',
     'Change password'                                        => '修改密码',
     'Please login first'                                     => '请登录后再操作',
+    'Uploaded successful'                                    => '上传成功',
+    'You can upload up to %d file%s'                         => '你最多还可以上传%d个文件',
+    'You can choose up to %d file%s'                         => '你最多还可以选择%d个文件',
+    'Chunk file write error'                                 => '分片写入失败',
+    'Chunk file info error'                                  => '分片文件错误',
+    'Chunk file merge error'                                 => '分片合并错误',
+    'Chunk file disabled'                                    => '未开启分片上传功能',
+    'Cancel upload'                                          => '取消上传',
+    'Upload canceled'                                        => '上传已取消',
+    'No file upload or server upload limit exceeded'         => '未上传文件或超出服务器上传限制',
+    'Uploaded file format is limited'                        => '上传文件格式受限制',
+    'Uploaded file is not a valid image'                     => '上传文件不是有效的图片文件',
+    'Are you sure you want to cancel this upload?'           => '确定取消上传?',
+    'Remove file'                                            => '移除文件',
+    'You can only upload a maximum of %s files'              => '你最多允许上传 %s 个文件',
+    'You can\'t upload files of this type'                   => '不允许上传的文件类型',
+    'Server responded with %s code'                          => '服务端响应(Code:%s)',
+    'File is too big (%sMiB), Max filesize: %sMiB'           => '当前上传(%sM),最大允许上传文件大小:%sM',
     'Send verification code'                                 => '发送验证码',
     'Redirect now'                                           => '立即跳转',
     'Operation completed'                                    => '操作成功!',

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

@@ -1,8 +1,3 @@
 <?php
 
-return [
-    'No file upload or server upload limit exceeded' => '未上传文件或超出服务器上传限制',
-    'Uploaded file format is limited'                => '上传文件格式受限制',
-    'Uploaded file is not a valid image'             => '上传文件不是有效的图片文件',
-    'Upload successful'                              => '上传成功',
-];
+return [];

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

@@ -55,7 +55,6 @@ return [
     'New mobile'                            => '新手机号',
     'Change password successful'            => '修改密码成功',
     'Captcha is incorrect'                  => '验证码不正确',
-    'Upload successful'                     => '上传成功',
     'Logged in successful'                  => '登录成功',
     'Logout successful'                     => '注销成功',
     'User center already closed'            => '会员中心已经关闭',

+ 14 - 10
application/index/view/user/login.html

@@ -1,9 +1,9 @@
 <div id="content-container" class="container">
     <div class="user-section login-section">
-        <div class="logon-tab clearfix"> <a class="active">{:__('Sign in')}</a> <a href="{:url('user/register')}?url={$url|urlencode}">{:__('Sign up')}</a> </div>
-        <div class="login-main"> 
+        <div class="logon-tab clearfix"><a class="active">{:__('Sign in')}</a> <a href="{:url('user/register')}?url={$url|urlencode}">{:__('Sign up')}</a></div>
+        <div class="login-main">
             <form name="form" id="login-form" class="form-vertical" method="POST" action="">
-                <input type="hidden" name="url" value="{$url}" />
+                <input type="hidden" name="url" value="{$url}"/>
                 {:token()}
                 <div class="form-group">
                     <label class="control-label" for="account">{:__('Account')}</label>
@@ -20,7 +20,11 @@
                 </div>
                 <div class="form-group">
                     <div class="controls">
-                        <input type="checkbox" name="keeplogin" checked="checked" value="1"> {:__('Keep login')} 
+                        <div class="checkbox inline">
+                            <label>
+                                <input type="checkbox" name="keeplogin" checked="checked" value="1"> {:__('Keep login')}
+                            </label>
+                        </div>
                         <div class="pull-right"><a href="javascript:;" class="btn-forgot">{:__('Forgot password')}</a></div>
                     </div>
                 </div>
@@ -34,27 +38,27 @@
 <script type="text/html" id="resetpwdtpl">
     <form id="resetpwd-form" class="form-horizontal form-layer" method="POST" action="{:url('api/user/resetpwd')}">
         <div class="form-body">
-            <input type="hidden" name="action" value="resetpwd" />
+            <input type="hidden" name="action" value="resetpwd"/>
             <div class="form-group">
                 <label class="control-label col-xs-12 col-sm-3">{:__('Type')}:</label>
                 <div class="col-xs-12 col-sm-8">
                     <div class="radio">
                         <label for="type-email"><input id="type-email" checked="checked" name="type" data-send-url="{:url('api/ems/send')}" data-check-url="{:url('api/validate/check_ems_correct')}" type="radio" value="email"> {:__('Reset password by email')}</label>
                         <label for="type-mobile"><input id="type-mobile" name="type" type="radio" data-send-url="{:url('api/sms/send')}" data-check-url="{:url('api/validate/check_sms_correct')}" value="mobile"> {:__('Reset password by mobile')}</label>
-                    </div>        
+                    </div>
                 </div>
             </div>
             <div class="form-group" data-type="email">
                 <label for="email" class="control-label col-xs-12 col-sm-3">{:__('Email')}:</label>
                 <div class="col-xs-12 col-sm-8">
-                    <input type="text" class="form-control" id="email" name="email" value="" data-rule="required(#type-email:checked);email;remote({:url('api/validate/check_email_exist')}, event=resetpwd, id={$user.id})" placeholder="">
+                    <input type="text" class="form-control" id="email" name="email" value="" data-rule="required(#type-email:checked);email;remote({:url('api/validate/check_email_exist')}, event=resetpwd, id=0)" placeholder="">
                     <span class="msg-box"></span>
                 </div>
             </div>
             <div class="form-group hide" data-type="mobile">
                 <label for="mobile" class="control-label col-xs-12 col-sm-3">{:__('Mobile')}:</label>
                 <div class="col-xs-12 col-sm-8">
-                    <input type="text" class="form-control" id="mobile" name="mobile" value="" data-rule="required(#type-mobile:checked);mobile;remote({:url('api/validate/check_mobile_exist')}, event=resetpwd, id={$user.id})" placeholder="">
+                    <input type="text" class="form-control" id="mobile" name="mobile" value="" data-rule="required(#type-mobile:checked);mobile;remote({:url('api/validate/check_mobile_exist')}, event=resetpwd, id=0)" placeholder="">
                     <span class="msg-box"></span>
                 </div>
             </div>
@@ -62,7 +66,7 @@
                 <label for="captcha" class="control-label col-xs-12 col-sm-3">{:__('Captcha')}:</label>
                 <div class="col-xs-12 col-sm-8">
                     <div class="input-group">
-                        <input type="text" name="captcha" class="form-control" data-rule="required;length(4);integer[+];remote({:url('api/validate/check_ems_correct')}, event=resetpwd, email:#email)" />
+                        <input type="text" name="captcha" class="form-control" data-rule="required;length(4);integer[+];remote({:url('api/validate/check_ems_correct')}, event=resetpwd, email:#email)"/>
                         <span class="input-group-btn" style="padding:0;border:none;">
                             <a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:url('api/ems/send')}" data-type="email" data-event="resetpwd">{:__('Send verification code')}</a>
                         </span>
@@ -85,4 +89,4 @@
             </div>
         </div>
     </form>
-</script>
+</script>

+ 3 - 3
application/index/view/user/profile.html

@@ -44,9 +44,9 @@
                             <label class="control-label col-xs-12 col-sm-2"></label>
                             <div class="col-xs-12 col-sm-4">
                                 <div class="profile-avatar-container">
-                                    <img class="profile-user-img img-responsive img-circle plupload" src="{$user.avatar|cdnurl}" alt="">
+                                    <img class="profile-user-img img-responsive img-circle" src="{$user.avatar|cdnurl}" alt="">
                                     <div class="profile-avatar-text img-circle">{:__('Click to edit')}</div>
-                                    <button id="plupload-avatar" class="plupload" data-mimetype="png,jpg,jpeg,gif" data-input-id="c-avatar"><i class="fa fa-upload"></i> {:__('Upload')}</button>
+                                    <button type="button" id="faupload-avatar" class="faupload" data-mimetype="png,jpg,jpeg,gif" data-input-id="c-avatar"><i class="fa fa-upload"></i> {:__('Upload')}</button>
                                 </div>
                             </div>
                         </div>
@@ -199,4 +199,4 @@
         margin-left:0;
         margin-right:0;
     }
-</style>
+</style>

+ 0 - 1
bower.json

@@ -12,7 +12,6 @@
     "bootstrap-table": "fastadmin-bootstraptable#~1.11.3",
     "jstree": "~3.3.2",
     "moment": "^2.20.1",
-    "plupload": "~2.2.0",
     "toastr": "~2.1.3",
     "jcrop": "~2.0.4",
     "eonasdan-bootstrap-datetimepicker": "~4.17.43",

+ 3 - 3
composer.json

@@ -19,12 +19,12 @@
         "topthink/framework": "~5.0.24",
         "topthink/think-captcha": "^1.0",
         "phpmailer/phpmailer": "~6.1.6",
-        "karsonzhang/fastadmin-addons": "~1.1.11",
+        "karsonzhang/fastadmin-addons": "~1.2.0",
         "overtrue/pinyin": "~3.0",
         "phpoffice/phpspreadsheet": "^1.2",
         "overtrue/wechat": "4.2.11",
-        "league/oauth2-server": "8.0",
-        "topthink/think-queue": "v1.1.6"
+        "ext-json": "*",
+        "ext-curl": "*"
     },
     "config": {
         "preferred-install": "dist"

+ 165 - 44
public/api.html

@@ -186,11 +186,12 @@
                     <div class="child collapse" id="验证接口">
                                                 <a href="javascript:;" data-id="23" class="list-group-item">检测邮箱</a>
                                                 <a href="javascript:;" data-id="24" class="list-group-item">检测用户名</a>
-                                                <a href="javascript:;" data-id="25" class="list-group-item">检测手机</a>
+                                                <a href="javascript:;" data-id="25" class="list-group-item">检测昵称</a>
                                                 <a href="javascript:;" data-id="26" class="list-group-item">检测手机</a>
-                                                <a href="javascript:;" data-id="27" class="list-group-item">检测邮箱</a>
-                                                <a href="javascript:;" data-id="28" class="list-group-item">检测手机验证码</a>
-                                                <a href="javascript:;" data-id="29" class="list-group-item">检测邮箱验证码</a>
+                                                <a href="javascript:;" data-id="27" class="list-group-item">检测手机</a>
+                                                <a href="javascript:;" data-id="28" class="list-group-item">检测邮箱</a>
+                                                <a href="javascript:;" data-id="29" class="list-group-item">检测手机验证码</a>
+                                                <a href="javascript:;" data-id="30" class="list-group-item">检测邮箱验证码</a>
                                             </div>
                                     </div>
             </div>
@@ -3137,7 +3138,7 @@
                     <div class="panel-heading" id="heading-25">
                         <h4 class="panel-title">
                             <span class="label label-success">GET</span>
-                            <a data-toggle="collapse" data-parent="#accordion25" href="#collapseOne25"> 检测手机 <span class="text-muted">/api/validate/check_mobile_available</span></a>
+                            <a data-toggle="collapse" data-parent="#accordion25" href="#collapseOne25"> 检测昵称 <span class="text-muted">/api/validate/check_nickname_available</span></a>
                         </h4>
                     </div>
                     <div id="collapseOne25" class="panel-collapse collapse">
@@ -3155,7 +3156,7 @@
 
                                 <div class="tab-pane active" id="info25">
                                     <div class="well">
-                                        检测手机                                    </div>
+                                        检测昵称                                    </div>
                                     <div class="panel panel-default">
                                         <div class="panel-heading"><strong>Headers</strong></div>
                                         <div class="panel-body">
@@ -3176,10 +3177,10 @@
                                                 </thead>
                                                 <tbody>
                                                                                                         <tr>
-                                                        <td>mobile</td>
+                                                        <td>nickname</td>
                                                         <td>string</td>
                                                         <td>是</td>
-                                                        <td>手机号</td>
+                                                        <td>昵称</td>
                                                     </tr>
                                                                                                         <tr>
                                                         <td>id</td>
@@ -3204,10 +3205,10 @@
                                                                                         <div class="panel panel-default">
                                                 <div class="panel-heading"><strong>参数</strong></div>
                                                 <div class="panel-body">
-                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_mobile_available" method="get" name="form25" id="form25">
+                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_nickname_available" method="get" name="form25" id="form25">
                                                                                                                 <div class="form-group">
-                                                            <label class="control-label" for="mobile">mobile</label>
-                                                            <input type="string" class="form-control input-sm" id="mobile" required placeholder="手机号" name="mobile">
+                                                            <label class="control-label" for="nickname">nickname</label>
+                                                            <input type="string" class="form-control input-sm" id="nickname" required placeholder="昵称" name="nickname">
                                                         </div>
                                                                                                                 <div class="form-group">
                                                             <label class="control-label" for="id">id</label>
@@ -3257,7 +3258,7 @@
                     <div class="panel-heading" id="heading-26">
                         <h4 class="panel-title">
                             <span class="label label-success">GET</span>
-                            <a data-toggle="collapse" data-parent="#accordion26" href="#collapseOne26"> 检测手机 <span class="text-muted">/api/validate/check_mobile_exist</span></a>
+                            <a data-toggle="collapse" data-parent="#accordion26" href="#collapseOne26"> 检测手机 <span class="text-muted">/api/validate/check_mobile_available</span></a>
                         </h4>
                     </div>
                     <div id="collapseOne26" class="panel-collapse collapse">
@@ -3301,6 +3302,12 @@
                                                         <td>是</td>
                                                         <td>手机号</td>
                                                     </tr>
+                                                                                                        <tr>
+                                                        <td>id</td>
+                                                        <td>string</td>
+                                                        <td>是</td>
+                                                        <td>排除会员ID</td>
+                                                    </tr>
                                                                                                     </tbody>
                                             </table>
                                                                                     </div>
@@ -3318,12 +3325,16 @@
                                                                                         <div class="panel panel-default">
                                                 <div class="panel-heading"><strong>参数</strong></div>
                                                 <div class="panel-body">
-                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_mobile_exist" method="get" name="form26" id="form26">
+                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_mobile_available" method="get" name="form26" id="form26">
                                                                                                                 <div class="form-group">
                                                             <label class="control-label" for="mobile">mobile</label>
                                                             <input type="string" class="form-control input-sm" id="mobile" required placeholder="手机号" name="mobile">
                                                         </div>
                                                                                                                 <div class="form-group">
+                                                            <label class="control-label" for="id">id</label>
+                                                            <input type="string" class="form-control input-sm" id="id" required placeholder="排除会员ID" name="id">
+                                                        </div>
+                                                                                                                <div class="form-group">
                                                             <button type="submit" class="btn btn-success send" rel="26">提交</button>
                                                             <button type="reset" class="btn btn-info" rel="26">重置</button>
                                                         </div>
@@ -3367,7 +3378,7 @@
                     <div class="panel-heading" id="heading-27">
                         <h4 class="panel-title">
                             <span class="label label-success">GET</span>
-                            <a data-toggle="collapse" data-parent="#accordion27" href="#collapseOne27"> 检测邮箱 <span class="text-muted">/api/validate/check_email_exist</span></a>
+                            <a data-toggle="collapse" data-parent="#accordion27" href="#collapseOne27"> 检测手机 <span class="text-muted">/api/validate/check_mobile_exist</span></a>
                         </h4>
                     </div>
                     <div id="collapseOne27" class="panel-collapse collapse">
@@ -3385,7 +3396,7 @@
 
                                 <div class="tab-pane active" id="info27">
                                     <div class="well">
-                                        检测邮箱                                    </div>
+                                        检测手机                                    </div>
                                     <div class="panel panel-default">
                                         <div class="panel-heading"><strong>Headers</strong></div>
                                         <div class="panel-body">
@@ -3409,7 +3420,7 @@
                                                         <td>mobile</td>
                                                         <td>string</td>
                                                         <td>是</td>
-                                                        <td>邮箱</td>
+                                                        <td>手机号</td>
                                                     </tr>
                                                                                                     </tbody>
                                             </table>
@@ -3428,10 +3439,10 @@
                                                                                         <div class="panel panel-default">
                                                 <div class="panel-heading"><strong>参数</strong></div>
                                                 <div class="panel-body">
-                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_email_exist" method="get" name="form27" id="form27">
+                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_mobile_exist" method="get" name="form27" id="form27">
                                                                                                                 <div class="form-group">
                                                             <label class="control-label" for="mobile">mobile</label>
-                                                            <input type="string" class="form-control input-sm" id="mobile" required placeholder="邮箱" name="mobile">
+                                                            <input type="string" class="form-control input-sm" id="mobile" required placeholder="手机号" name="mobile">
                                                         </div>
                                                                                                                 <div class="form-group">
                                                             <button type="submit" class="btn btn-success send" rel="27">提交</button>
@@ -3477,7 +3488,7 @@
                     <div class="panel-heading" id="heading-28">
                         <h4 class="panel-title">
                             <span class="label label-success">GET</span>
-                            <a data-toggle="collapse" data-parent="#accordion28" href="#collapseOne28"> 检测手机验证码 <span class="text-muted">/api/validate/check_sms_correct</span></a>
+                            <a data-toggle="collapse" data-parent="#accordion28" href="#collapseOne28"> 检测邮箱 <span class="text-muted">/api/validate/check_email_exist</span></a>
                         </h4>
                     </div>
                     <div id="collapseOne28" class="panel-collapse collapse">
@@ -3495,6 +3506,116 @@
 
                                 <div class="tab-pane active" id="info28">
                                     <div class="well">
+                                        检测邮箱                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>Headers</strong></div>
+                                        <div class="panel-body">
+                                                                                        无
+                                                                                    </div>
+                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>参数</strong></div>
+                                        <div class="panel-body">
+                                                                                        <table class="table table-hover">
+                                                <thead>
+                                                    <tr>
+                                                        <th>名称</th>
+                                                        <th>类型</th>
+                                                        <th>必选</th>
+                                                        <th>描述</th>
+                                                    </tr>
+                                                </thead>
+                                                <tbody>
+                                                                                                        <tr>
+                                                        <td>mobile</td>
+                                                        <td>string</td>
+                                                        <td>是</td>
+                                                        <td>邮箱</td>
+                                                    </tr>
+                                                                                                    </tbody>
+                                            </table>
+                                                                                    </div>
+                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>正文</strong></div>
+                                        <div class="panel-body">
+                                            无                                        </div>
+                                    </div>
+                                </div><!-- #info -->
+
+                                <div class="tab-pane" id="sandbox28">
+                                    <div class="row">
+                                        <div class="col-md-12">
+                                                                                        <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>参数</strong></div>
+                                                <div class="panel-body">
+                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_email_exist" method="get" name="form28" id="form28">
+                                                                                                                <div class="form-group">
+                                                            <label class="control-label" for="mobile">mobile</label>
+                                                            <input type="string" class="form-control input-sm" id="mobile" required placeholder="邮箱" name="mobile">
+                                                        </div>
+                                                                                                                <div class="form-group">
+                                                            <button type="submit" class="btn btn-success send" rel="28">提交</button>
+                                                            <button type="reset" class="btn btn-info" rel="28">重置</button>
+                                                        </div>
+                                                    </form>
+                                                </div>
+                                            </div>
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>响应输出</strong></div>
+                                                <div class="panel-body">
+                                                    <div class="row">
+                                                        <div class="col-md-12" style="overflow-x:auto">
+                                                            <pre id="response_headers28"></pre>
+                                                            <pre id="response28"></pre>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>返回参数</strong></div>
+                                                <div class="panel-body">
+                                                                                                        无
+                                                                                                    </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div><!-- #sandbox -->
+
+                                <div class="tab-pane" id="sample28">
+                                    <div class="row">
+                                        <div class="col-md-12">
+                                            <pre id="sample_response28">无</pre>
+                                        </div>
+                                    </div>
+                                </div><!-- #sample -->
+
+                            </div><!-- .tab-content -->
+                        </div>
+                    </div>
+                </div>
+                                <div class="panel panel-default">
+                    <div class="panel-heading" id="heading-29">
+                        <h4 class="panel-title">
+                            <span class="label label-success">GET</span>
+                            <a data-toggle="collapse" data-parent="#accordion29" href="#collapseOne29"> 检测手机验证码 <span class="text-muted">/api/validate/check_sms_correct</span></a>
+                        </h4>
+                    </div>
+                    <div id="collapseOne29" class="panel-collapse collapse">
+                        <div class="panel-body">
+
+                            <!-- Nav tabs -->
+                            <ul class="nav nav-tabs" id="doctab29">
+                                <li class="active"><a href="#info29" data-toggle="tab">基础信息</a></li>
+                                <li><a href="#sandbox29" data-toggle="tab">在线测试</a></li>
+                                <li><a href="#sample29" data-toggle="tab">返回示例</a></li>
+                            </ul>
+
+                            <!-- Tab panes -->
+                            <div class="tab-content">
+
+                                <div class="tab-pane active" id="info29">
+                                    <div class="well">
                                         检测手机验证码                                    </div>
                                     <div class="panel panel-default">
                                         <div class="panel-heading"><strong>Headers</strong></div>
@@ -3544,13 +3665,13 @@
                                     </div>
                                 </div><!-- #info -->
 
-                                <div class="tab-pane" id="sandbox28">
+                                <div class="tab-pane" id="sandbox29">
                                     <div class="row">
                                         <div class="col-md-12">
                                                                                         <div class="panel panel-default">
                                                 <div class="panel-heading"><strong>参数</strong></div>
                                                 <div class="panel-body">
-                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_sms_correct" method="get" name="form28" id="form28">
+                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_sms_correct" method="get" name="form29" id="form29">
                                                                                                                 <div class="form-group">
                                                             <label class="control-label" for="mobile">mobile</label>
                                                             <input type="string" class="form-control input-sm" id="mobile" required placeholder="手机号" name="mobile">
@@ -3564,8 +3685,8 @@
                                                             <input type="string" class="form-control input-sm" id="event" required placeholder="事件" name="event">
                                                         </div>
                                                                                                                 <div class="form-group">
-                                                            <button type="submit" class="btn btn-success send" rel="28">提交</button>
-                                                            <button type="reset" class="btn btn-info" rel="28">重置</button>
+                                                            <button type="submit" class="btn btn-success send" rel="29">提交</button>
+                                                            <button type="reset" class="btn btn-info" rel="29">重置</button>
                                                         </div>
                                                     </form>
                                                 </div>
@@ -3575,8 +3696,8 @@
                                                 <div class="panel-body">
                                                     <div class="row">
                                                         <div class="col-md-12" style="overflow-x:auto">
-                                                            <pre id="response_headers28"></pre>
-                                                            <pre id="response28"></pre>
+                                                            <pre id="response_headers29"></pre>
+                                                            <pre id="response29"></pre>
                                                         </div>
                                                     </div>
                                                 </div>
@@ -3591,10 +3712,10 @@
                                     </div>
                                 </div><!-- #sandbox -->
 
-                                <div class="tab-pane" id="sample28">
+                                <div class="tab-pane" id="sample29">
                                     <div class="row">
                                         <div class="col-md-12">
-                                            <pre id="sample_response28">无</pre>
+                                            <pre id="sample_response29">无</pre>
                                         </div>
                                     </div>
                                 </div><!-- #sample -->
@@ -3604,26 +3725,26 @@
                     </div>
                 </div>
                                 <div class="panel panel-default">
-                    <div class="panel-heading" id="heading-29">
+                    <div class="panel-heading" id="heading-30">
                         <h4 class="panel-title">
                             <span class="label label-success">GET</span>
-                            <a data-toggle="collapse" data-parent="#accordion29" href="#collapseOne29"> 检测邮箱验证码 <span class="text-muted">/api/validate/check_ems_correct</span></a>
+                            <a data-toggle="collapse" data-parent="#accordion30" href="#collapseOne30"> 检测邮箱验证码 <span class="text-muted">/api/validate/check_ems_correct</span></a>
                         </h4>
                     </div>
-                    <div id="collapseOne29" class="panel-collapse collapse">
+                    <div id="collapseOne30" class="panel-collapse collapse">
                         <div class="panel-body">
 
                             <!-- Nav tabs -->
-                            <ul class="nav nav-tabs" id="doctab29">
-                                <li class="active"><a href="#info29" data-toggle="tab">基础信息</a></li>
-                                <li><a href="#sandbox29" data-toggle="tab">在线测试</a></li>
-                                <li><a href="#sample29" data-toggle="tab">返回示例</a></li>
+                            <ul class="nav nav-tabs" id="doctab30">
+                                <li class="active"><a href="#info30" data-toggle="tab">基础信息</a></li>
+                                <li><a href="#sandbox30" data-toggle="tab">在线测试</a></li>
+                                <li><a href="#sample30" data-toggle="tab">返回示例</a></li>
                             </ul>
 
                             <!-- Tab panes -->
                             <div class="tab-content">
 
-                                <div class="tab-pane active" id="info29">
+                                <div class="tab-pane active" id="info30">
                                     <div class="well">
                                         检测邮箱验证码                                    </div>
                                     <div class="panel panel-default">
@@ -3674,13 +3795,13 @@
                                     </div>
                                 </div><!-- #info -->
 
-                                <div class="tab-pane" id="sandbox29">
+                                <div class="tab-pane" id="sandbox30">
                                     <div class="row">
                                         <div class="col-md-12">
                                                                                         <div class="panel panel-default">
                                                 <div class="panel-heading"><strong>参数</strong></div>
                                                 <div class="panel-body">
-                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_ems_correct" method="get" name="form29" id="form29">
+                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="/api/validate/check_ems_correct" method="get" name="form30" id="form30">
                                                                                                                 <div class="form-group">
                                                             <label class="control-label" for="email">email</label>
                                                             <input type="string" class="form-control input-sm" id="email" required placeholder="邮箱" name="email">
@@ -3694,8 +3815,8 @@
                                                             <input type="string" class="form-control input-sm" id="event" required placeholder="事件" name="event">
                                                         </div>
                                                                                                                 <div class="form-group">
-                                                            <button type="submit" class="btn btn-success send" rel="29">提交</button>
-                                                            <button type="reset" class="btn btn-info" rel="29">重置</button>
+                                                            <button type="submit" class="btn btn-success send" rel="30">提交</button>
+                                                            <button type="reset" class="btn btn-info" rel="30">重置</button>
                                                         </div>
                                                     </form>
                                                 </div>
@@ -3705,8 +3826,8 @@
                                                 <div class="panel-body">
                                                     <div class="row">
                                                         <div class="col-md-12" style="overflow-x:auto">
-                                                            <pre id="response_headers29"></pre>
-                                                            <pre id="response29"></pre>
+                                                            <pre id="response_headers30"></pre>
+                                                            <pre id="response30"></pre>
                                                         </div>
                                                     </div>
                                                 </div>
@@ -3721,10 +3842,10 @@
                                     </div>
                                 </div><!-- #sandbox -->
 
-                                <div class="tab-pane" id="sample29">
+                                <div class="tab-pane" id="sample30">
                                     <div class="row">
                                         <div class="col-md-12">
-                                            <pre id="sample_response29">无</pre>
+                                            <pre id="sample_response30">无</pre>
                                         </div>
                                     </div>
                                 </div><!-- #sample -->
@@ -3739,7 +3860,7 @@
 
             <div class="row mt0 footer">
                 <div class="col-md-6" align="left">
-                    Generated on 2020-02-16 16:01:35                </div>
+                    Generated on 2020-08-12 21:00:33                </div>
                 <div class="col-md-6" align="right">
                     <a href="./" target="_blank">我的网站</a>
                 </div>

+ 34 - 7
public/assets/css/backend.css

@@ -119,6 +119,20 @@ table.table-template {
   right: 0;
   top: 0;
 }
+.sp_container .sp_element_box {
+  overflow: unset;
+}
+.sp_container .sp_element_box > li.input_box {
+  position: unset;
+}
+.sp_container .sp_element_box .msg-box {
+  right: -24px;
+}
+@media (max-width: 767px) {
+  .sp_container .sp_element_box .msg-box {
+    left: inherit;
+  }
+}
 .toast-top-right-index {
   top: 62px;
   right: 12px;
@@ -133,6 +147,12 @@ table.table-template {
   margin-bottom: -5px;
   padding: 10px 20px;
 }
+select.bs-select-hidden,
+select.selectpicker {
+  display: inherit !important;
+  max-height: 31px;
+  overflow: hidden;
+}
 .img-center {
   margin: 0 auto;
   display: inline;
@@ -599,6 +619,7 @@ form.form-horizontal .control-label {
 .nice-validator select,
 .nice-validator textarea,
 .nice-validator [contenteditable] {
+  vertical-align: top;
   display: inline-block;
   *display: inline;
   *zoom: 1;
@@ -608,23 +629,29 @@ form.form-horizontal .control-label {
   display: inherit;
 }
 /*预览区域*/
-.plupload-preview {
+.plupload-preview,
+.faupload-preview {
   padding: 0 10px;
   margin-bottom: 0;
 }
-.plupload-preview li {
+.plupload-preview li,
+.faupload-preview li {
   margin-top: 15px;
 }
-.plupload-preview .thumbnail {
+.plupload-preview .thumbnail,
+.faupload-preview .thumbnail {
   margin-bottom: 10px;
 }
-.plupload-preview a {
+.plupload-preview a,
+.faupload-preview a {
   display: block;
 }
-.plupload-preview a:first-child {
+.plupload-preview a:first-child,
+.faupload-preview a:first-child {
   height: 90px;
 }
-.plupload-preview a img {
+.plupload-preview a img,
+.faupload-preview a img {
   height: 80px;
   object-fit: cover;
 }
@@ -877,7 +904,7 @@ table.table-nowrap thead > tr > th {
   margin: 0;
   padding: 0;
 }
-.n-bootstrap .input-group > .n-right {
+.input-group > .msg-box.n-right {
   position: absolute;
 }
 @media (min-width: 564px) {

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


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


+ 2 - 3
public/assets/css/fastadmin.css

@@ -943,8 +943,7 @@ a:focus {
  */
 /*Dropdowns in general*/
 .dropdown-menu {
-  box-shadow: none;
-  border-color: #eee;
+  border: none;
 }
 .dropdown-menu > li > a {
   /*color: #777;*/
@@ -5829,4 +5828,4 @@ fieldset[disabled] .btn-yahoo.active {
     white-space: normal !important;
   }
 }
-/*# sourceMappingURL=../css/fastadmin.css.map */
+/*# sourceMappingURL=fastadmin.css.map */

+ 25 - 7
public/assets/css/frontend.css

@@ -21,7 +21,7 @@ body {
 .navbar {
   border: none;
 }
-.navbar-nav li > a {
+.navbar-nav > li > a {
   font-size: 14px;
 }
 .toast-top-center {
@@ -37,23 +37,29 @@ body {
   display: inherit;
 }
 /*预览区域*/
-.plupload-preview {
+.plupload-preview,
+.faupload-preview {
   padding: 0 10px;
   margin-bottom: 0;
 }
-.plupload-preview li {
+.plupload-preview li,
+.faupload-preview li {
   margin-top: 10px;
 }
-.plupload-preview .thumbnail {
+.plupload-preview .thumbnail,
+.faupload-preview .thumbnail {
   margin-bottom: 10px;
 }
-.plupload-preview a {
+.plupload-preview a,
+.faupload-preview a {
   display: block;
 }
-.plupload-preview a:first-child {
+.plupload-preview a:first-child,
+.faupload-preview a:first-child {
   height: 90px;
 }
-.plupload-preview a img {
+.plupload-preview a img,
+.faupload-preview a img {
   height: 80px;
   object-fit: cover;
 }
@@ -70,6 +76,18 @@ body {
   padding: 12px 25px;
   text-align: center;
 }
+.input-group > .msg-box.n-right {
+  position: absolute;
+}
+/*修复radio和checkbox样式对齐*/
+.radio > label,
+.checkbox > label {
+  margin-right: 10px;
+}
+.radio > label > input,
+.checkbox > label > input {
+  margin: 2px 0 0;
+}
 #header-navbar li.dropdown ul.dropdown-menu {
   min-width: 94px;
 }

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


+ 2 - 2
public/assets/js/backend/addon.js

@@ -164,7 +164,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
 
             // 离线安装
             require(['upload'], function (Upload) {
-                Upload.api.plupload("#plupload-addon", function (data, ret) {
+                Upload.api.upload("#faupload-addon", function (data, ret) {
                     Config['addons'][data.addon.name] = data.addon;
                     Toastr.success(ret.msg);
                     operate(data.addon.name, 'enable', false);
@@ -585,4 +585,4 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
         }
     };
     return Controller;
-});
+});

+ 9 - 7
public/assets/js/backend/general/attachment.js

@@ -27,11 +27,8 @@ define(['jquery', 'bootstrap', 'backend', 'form', 'table'], function ($, undefin
                         {field: 'admin_id', title: __('Admin_id'), visible: false, addClass: "selectpage", extend: "data-source='auth/admin/index' data-field='nickname'"},
                         {field: 'user_id', title: __('User_id'), visible: false, addClass: "selectpage", extend: "data-source='user/user/index' data-field='nickname'"},
                         {field: 'url', title: __('Preview'), formatter: Controller.api.formatter.thumb, operate: false},
-                        {field: 'url', title: __('Url'), formatter: Controller.api.formatter.url},
-                        {field: 'imagewidth', title: __('Imagewidth'), sortable: true},
-                        {field: 'imageheight', title: __('Imageheight'), sortable: true},
-                        {field: 'imagetype', title: __('Imagetype'), formatter: Table.api.formatter.search},
-                        {field: 'storage', title: __('Storage'), formatter: Table.api.formatter.search},
+                        {field: 'url', title: __('Url'), formatter: Controller.api.formatter.url, visible: false},
+                        {field: 'filename', title: __('Filename'), formatter: Table.api.formatter.search, operate: 'like'},
                         {
                             field: 'filesize', title: __('Filesize'), operate: 'BETWEEN', sortable: true, formatter: function (value, row, index) {
                                 var size = parseFloat(value);
@@ -39,6 +36,10 @@ define(['jquery', 'bootstrap', 'backend', 'form', 'table'], function ($, undefin
                                 return (size / Math.pow(1024, i)).toFixed(i < 2 ? 0 : 2) * 1 + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
                             }
                         },
+                        {field: 'imagewidth', title: __('Imagewidth'), sortable: true},
+                        {field: 'imageheight', title: __('Imageheight'), sortable: true},
+                        {field: 'imagetype', title: __('Imagetype'), formatter: Table.api.formatter.search, operate: 'like'},
+                        {field: 'storage', title: __('Storage'), formatter: Table.api.formatter.search, operate: 'like'},
                         {field: 'mimetype', title: __('Mimetype'), formatter: Table.api.formatter.search},
                         {
                             field: 'createtime',
@@ -105,6 +106,7 @@ define(['jquery', 'bootstrap', 'backend', 'form', 'table'], function ($, undefin
                         {field: 'admin_id', title: __('Admin_id'), formatter: Table.api.formatter.search, visible: false},
                         {field: 'user_id', title: __('User_id'), formatter: Table.api.formatter.search, visible: false},
                         {field: 'url', title: __('Preview'), formatter: Controller.api.formatter.thumb, operate: false},
+                        {field: 'filename', title: __('Filename'), formatter: Table.api.formatter.search, operate: 'like'},
                         {field: 'imagewidth', title: __('Imagewidth'), operate: false},
                         {field: 'imageheight', title: __('Imageheight'), operate: false},
                         {
@@ -143,7 +145,7 @@ define(['jquery', 'bootstrap', 'backend', 'form', 'table'], function ($, undefin
             // 为表格绑定事件
             Table.api.bindevent(table);
             require(['upload'], function (Upload) {
-                Upload.api.plupload($("#toolbar .plupload"), function () {
+                Upload.api.faupload($("#toolbar .faupload"), function () {
                     $(".btn-refresh").trigger("click");
                 });
             });
@@ -175,4 +177,4 @@ define(['jquery', 'bootstrap', 'backend', 'form', 'table'], function ($, undefin
 
     };
     return Controller;
-});
+});

+ 1 - 42
public/assets/js/backend/general/config.js

@@ -2,47 +2,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
 
     var Controller = {
         index: function () {
-            // 初始化表格参数配置
-            Table.api.init({
-                extend: {
-                    index_url: 'general/config/index',
-                    add_url: 'general/config/add',
-                    edit_url: 'general/config/edit',
-                    del_url: 'general/config/del',
-                    multi_url: 'general/config/multi',
-                    table: 'config',
-                }
-            });
-
-            var table = $("#table");
-
-            // 初始化表格
-            table.bootstrapTable({
-                url: $.fn.bootstrapTable.defaults.extend.index_url,
-                pk: 'id',
-                sortName: 'id',
-                columns: [
-                    [
-                        {field: 'state', checkbox: true},
-                        {field: 'id', title: __('Id')},
-                        {field: 'name', title: __('Name')},
-                        {field: 'intro', title: __('Intro')},
-                        {field: 'group', title: __('Group')},
-                        {field: 'type', title: __('Type')},
-                        {
-                            field: 'operate',
-                            title: __('Operate'),
-                            table: table,
-                            events: Table.api.events.operate,
-                            formatter: Table.api.formatter.operate
-                        }
-                    ]
-                ]
-            });
-
-            // 为表格绑定事件
-            Table.api.bindevent(table);
-
             $("form.edit-form").data("validator-options", {
                 display: function (elem) {
                     return $(elem).closest('tr').find("td:first").text();
@@ -131,4 +90,4 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         }
     };
     return Controller;
-});
+});

+ 3 - 3
public/assets/js/backend/general/profile.js

@@ -38,12 +38,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'upload'], function (
             Table.api.bindevent(table);//当内容渲染完成后
 
             // 给上传按钮添加上传成功事件
-            $("#plupload-avatar").data("upload-success", function (data) {
+            $("#faupload-avatar").data("upload-success", function (data) {
                 var url = Backend.api.cdnurl(data.url);
                 $(".profile-user-img").prop("src", url);
                 Toastr.success("上传成功!");
             });
-            
+
             // 给表单绑定事件
             Form.api.bindevent($("#update-form"), function () {
                 $("input[name='row[password]']").val('');
@@ -54,4 +54,4 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'upload'], function (
         },
     };
     return Controller;
-});
+});

File diff suppressed because it is too large
+ 3851 - 0
public/assets/js/dropzone.js


File diff suppressed because it is too large
+ 1 - 0
public/assets/js/dropzone.min.js


+ 2 - 2
public/assets/js/frontend/user.js

@@ -74,7 +74,7 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
         },
         profile: function () {
             // 给上传按钮添加上传成功事件
-            $("#plupload-avatar").data("upload-success", function (data) {
+            $("#faupload-avatar").data("upload-success", function (data) {
                 var url = Fast.api.cdnurl(data.url);
                 $(".profile-user-img").prop("src", url);
                 Toastr.success(__('Upload successful'));
@@ -101,4 +101,4 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
         }
     };
     return Controller;
-});
+});

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

@@ -4,8 +4,7 @@ require.config({
         name: 'moment',
         location: '../libs/moment',
         main: 'moment'
-    }
-    ],
+    }],
     //在打包压缩时将会把include中的模块合并到主文件中
     include: ['css', 'layer', 'toastr', 'fast', 'backend', 'backend-init', 'table', 'form', 'dragsort', 'drag', 'drop', 'addtabs', 'selectpage'],
     paths: {
@@ -16,6 +15,7 @@ require.config({
         'validator': 'require-validator',
         'drag': 'jquery.drag.min',
         'drop': 'jquery.drop.min',
+        'dropzone': 'dropzone.min',
         'echarts': 'echarts.min',
         'echarts-theme': 'echarts-theme',
         'adminlte': 'adminlte',
@@ -42,7 +42,6 @@ require.config({
         'slimscroll': '../libs/jquery-slimscroll/jquery.slimscroll',
         'validator-core': '../libs/nice-validator/dist/jquery.validator',
         'validator-lang': '../libs/nice-validator/dist/local/zh-CN',
-        'plupload': '../libs/plupload/js/plupload.min',
         'toastr': '../libs/toastr/toastr',
         'jstree': '../libs/jstree/dist/jstree.min',
         'layer': '../libs/fastadmin-layer/dist/layer',
@@ -114,11 +113,7 @@ require.config({
 //        'bootstrap-select': ['css!../libs/bootstrap-select/dist/css/bootstrap-select.min.css',],
         'bootstrap-select-lang': ['bootstrap-select'],
 //        'toastr': ['css!../libs/toastr/toastr.min.css'],
-        'jstree': ['css!../libs/jstree/dist/themes/default/style.css',],
-        'plupload': {
-            deps: ['../libs/plupload/js/moxie.min'],
-            exports: "plupload"
-        },
+        'jstree': ['css!../libs/jstree/dist/themes/default/style.css'],
 //        'layer': ['css!../libs/fastadmin-layer/dist/theme/default/layer.css'],
 //        'validator-core': ['css!../libs/nice-validator/dist/jquery.validator.css'],
         'validator-lang': ['validator-core'],

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


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

@@ -221,10 +221,22 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                     });
                 }
             },
+            /**
+             * 绑定上传事件
+             * @param form
+             * @deprecated Use faupload instead.
+             */
             plupload: function (form) {
-                //绑定plupload上传元素事件
-                if ($(".plupload", form).size() > 0) {
-                    Upload.api.plupload($(".plupload", form));
+                Form.events.faupload(form);
+            },
+            /**
+             * 绑定上传事件
+             * @param form
+             */
+            faupload: function (form) {
+                //绑定上传元素事件
+                if ($(".plupload,.faupload", form).size() > 0) {
+                    Upload.api.upload($(".plupload,.faupload", form));
                 }
             },
             faselect: function (form) {
@@ -495,7 +507,7 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
 
                 events.datetimepicker(form);
 
-                events.plupload(form);
+                events.faupload(form);
 
                 events.faselect(form);
 

+ 6 - 11
public/assets/js/require-frontend.js

@@ -1,11 +1,10 @@
 require.config({
     urlArgs: "v=" + requirejs.s.contexts._.config.config.site.version,
     packages: [{
-            name: 'moment',
-            location: '../libs/moment',
-            main: 'moment'
-        }
-    ],
+        name: 'moment',
+        location: '../libs/moment',
+        main: 'moment'
+    }],
     //在打包压缩时将会把include中的模块合并到主文件中
     include: ['css', 'layer', 'toastr', 'fast', 'frontend', 'frontend-init'],
     paths: {
@@ -16,6 +15,7 @@ require.config({
         'validator': 'require-validator',
         'drag': 'jquery.drag.min',
         'drop': 'jquery.drop.min',
+        'dropzone': 'dropzone.min',
         'echarts': 'echarts.min',
         'echarts-theme': 'echarts-theme',
         'adminlte': 'adminlte',
@@ -41,7 +41,6 @@ require.config({
         'slimscroll': '../libs/jquery-slimscroll/jquery.slimscroll',
         'validator-core': '../libs/nice-validator/dist/jquery.validator',
         'validator-lang': '../libs/nice-validator/dist/local/zh-CN',
-        'plupload': '../libs/plupload/js/plupload.min',
         'toastr': '../libs/toastr/toastr',
         'jstree': '../libs/jstree/dist/jstree.min',
         'layer': '../libs/fastadmin-layer/dist/layer',
@@ -113,11 +112,7 @@ require.config({
 //        'bootstrap-select': ['css!../libs/bootstrap-select/dist/css/bootstrap-select.min.css', ],
         'bootstrap-select-lang': ['bootstrap-select'],
 //        'toastr': ['css!../libs/toastr/toastr.min.css'],
-        'jstree': ['css!../libs/jstree/dist/themes/default/style.css', ],
-        'plupload': {
-            deps: ['../libs/plupload/js/moxie.min'],
-            exports: "plupload"
-        },
+        'jstree': ['css!../libs/jstree/dist/themes/default/style.css'],
 //        'layer': ['css!../libs/fastadmin-layer/dist/theme/default/layer.css'],
 //        'validator-core': ['css!../libs/nice-validator/dist/jquery.validator.css'],
         'validator-lang': ['validator-core'],

+ 6 - 11
public/assets/js/require-frontend.min.js

@@ -14,11 +14,10 @@ define("bootstrap", ["jquery"], function(){});
 require.config({
     urlArgs: "v=" + requirejs.s.contexts._.config.config.site.version,
     packages: [{
-            name: 'moment',
-            location: '../libs/moment',
-            main: 'moment'
-        }
-    ],
+        name: 'moment',
+        location: '../libs/moment',
+        main: 'moment'
+    }],
     //在打包压缩时将会把include中的模块合并到主文件中
     include: ['css', 'layer', 'toastr', 'fast', 'frontend', 'frontend-init'],
     paths: {
@@ -29,6 +28,7 @@ require.config({
         'validator': 'require-validator',
         'drag': 'jquery.drag.min',
         'drop': 'jquery.drop.min',
+        'dropzone': 'dropzone.min',
         'echarts': 'echarts.min',
         'echarts-theme': 'echarts-theme',
         'adminlte': 'adminlte',
@@ -54,7 +54,6 @@ require.config({
         'slimscroll': '../libs/jquery-slimscroll/jquery.slimscroll',
         'validator-core': '../libs/nice-validator/dist/jquery.validator',
         'validator-lang': '../libs/nice-validator/dist/local/zh-CN',
-        'plupload': '../libs/plupload/js/plupload.min',
         'toastr': '../libs/toastr/toastr',
         'jstree': '../libs/jstree/dist/jstree.min',
         'layer': '../libs/fastadmin-layer/dist/layer',
@@ -126,11 +125,7 @@ require.config({
 //        'bootstrap-select': ['css!../libs/bootstrap-select/dist/css/bootstrap-select.min.css', ],
         'bootstrap-select-lang': ['bootstrap-select'],
 //        'toastr': ['css!../libs/toastr/toastr.min.css'],
-        'jstree': ['css!../libs/jstree/dist/themes/default/style.css', ],
-        'plupload': {
-            deps: ['../libs/plupload/js/moxie.min'],
-            exports: "plupload"
-        },
+        'jstree': ['css!../libs/jstree/dist/themes/default/style.css'],
 //        'layer': ['css!../libs/fastadmin-layer/dist/theme/default/layer.css'],
 //        'validator-core': ['css!../libs/nice-validator/dist/jquery.validator.css'],
         'validator-lang': ['validator-core'],

+ 8 - 8
public/assets/js/require-table.js

@@ -57,7 +57,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
             valign: 'middle',
         },
         config: {
-            firsttd: 'tbody tr td:first-child:not(:has(div.card-views))',
+            firsttd: 'tbody>tr>td.bs-checkbox',
             toolbar: '.toolbar',
             refreshbtn: '.btn-refresh',
             addbtn: '.btn-add',
@@ -169,10 +169,11 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 table.on('post-body.bs.table', function (e, settings, json, xhr) {
                     $(Table.config.refreshbtn, toolbar).find(".fa").removeClass("fa-spin");
                     $(Table.config.disabledbtn, toolbar).toggleClass('disabled', true);
-                    if ($(Table.config.firsttd, table).find("input[type='checkbox'][data-index]").size() > 0) {
-                        // 拽选择,需要重新绑定事件
+                    if ($(Table.config.firsttd + ":first", table).find("input[type='checkbox'][data-index]").size() > 0) {
+                        // 拽选择,需要重新绑定事件
                         require(['drag', 'drop'], function () {
-                            $(Table.config.firsttd, table).drag("start", function (ev, dd) {
+                            var firsttd = $(Table.config.firsttd, table);
+                            firsttd.drag("start", function (ev, dd) {
                                 return $('<div class="selection" />').css('opacity', .65).appendTo(document.body);
                             }).drag(function (ev, dd) {
                                 $(dd.proxy).css({
@@ -184,7 +185,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                             }).drag("end", function (ev, dd) {
                                 $(dd.proxy).remove();
                             });
-                            $(Table.config.firsttd, table).drop("start", function () {
+                            firsttd.drop("start", function () {
                                 Table.api.toggleattr(this);
                             }).drop(function () {
                                 Table.api.toggleattr(this);
@@ -231,7 +232,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 // 导入按钮事件
                 if ($(Table.config.importbtn, toolbar).size() > 0) {
                     require(['upload'], function (Upload) {
-                        Upload.api.plupload($(Table.config.importbtn, toolbar), function (data, ret) {
+                        Upload.api.upload($(Table.config.importbtn, toolbar), function (data, ret) {
                             Fast.api.ajax({
                                 url: options.extend.import_url,
                                 data: {file: data.url},
@@ -382,8 +383,7 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
                 var options = table.bootstrapTable('getOptions');
                 var data = element ? $(element).data() : {};
                 var ids = ($.isArray(ids) ? ids.join(",") : ids);
-                var url = typeof data.url !== "undefined" ? data.url : (action == "del" ? options.extend.del_url : options.extend.multi_url);
-                url = this.replaceurl(url, {ids: ids}, table);
+                var url = typeof data.url !== "undefined" ? Table.api.replaceurl(data.url, {ids: ids}, table) : (action == "del" ? options.extend.del_url : options.extend.multi_url);
                 var params = typeof data.params !== "undefined" ? (typeof data.params == 'object' ? $.param(data.params) : data.params) : '';
                 var options = {url: url, data: {action: action, ids: ids, params: params}};
                 Fast.api.ajax(options, function (data, ret) {

+ 167 - 185
public/assets/js/require-upload.js

@@ -1,62 +1,21 @@
-define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined, Plupload, Template) {
+define(['jquery', 'bootstrap', 'dropzone', 'template'], function ($, undefined, Dropzone, Template) {
     var Upload = {
             list: {},
+            options: {},
             config: {
                 container: document.body,
-                classname: '.plupload:not([initialized])',
+                classname: '.plupload:not([initialized]),.faupload:not([initialized])',
                 previewtpl: '<li class="col-xs-3"><a href="<%=fullurl%>" data-url="<%=url%>" target="_blank" class="thumbnail"><img src="<%=fullurl%>" onerror="this.src=\'' + Fast.api.fixurl("ajax/icon") + '?suffix=<%=suffix%>\';this.onerror=null;" class="img-responsive"></a><a href="javascript:;" class="btn btn-danger btn-xs btn-trash"><i class="fa fa-trash"></i></a></li>',
             },
             events: {
-                onInit: function (up) {
-                    //修复少数安卓浏览器无法上传图片的Bug
-                    var input = $("input[type=file]", up.settings.container);
-                    if (input && input.prop("accept") && input.prop("accept").match(/image\//)) {
-                        input.prop("accept", "image/jpg," + input.prop("accept"));
-                    }
-                },
-                //初始化完成
-                onPostInit: function (up) {
-
-                },
-                //文件添加成功后
-                onFileAdded: function (up, files) {
-                    var button = up.settings.button;
-                    $(button).data("bakup-html", $(button).html());
-                    var maxcount = $(button).data("maxcount");
-                    var input_id = $(button).data("input-id") ? $(button).data("input-id") : "";
-                    maxcount = typeof maxcount !== "undefined" ? maxcount : 0;
-                    if (maxcount > 0 && input_id) {
-                        var inputObj = $("#" + input_id);
-                        if (inputObj.size() > 0) {
-                            var value = $.trim(inputObj.val());
-                            var nums = value === '' ? 0 : value.split(/\,/).length;
-                            var remains = maxcount - nums;
-                            if (files.length > remains) {
-                                for (var i = 0; i < files.length; i++) {
-                                    up.removeFile(files[i]);
-                                }
-                                Toastr.error(__('You can upload up to %d file%s', remains));
-                                return false;
-                            }
-                        }
-                    }
-                    //添加后立即上传
-                    setTimeout(function () {
-                        up.start();
-                    }, 1);
-                },
-                //上传进行中的回调
-                onUploadProgress: function (up, file) {
-
-                },
-                //上传之前的回调
-                onBeforeUpload: function (up, file) {
+                //初始化
+                onInit: function () {
 
                 },
                 //上传成功的回调
-                onUploadSuccess: function (up, ret) {
-                    var button = up.settings.button;
-                    var onUploadSuccess = up.settings.onUploadSuccess;
+                onUploadSuccess: function (up, ret, file) {
+                    var button = up.element;
+                    var onUploadSuccess = up.options.onUploadSuccess;
                     var data = typeof ret.data !== 'undefined' ? ret.data : null;
                     //上传成功后回调
                     if (button) {
@@ -92,9 +51,9 @@ define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined,
                     }
                 },
                 //上传错误的回调
-                onUploadError: function (up, ret) {
-                    var button = up.settings.button;
-                    var onUploadError = up.settings.onUploadError;
+                onUploadError: function (up, ret, file) {
+                    var button = up.element;
+                    var onUploadError = up.options.onUploadError;
                     var data = typeof ret.data !== 'undefined' ? ret.data : null;
                     if (button) {
                         var onDomUploadError = $(button).data("upload-error");
@@ -119,7 +78,7 @@ define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined,
                     Toastr.error(ret.msg + "(code:" + ret.code + ")");
                 },
                 //服务器响应数据后
-                onUploadResponse: function (response) {
+                onUploadResponse: function (response, up, file) {
                     try {
                         var ret = typeof response === 'object' ? response : JSON.parse(response);
                         if (!ret.hasOwnProperty('code')) {
@@ -132,8 +91,8 @@ define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined,
                 },
                 //上传全部结束后
                 onUploadComplete: function (up, files) {
-                    var button = up.settings.button;
-                    var onUploadComplete = up.settings.onUploadComplete;
+                    var button = up.element;
+                    var onUploadComplete = up.options.onUploadComplete;
                     if (button) {
                         var onDomUploadComplete = $(button).data("upload-complete");
                         if (onDomUploadComplete) {
@@ -157,8 +116,8 @@ define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined,
                 }
             },
             api: {
-                //Plupload上传
-                plupload: function (element, onUploadSuccess, onUploadError, onUploadComplete) {
+                //上传接口
+                upload: function (element, onUploadSuccess, onUploadError, onUploadComplete) {
                     element = typeof element === 'undefined' ? Upload.config.classname : element;
                     $(element, Upload.config.container).each(function () {
                         if ($(this).attr("initialized")) {
@@ -169,6 +128,7 @@ define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined,
                         var id = $(this).prop("id");
                         var url = $(this).data("url");
                         var maxsize = $(this).data("maxsize");
+                        var maxcount = $(this).data("maxcount");
                         var mimetype = $(this).data("mimetype");
                         var multipart = $(this).data("multipart");
                         var multiple = $(this).data("multiple");
@@ -189,65 +149,136 @@ define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined,
                         multipart = typeof multipart !== "undefined" ? multipart : Config.upload.multipart;
                         //是否支持批量上传
                         multiple = typeof multiple !== "undefined" ? multiple : Config.upload.multiple;
-                        var mimetypeArr = new Array();
-                        //支持后缀和Mimetype格式,以,分隔
-                        if (mimetype && mimetype !== "*" && mimetype.indexOf("/") === -1) {
-                            var tempArr = mimetype.split(',');
-                            for (var i = 0; i < tempArr.length; i++) {
-                                mimetypeArr.push({title: __('Files'), extensions: tempArr[i]});
-                            }
-                            mimetype = mimetypeArr;
-                        }
-                        //生成Plupload实例
-                        Upload.list[id] = new Plupload.Uploader({
-                            runtimes: 'html5,flash,silverlight,html4',
-                            multi_selection: multiple, //是否允许多选批量上传
-                            browse_button: id, // 浏览按钮的ID
-                            container: $(this).parent().get(0), //取按钮的上级元素
-                            flash_swf_url: '/assets/libs/plupload/js/Moxie.swf',
-                            silverlight_xap_url: '/assets/libs/plupload/js/Moxie.xap',
-                            drop_element: [id, $(this).data("input-id")],
-                            filters: {
-                                max_file_size: maxsize,
-                                mime_types: mimetype,
-                            },
+                        //后缀特殊处理
+                        mimetype = mimetype.split(",").map(function (k) {
+                            return k.indexOf("/") > -1 ? k : (!k || k === "*" || k.charAt(0) === "." ? k : "." + k);
+                        }).join(",");
+
+                        //最大文件限制转换成mb
+                        var maxFilesize = (function (maxsize) {
+                            var matches = maxsize.toString().match(/^([0-9\.]+)(\w+)$/);
+                            var size = matches ? parseFloat(matches[1]) : parseFloat(maxsize),
+                                unit = matches ? matches[2].toLowerCase() : 'b';
+                            var unitDict = {'b': 0, 'k': 1, 'kb': 1, 'm': 2, 'mb': 2, 'gb': 3, 'g': 3, 'tb': 4, 't': 4};
+                            var y = typeof unitDict[unit] !== 'undefined' ? unitDict[unit] : 0;
+                            var bytes = size * Math.pow(1024, y);
+                            return bytes / Math.pow(1024, 2);
+                        }(maxsize));
+
+                        var options = $("#" + id).data() || {};
+                        delete options.success;
+                        delete options.url;
+                        multipart = $.isArray(multipart) ? {} : multipart;
+
+                        Upload.list[id] = new Dropzone("#" + id, $.extend({
                             url: url,
-                            multipart_params: $.isArray(multipart) ? {} : multipart,
-                            init: {
-                                Init: Upload.events.onInit,
-                                PostInit: Upload.events.onPostInit,
-                                FilesAdded: Upload.events.onFileAdded,
-                                BeforeUpload: Upload.events.onBeforeUpload,
-                                UploadProgress: function (up, file) {
-                                    var button = up.settings.button;
-                                    $(button).prop("disabled", true).html("<i class='fa fa-upload'></i> " + __('Upload') + file.percent + "%");
-                                    Upload.events.onUploadProgress(up, file);
-                                },
-                                FileUploaded: function (up, file, info) {
-                                    var button = up.settings.button;
-                                    //还原按钮文字及状态
-                                    $(button).prop("disabled", false).html($(button).data("bakup-html"));
-                                    var ret = Upload.events.onUploadResponse(info.response, info, up, file);
-                                    file.ret = ret;
-                                    if (ret.code === 1) {
-                                        Upload.events.onUploadSuccess(up, ret, file);
-                                    } else {
-                                        Upload.events.onUploadError(up, ret, file);
+                            params: function (files, xhr, chunk) {
+                                var params = multipart;
+                                if (chunk) {
+                                    return $.extend({}, params, {
+                                        filesize: chunk.file.size,
+                                        filename: chunk.file.name,
+                                        chunkid: chunk.file.upload.uuid,
+                                        chunkindex: chunk.index,
+                                        chunkcount: chunk.file.upload.totalChunkCount,
+                                        chunksize: this.options.chunkSize,
+                                        chunkfilesize: chunk.dataBlock.data.size,
+                                        width: chunk.file.width || 0,
+                                        height: chunk.file.height || 0,
+                                        type: chunk.file.type,
+                                    });
+                                }
+                                return params;
+                            },
+                            maxFilesize: maxFilesize,
+                            acceptedFiles: mimetype,
+                            maxFiles: (maxcount && parseInt(maxcount) > 1 ? maxcount : (multiple ? null : 1)),
+                            previewsContainer: false,
+                            dictDefaultMessage: __("Drop files here to upload"),
+                            dictFallbackMessage: __("Your browser does not support drag'n'drop file uploads"),
+                            dictFallbackText: __("Please use the fallback form below to upload your files like in the olden days"),
+                            dictFileTooBig: __("File is too big (%sMiB), Max filesize: %sMiB", "{{filesize}}", "{{maxFilesize}}"),
+                            dictInvalidFileType: __("You can't upload files of this type"),
+                            dictResponseError: __("Server responded with %s code.", "{{statusCode}}"),
+                            dictCancelUpload: __("Cancel upload"),
+                            dictUploadCanceled: __("Upload canceled"),
+                            dictCancelUploadConfirmation: __("Are you sure you want to cancel this upload?"),
+                            dictRemoveFile: __("Remove file"),
+                            dictMaxFilesExceeded: __("You can only upload a maximum of %s files", "{{maxFiles}}"),
+                            init: function () {
+                                Upload.events.onInit.call(this);
+                                //必须添加dz-message,否则点击icon无法唤起上传窗口
+                                $(">i", this.element).addClass("dz-message");
+                                this.options.elementHtml = $(this.element).html();
+                            },
+                            addedfiles: function (files) {
+                                if (this.options.maxFiles && this.options.maxFiles > 0 && this.options.inputId) {
+                                    var inputObj = $("#" + this.options.inputId);
+                                    if (inputObj.size() > 0) {
+                                        var value = $.trim(inputObj.val());
+                                        var nums = value === '' ? 0 : value.split(/\,/).length;
+                                        var remain = this.options.maxFiles - nums;
+                                        if (remain === 0 || files.length > remain) {
+                                            files = Array.prototype.slice.call(files, remain);
+                                            for (var i = 0; i < files.length; i++) {
+                                                this.removeFile(files[i]);
+                                            }
+                                            Toastr.error(__("You can only upload a maximum of %s files", this.options.maxFiles));
+                                        }
                                     }
-                                },
-                                UploadComplete: Upload.events.onUploadComplete,
-                                Error: function (up, err) {
-                                    var button = up.settings.button;
-                                    $(button).prop("disabled", false).html($(button).data("bakup-html"));
-                                    var ret = {code: err.code, msg: err.message, data: null};
-                                    Upload.events.onUploadError(up, ret);
                                 }
                             },
+                            success: function (file, response) {
+                                var ret = Upload.events.onUploadResponse(response, this, file);
+                                file.ret = ret;
+                                if (ret.code === 1) {
+                                    Upload.events.onUploadSuccess(this, ret, file);
+                                } else {
+                                    Upload.events.onUploadError(this, ret, file);
+                                }
+                            },
+                            error: function (file, response, xhr) {
+                                var ret = {code: 0, data: null, msg: response};
+                                Upload.events.onUploadError(this, ret, file);
+                            },
+                            uploadprogress: function (file, progress, bytesSent) {
+
+                            },
+                            totaluploadprogress: function (progress, bytesSent) {
+                                if (this.getActiveFiles().length > 0) {
+                                    $(this.element).prop("disabled", true).html("<i class='fa fa-upload'></i> " + __('Upload') + Math.floor(progress) + "%");
+                                }
+                            },
+                            queuecomplete: function () {
+                                Upload.events.onUploadComplete(this, this.files);
+                                this.removeAllFiles(true);
+                                $(this.element).prop("disabled", false).html(this.options.elementHtml);
+                            },
+                            chunkSuccess: function (chunk, file, response) {
+                            },
+                            chunksUploaded: function (file, done) {
+                                var that = this;
+                                Fast.api.ajax({
+                                    url: this.options.url,
+                                    data: {
+                                        action: 'merge',
+                                        filesize: file.size,
+                                        filename: file.name,
+                                        chunkid: file.upload.uuid,
+                                        chunkcount: file.upload.totalChunkCount,
+                                    }
+                                }, function (data, ret) {
+                                    done(JSON.stringify(ret));
+                                    return false;
+                                }, function (data, ret) {
+                                    file.accepted = false;
+                                    that._errorProcessing([file], ret.msg);
+                                });
+                            },
                             onUploadSuccess: onUploadSuccess,
                             onUploadError: onUploadError,
                             onUploadComplete: onUploadComplete,
-                            button: that
-                        });
+                        }, Upload.options, options));
 
                         //拖动排序
                         if (preview_id && multiple) {
@@ -335,70 +366,46 @@ define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined,
                             });
                         }
                         if (input_id) {
-                            //粘贴上传
-                            $("body").on('paste', "#" + input_id, function (event) {
-                                var that = this;
-                                var image, pasteEvent;
-                                pasteEvent = event.originalEvent;
-                                if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
-                                    image = Upload.api.getImageFromClipboard(pasteEvent);
-                                    if (image) {
-                                        event.preventDefault();
-                                        var button = $(".plupload[data-input-id='" + $(that).attr("id") + "']");
-                                        Upload.api.send(image, function (data) {
-                                            var urlArr = [];
-                                            if (button && button.data("multiple") && $(that).val() !== '') {
-                                                urlArr.push($(that).val());
-                                            }
-                                            urlArr.push(data.url);
-                                            $(that).val(urlArr.join(",")).trigger("change").trigger("validate");
-                                        });
+                            //粘贴上传、拖拽上传
+                            $("body").on('paste drop', "#" + input_id, function (event) {
+                                var originEvent = event.originalEvent;
+                                var button = $(".plupload[data-input-id='" + $(this).attr("id") + "'],.faupload[data-input-id='" + $(this).attr("id") + "']");
+                                if (event.type === 'paste' && originEvent.clipboardData && originEvent.clipboardData.items) {
+                                    var items = originEvent.clipboardData.items;
+                                    if ((items.length === 1 && items[0].type.indexOf("text") > -1) || (items.length === 2 && items[1].type.indexOf("text") > -1)) {
+
+                                    } else {
+                                        Upload.list[button.attr("id")].paste(originEvent);
+                                        return false;
                                     }
                                 }
-                            });
-                            //拖拽上传
-                            $("body").on('drop', "#" + input_id, function (event) {
-                                var that = this;
-                                var images, pasteEvent;
-                                pasteEvent = event.originalEvent;
-                                if (pasteEvent.dataTransfer && pasteEvent.dataTransfer.files) {
-                                    images = Upload.api.getImageFromDrop(pasteEvent);
-                                    if (images.length > 0) {
-                                        event.preventDefault();
-                                        var button = $(".plupload[data-input-id='" + $(that).attr("id") + "']");
-                                        $.each(images, function (i, image) {
-                                            Upload.api.send(image, function (data) {
-                                                var urlArr = [];
-                                                if (button && button.data("multiple") && $(that).val() !== '') {
-                                                    urlArr.push($(that).val());
-                                                }
-                                                urlArr.push(data.url);
-                                                $(that).val(urlArr.join(",")).trigger("change").trigger("validate");
-                                            });
-                                        });
-                                    }
+                                if (event.type === 'drop' && originEvent.dataTransfer && originEvent.dataTransfer.files) {
+                                    Upload.list[button.attr("id")].drop(originEvent);
+                                    return false;
                                 }
                             });
                         }
-                        Upload.list[id].init();
                     });
                 },
+                /**
+                 * @deprecated Use upload instead.
+                 */
+                plupload: function (element, onUploadSuccess, onUploadError, onUploadComplete) {
+                    return Upload.api.upload(element, onUploadSuccess, onUploadError, onUploadComplete);
+                },
                 // AJAX异步上传
                 send: function (file, onUploadSuccess, onUploadError, onUploadComplete) {
                     var index = Layer.msg(__('Uploading'), {offset: 't', time: 0});
-                    var id = Plupload.guid();
-                    var _onPostInit = Upload.events.onPostInit;
-                    Upload.events.onPostInit = function () {
-                        // 当加载完成后添加文件并上传
-                        Upload.list[id].addFile(file);
-                        //Upload.list[id].start();
-                    };
-                    $('<button type="button" id="' + id + '" class="btn btn-danger hidden plupload" />').appendTo("body");
+                    var id = "dropzone-" + Dropzone.uuidv4();
+                    $('<button type="button" id="' + id + '" class="btn btn-danger hidden faupload" />').appendTo("body");
                     $("#" + id).data("upload-complete", function (files) {
-                        Upload.events.onPostInit = _onPostInit;
                         Layer.close(index);
+                        Upload.list[id].removeAllFiles(true);
                     });
-                    Upload.api.plupload("#" + id, onUploadSuccess, onUploadError, onUploadComplete);
+                    Upload.api.upload("#" + id, onUploadSuccess, onUploadError, onUploadComplete);
+                    setTimeout(function () {
+                        Upload.list[id].addFile(file);
+                    }, 1);
                 },
                 custom: {
                     //自定义上传完成回调
@@ -406,31 +413,6 @@ define(['jquery', 'bootstrap', 'plupload', 'template'], function ($, undefined,
                         console.log(this, response);
                         alert("Custom Callback,Response URL:" + response.url);
                     },
-                },
-                getImageFromClipboard: function (data) {
-                    var i, item;
-                    i = 0;
-                    while (i < data.clipboardData.items.length) {
-                        item = data.clipboardData.items[i];
-                        if (item.type.indexOf("image") !== -1) {
-                            return item.getAsFile() || false;
-                        }
-                        i++;
-                    }
-                    return false;
-                },
-                getImageFromDrop: function (data) {
-                    var i, item, images;
-                    i = 0;
-                    images = [];
-                    while (i < data.dataTransfer.files.length) {
-                        item = data.dataTransfer.files[i];
-                        if (item.type.indexOf("image") !== -1) {
-                            images.push(item);
-                        }
-                        i++;
-                    }
-                    return images;
                 }
             }
         }

+ 34 - 9
public/assets/less/backend.less

@@ -151,10 +151,30 @@ table.table-template {
   overflow: hidden;
 }
 
-.sp_container .msg-box {
-  position: absolute;
-  right: 0;
-  top: 0;
+.sp_container {
+  .msg-box {
+    position: absolute;
+    right: 0;
+    top: 0;
+  }
+
+  .sp_element_box {
+    overflow: unset;
+
+    > li.input_box {
+      position: unset;
+    }
+
+    .msg-box {
+      right: -24px;
+    }
+  }
+}
+
+@media (max-width: 767px) {
+  .sp_container .sp_element_box .msg-box {
+    left: inherit;
+  }
 }
 
 .toast-top-right-index {
@@ -173,6 +193,12 @@ table.table-template {
   padding: 10px 20px;
 }
 
+select.bs-select-hidden, select.selectpicker {
+  display: inherit !important;
+  max-height: 31px;
+  overflow: hidden;
+}
+
 .img-center {
   margin: 0 auto;
   display: inline;
@@ -738,6 +764,7 @@ form.form-horizontal .control-label {
 /*修复nice-validator新版下的一处BUG*/
 .nice-validator {
   input, select, textarea, [contenteditable] {
+    vertical-align: top;
     display: inline-block;
     *display: inline;
     *zoom: 1;
@@ -750,7 +777,7 @@ form.form-horizontal .control-label {
 }
 
 /*预览区域*/
-.plupload-preview {
+.plupload-preview, .faupload-preview {
   padding: 0 10px;
   margin-bottom: 0;
 
@@ -1081,10 +1108,8 @@ table.table-nowrap {
   }
 }
 
-.n-bootstrap {
-  .input-group > .n-right {
-    position: absolute;
-  }
+.input-group > .msg-box.n-right {
+  position: absolute;
 }
 
 @media (min-width: 564px) {

+ 1 - 2
public/assets/less/fastadmin/dropdown.less

@@ -5,8 +5,7 @@
 
 /*Dropdowns in general*/
 .dropdown-menu {
-    box-shadow: none;
-    border-color: #eee;
+    border:none;
     > li > a {
         /*color: #777;*/
     }

+ 18 - 3
public/assets/less/frontend.less

@@ -40,7 +40,7 @@ body {
     border:none;
 }
 .navbar-nav {
-    li > a {
+    > li > a {
         font-size:14px;
     }
 }
@@ -57,7 +57,7 @@ body {
 }
 
 /*预览区域*/
-.plupload-preview {
+.plupload-preview,.faupload-preview {
     padding:0 10px;
     margin-bottom:0;
     li {
@@ -92,6 +92,21 @@ body {
     }
 }
 
+.input-group > .msg-box.n-right {
+    position: absolute;
+}
+
+/*修复radio和checkbox样式对齐*/
+.radio, .checkbox {
+    > label {
+        margin-right: 10px;
+
+        > input {
+            margin: 2px 0 0;
+        }
+    }
+}
+
 #header-navbar li.dropdown ul.dropdown-menu {
     min-width:94px;
 }
@@ -485,4 +500,4 @@ main.content {
     margin-right: 5px;
     text-align: center;
     display: inline-block;
-}
+}