Browse Source

初始化

程旭源 4 years ago
parent
commit
b6cf7d2d53
100 changed files with 9028 additions and 2 deletions
  1. 10 0
      .bowerrc
  2. 11 0
      .env
  3. 11 0
      .env.sample
  4. 2 0
      .gitignore
  5. 1 0
      .htaccess
  6. 26 0
      404.html
  7. 191 0
      LICENSE
  8. 91 2
      README.md
  9. 77 0
      addons/alisms/Alisms.php
  10. 76 0
      addons/alisms/config.php
  11. 52 0
      addons/alisms/controller/Index.php
  12. 8 0
      addons/alisms/info.ini
  13. 169 0
      addons/alisms/library/Alisms.php
  14. 75 0
      addons/alisms/view/index/index.html
  15. 145 0
      addons/blog/Blog.php
  16. 144 0
      addons/blog/application/admin/controller/blog/Ajax.php
  17. 63 0
      addons/blog/application/admin/controller/blog/Block.php
  18. 73 0
      addons/blog/application/admin/controller/blog/Category.php
  19. 63 0
      addons/blog/application/admin/controller/blog/Comment.php
  20. 101 0
      addons/blog/application/admin/controller/blog/Post.php
  21. 13 0
      addons/blog/application/admin/lang/zh-cn/blog/block.php
  22. 17 0
      addons/blog/application/admin/lang/zh-cn/blog/category.php
  23. 18 0
      addons/blog/application/admin/lang/zh-cn/blog/comment.php
  24. 23 0
      addons/blog/application/admin/lang/zh-cn/blog/post.php
  25. 40 0
      addons/blog/application/admin/model/BlogBlock.php
  26. 81 0
      addons/blog/application/admin/model/BlogCategory.php
  27. 39 0
      addons/blog/application/admin/model/BlogComment.php
  28. 105 0
      addons/blog/application/admin/model/BlogPost.php
  29. 66 0
      addons/blog/application/admin/view/blog/block/add.html
  30. 72 0
      addons/blog/application/admin/view/blog/block/edit.html
  31. 36 0
      addons/blog/application/admin/view/blog/block/index.html
  32. 92 0
      addons/blog/application/admin/view/blog/category/add.html
  33. 91 0
      addons/blog/application/admin/view/blog/category/edit.html
  34. 28 0
      addons/blog/application/admin/view/blog/category/index.html
  35. 80 0
      addons/blog/application/admin/view/blog/comment/add.html
  36. 80 0
      addons/blog/application/admin/view/blog/comment/edit.html
  37. 37 0
      addons/blog/application/admin/view/blog/comment/index.html
  38. 130 0
      addons/blog/application/admin/view/blog/post/add.html
  39. 130 0
      addons/blog/application/admin/view/blog/post/edit.html
  40. 37 0
      addons/blog/application/admin/view/blog/post/index.html
  41. 5 0
      addons/blog/assets/default/bootstrap/css/bootstrap-grid.min.css
  42. 6 0
      addons/blog/assets/default/bootstrap/css/bootstrap-reboot.min.css
  43. 5 0
      addons/blog/assets/default/bootstrap/css/bootstrap.min.css
  44. 5 0
      addons/blog/assets/default/bootstrap/js/bootstrap.bundle.min.js
  45. 5 0
      addons/blog/assets/default/bootstrap/js/bootstrap.min.js
  46. 754 0
      addons/blog/assets/default/css/common.css
  47. BIN
      addons/blog/assets/default/example/12.jpg
  48. BIN
      addons/blog/assets/default/example/13.jpg
  49. BIN
      addons/blog/assets/default/example/15.jpg
  50. BIN
      addons/blog/assets/default/example/16.jpg
  51. BIN
      addons/blog/assets/default/example/21.jpg
  52. 71 0
      addons/blog/assets/default/js/common.js
  53. 215 0
      addons/blog/assets/default/js/jquery.autocomplete.js
  54. 152 0
      addons/blog/assets/default/js/post.js
  55. BIN
      addons/blog/assets/img/avatar.png
  56. BIN
      addons/blog/assets/img/logo.jpg
  57. BIN
      addons/blog/assets/img/qrcode.png
  58. BIN
      addons/blog/assets/img/thumb.png
  59. 384 0
      addons/blog/config.php
  60. 20 0
      addons/blog/controller/Ajax.php
  61. 30 0
      addons/blog/controller/Archive.php
  62. 29 0
      addons/blog/controller/Base.php
  63. 53 0
      addons/blog/controller/Category.php
  64. 73 0
      addons/blog/controller/Comment.php
  65. 38 0
      addons/blog/controller/Index.php
  66. 47 0
      addons/blog/controller/Post.php
  67. 140 0
      addons/blog/controller/Search.php
  68. 43 0
      addons/blog/controller/Sitemap.php
  69. 42 0
      addons/blog/controller/wxapp/Archive.php
  70. 23 0
      addons/blog/controller/wxapp/Base.php
  71. 61 0
      addons/blog/controller/wxapp/Comment.php
  72. 46 0
      addons/blog/controller/wxapp/Common.php
  73. 57 0
      addons/blog/controller/wxapp/Index.php
  74. 28 0
      addons/blog/controller/wxapp/Page.php
  75. 70 0
      addons/blog/controller/wxapp/Post.php
  76. 1 0
      addons/blog/data/words.dic
  77. 8 0
      addons/blog/info.ini
  78. 135 0
      addons/blog/install.sql
  79. 13 0
      addons/blog/lang/zh-cn.php
  80. 215 0
      addons/blog/library/Bootstrap.php
  81. 18 0
      addons/blog/library/CommentException.php
  82. 133 0
      addons/blog/library/FulltextSearch.php
  83. 180 0
      addons/blog/library/HashMap.php
  84. 296 0
      addons/blog/library/SensitiveHelper.php
  85. 59 0
      addons/blog/library/Service.php
  86. 27 0
      addons/blog/library/aip/AipContentCensor.php
  87. 224 0
      addons/blog/library/aip/AipImageCensor.php
  88. 429 0
      addons/blog/library/aip/AipNlp.php
  89. 628 0
      addons/blog/library/aip/AipOcr.php
  90. 401 0
      addons/blog/library/aip/lib/AipBase.php
  91. 227 0
      addons/blog/library/aip/lib/AipHttpClient.php
  92. 181 0
      addons/blog/library/aip/lib/AipHttpUtil.php
  93. 181 0
      addons/blog/library/aip/lib/AipSampleSigner.php
  94. 19 0
      addons/blog/library/aip/lib/AipSignOption.php
  95. 109 0
      addons/blog/model/Block.php
  96. 47 0
      addons/blog/model/Category.php
  97. 173 0
      addons/blog/model/Comment.php
  98. 158 0
      addons/blog/model/Post.php
  99. 58 0
      addons/blog/public/assets/js/backend/blog/block.js
  100. 102 0
      addons/blog/public/assets/js/backend/blog/category.js

+ 10 - 0
.bowerrc

@@ -0,0 +1,10 @@
+{
+  "directory": "public/assets/libs",
+  "ignoredDependencies": [
+    "es6-promise",
+    "file-saver",
+    "html2canvas",
+    "jspdf",
+    "jspdf-autotable"
+  ]
+}

+ 11 - 0
.env

@@ -0,0 +1,11 @@
+[app]
+debug = false
+trace = false
+
+[database]
+hostname = 127.0.0.1
+database = proxy_wxvpn_cn
+username = proxy_wxvpn_cn
+password = PijtXhjPZC6bnyjb
+hostport = 3306
+prefix = fa_

+ 11 - 0
.env.sample

@@ -0,0 +1,11 @@
+[app]
+debug = false
+trace = false
+
+[database]
+hostname = 127.0.0.1
+database = fastadmin
+username = root
+password = root
+hostport = 3306
+prefix = fa_

+ 2 - 0
.gitignore

@@ -62,3 +62,5 @@ fabric.properties
 
 # Sonarlint plugin
 .idea/sonarlint
+vendor
+.idea

+ 1 - 0
.htaccess

@@ -0,0 +1 @@
+ 

+ 26 - 0
404.html

@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+<title>404</title>
+<style>
+	body{
+		background-color:#444;
+		font-size:14px;
+	}
+	h3{
+		font-size:60px;
+		color:#eee;
+		text-align:center;
+		padding-top:30px;
+		font-weight:normal;
+	}
+</style>
+</head>
+
+<body>
+<h3>404,您请求的文件不存在!</h3>
+</body>
+</html>

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "{}" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright 2017 Karson
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 91 - 2
README.md

@@ -1,3 +1,92 @@
-# switch
+FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架。
 
-switch 加速
+
+## 主要特性
+
+* 基于`Auth`验证的权限管理系统
+    * 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
+    * 支持单管理员多角色
+    * 支持管理子级数据或个人数据
+* 强大的一键生成功能
+    * 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
+    * 一键压缩打包JS和CSS文件,一键CDN静态资源部署
+    * 一键生成控制器菜单和规则
+    * 一键生成API接口文档
+* 完善的前端功能组件开发
+    * 基于`AdminLTE`二次开发
+    * 基于`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、博客、知识付费问答无缝整合[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、又拍云)功能
+* 第三方富文本编辑器支持(Summernote、Kindeditor、百度编辑器)
+* 第三方登录(QQ、微信、微博)整合
+* 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
+* 丰富的插件应用市场
+
+## 安装使用
+
+https://doc.fastadmin.net
+
+## 在线演示
+
+https://demo.fastadmin.net
+
+用户名:admin
+
+密 码:123456
+
+提 示:演示站数据无法进行修改,请下载源码安装体验全部功能
+
+## 界面截图
+![控制台](https://gitee.com/uploads/images/2017/0411/113717_e99ff3e7_10933.png "控制台")
+
+## 问题反馈
+
+在使用中有任何问题,请使用以下联系方式联系我们
+
+交流社区: https://ask.fastadmin.net
+
+QQ群: [636393962](https://jq.qq.com/?_wv=1027&k=487PNBb)(满) [708784003](https://jq.qq.com/?_wv=1027&k=5ObjtwM)(满) [964776039](https://jq.qq.com/?_wv=1027&k=59qjU2P)(3群) [749803490](https://jq.qq.com/?_wv=1027&k=5tczi88)(满) [767103006](https://jq.qq.com/?_wv=1027&k=5Z1U751)(满) [675115483](https://jq.qq.com/?_wv=1027&k=54I6mts)(6群)
+
+Github: https://github.com/karsonzhang/fastadmin
+
+Gitee: https://gitee.com/karson/fastadmin
+
+## 特别鸣谢
+
+感谢以下的项目,排名不分先后
+
+ThinkPHP:http://www.thinkphp.cn
+
+AdminLTE:https://adminlte.io
+
+Bootstrap:http://getbootstrap.com
+
+jQuery:http://jquery.com
+
+Bootstrap-table:https://github.com/wenzhixin/bootstrap-table
+
+Nice-validator: https://validator.niceue.com
+
+SelectPage: https://github.com/TerryZ/SelectPage
+
+
+## 版权信息
+
+FastAdmin遵循Apache2开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2017-2020 by FastAdmin (https://www.fastadmin.net)
+
+All rights reserved。

+ 77 - 0
addons/alisms/Alisms.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace addons\alisms;
+
+use think\Addons;
+
+/**
+ * Alisms
+ */
+class Alisms extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+    /**
+     * 短信发送行为
+     * @param   $params
+     * @return  boolean
+     */
+    public function smsSend(&$params)
+    {
+        $config = get_addon_config('alisms');
+        $alisms = new library\Alisms();
+        $result = $alisms->mobile($params->mobile)
+            ->template($config['template'][$params->event])
+            ->param(['code' => $params->code])
+            ->send();
+        return $result;
+    }
+
+    /**
+     * 短信发送通知
+     * @param   $params
+     * @return  boolean
+     */
+    public function smsNotice(&$params)
+    {
+        $alisms = library\Alisms::instance();
+        if (is_array($params['msg'])) {
+            $param = $params['msg'];
+        } else {
+            parse_str($params['msg'], $param);
+        }
+        $param = $param ? $param : [];
+        $result = $alisms->mobile($params['mobile'])
+            ->template($params['template'])
+            ->param($param)
+            ->send();
+        return $result;
+    }
+
+    /**
+     * 检测验证是否正确
+     * @param   $params
+     * @return  boolean
+     */
+    public function smsCheck(&$params)
+    {
+        return true;
+    }
+}

+ 76 - 0
addons/alisms/config.php

@@ -0,0 +1,76 @@
+<?php
+
+return array(
+    array(
+        'name'    => 'key',
+        'title'   => '应用key',
+        'type'    => 'string',
+        'content' =>
+            array(),
+        'value'   => 'your key',
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '',
+        'ok'      => '',
+        'extend'  => '',
+    ),
+    array(
+        'name'    => 'secret',
+        'title'   => '密钥secret',
+        'type'    => 'string',
+        'content' =>
+            array(),
+        'value'   => 'your secret',
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '',
+        'ok'      => '',
+        'extend'  => '',
+    ),
+    array(
+        'name'    => 'sign',
+        'title'   => '签名',
+        'type'    => 'string',
+        'content' =>
+            array(),
+        'value'   => 'your sign',
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '',
+        'ok'      => '',
+        'extend'  => '',
+    ),
+    array(
+        'name'    => 'template',
+        'title'   => '短信模板',
+        'type'    => 'array',
+        'content' =>
+            array(),
+        'value'   =>
+            array(
+                'register'     => 'SMS_114000000',
+                'resetpwd'     => 'SMS_114000000',
+                'changepwd'    => 'SMS_114000000',
+                'changemobile' => 'SMS_114000000',
+                'profile'      => 'SMS_114000000',
+            ),
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '',
+        'ok'      => '',
+        'extend'  => '',
+    ),
+    array(
+        'name'    => '__tips__',
+        'title'   => '温馨提示',
+        'type'    => 'string',
+        'content' =>
+            array(),
+        'value'   => '应用key和密钥你可以通过 https://ak-console.aliyun.com/?spm=a2c4g.11186623.2.13.fd315777PX3tjy#/accesskey 获取',
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '',
+        'ok'      => '',
+        'extend'  => '',
+    ),
+);

+ 52 - 0
addons/alisms/controller/Index.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace addons\alisms\controller;
+
+use think\addons\Controller;
+
+/**
+ * 阿里短信
+ */
+class Index extends Controller
+{
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        if (!\app\admin\library\Auth::instance()->id) {
+            $this->error('暂无权限浏览');
+        }
+        parent::_initialize();
+    }
+
+    public function index()
+    {
+        return $this->view->fetch();
+    }
+
+    public function send()
+    {
+        $config = get_addon_config('alisms');
+        $mobile = $this->request->post('mobile');
+        $template = $this->request->post('template');
+        $sign = $this->request->post('sign');
+        if (!$mobile || !$template) {
+            $this->error('手机号、模板ID不能为空');
+        }
+        $sign = $sign ? $sign : $config['sign'];
+        $param = (array)json_decode($this->request->post('param', '', 'trim'));
+        $alisms = new \addons\alisms\library\Alisms();
+        $ret = $alisms->mobile($mobile)
+            ->template($template)
+            ->sign($sign)
+            ->param($param)
+            ->send();
+        if ($ret) {
+            $this->success("发送成功");
+        } else {
+            $this->error("发送失败!失败原因:" . $alisms->getError());
+        }
+    }
+
+}

+ 8 - 0
addons/alisms/info.ini

@@ -0,0 +1,8 @@
+name = alisms
+title = 阿里短信发送
+intro = 阿里短信发送插件
+author = Karson
+website = https://www.fastadmin.net
+version = 1.0.6
+state = 0
+url = /addons/alisms

+ 169 - 0
addons/alisms/library/Alisms.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace addons\alisms\library;
+
+/**
+ * 阿里大于SMS短信发送
+ */
+class Alisms
+{
+    private $_params = [];
+    public $error = '';
+    protected $config = [];
+    protected static $instance;
+
+    public function __construct($options = [])
+    {
+        if ($config = get_addon_config('alisms')) {
+            $this->config = array_merge($this->config, $config);
+        }
+        $this->config = array_merge($this->config, is_array($options) ? $options : []);
+    }
+
+    /**
+     * 单例
+     * @param array $options 参数
+     * @return Alisms
+     */
+    public static function instance($options = [])
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new static($options);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 设置签名
+     * @param string $sign
+     * @return Alisms
+     */
+    public function sign($sign = '')
+    {
+        $this->_params['SignName'] = $sign;
+        return $this;
+    }
+
+    /**
+     * 设置参数
+     * @param array $param
+     * @return Alisms
+     */
+    public function param(array $param = [])
+    {
+        foreach ($param as $k => &$v) {
+            $v = (string)$v;
+        }
+        unset($v);
+        $this->_params['TemplateParam'] = json_encode($param);
+        return $this;
+    }
+
+    /**
+     * 设置模板
+     * @param string $code 短信模板
+     * @return Alisms
+     */
+    public function template($code = '')
+    {
+        $this->_params['TemplateCode'] = $code;
+        return $this;
+    }
+
+    /**
+     * 接收手机
+     * @param string $mobile 手机号码
+     * @return Alisms
+     */
+    public function mobile($mobile = '')
+    {
+        $this->_params['PhoneNumbers'] = $mobile;
+        return $this;
+    }
+
+    /**
+     * 立即发送
+     * @return boolean
+     */
+    public function send()
+    {
+        $this->error = '';
+        $params = $this->_params();
+        $params['Signature'] = $this->_signed($params);
+        $response = $this->_curl($params);
+        if ($response !== false) {
+            $res = (array)json_decode($response, true);
+            if (isset($res['Code']) && $res['Code'] == 'OK') {
+                return true;
+            }
+            $this->error = isset($res['Message']) ? $res['Message'] : 'InvalidResult';
+        } else {
+            $this->error = 'InvalidResult';
+        }
+        return false;
+    }
+
+    /**
+     * 获取错误信息
+     * @return string
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    private function _params()
+    {
+        return array_merge([
+            'AccessKeyId'      => $this->config['key'],
+            'SignName'         => isset($this->config['sign']) ? $this->config['sign'] : '',
+            'Action'           => 'SendSms',
+            'Format'           => 'JSON',
+            'Version'          => '2017-05-25',
+            'SignatureVersion' => '1.0',
+            'SignatureMethod'  => 'HMAC-SHA1',
+            'SignatureNonce'   => uniqid(),
+            'Timestamp'        => gmdate('Y-m-d\TH:i:s\Z'),
+        ], $this->_params);
+    }
+
+    private function percentEncode($string)
+    {
+        $string = urlencode($string);
+        $string = preg_replace('/\+/', '%20', $string);
+        $string = preg_replace('/\*/', '%2A', $string);
+        $string = preg_replace('/%7E/', '~', $string);
+        return $string;
+    }
+
+    private function _signed($params)
+    {
+        $sign = $this->config['secret'];
+        ksort($params);
+        $canonicalizedQueryString = '';
+        foreach ($params as $key => $value) {
+            $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value);
+        }
+        $stringToSign = 'GET&%2F&' . $this->percentencode(substr($canonicalizedQueryString, 1));
+        $signature = base64_encode(hash_hmac('sha1', $stringToSign, $sign . '&', true));
+        return $signature;
+    }
+
+    private function _curl($params)
+    {
+        $uri = 'http://dysmsapi.aliyuncs.com/?' . http_build_query($params);
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+        curl_setopt($ch, CURLOPT_URL, $uri);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.98 Safari/537.36");
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+        $reponse = curl_exec($ch);
+        curl_close($ch);
+        return $reponse;
+    }
+}

+ 75 - 0
addons/alisms/view/index/index.html

@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
+    <title>阿里云通信短信发送示例 - FastAdmin</title>
+
+    <!-- Bootstrap Core CSS -->
+    <link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
+
+    <!-- Custom CSS -->
+    <link href="__CDN__/assets/css/frontend.css" rel="stylesheet">
+
+    <!-- Plugin CSS -->
+    <link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
+    <link href="https://cdn.staticfile.org/simple-line-icons/2.4.1/css/simple-line-icons.min.css" rel="stylesheet">
+
+    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
+    <!--[if lt IE 9]>
+    <script src="https://cdn.staticfile.org/html5shiv/3.7.3/html5shiv.min.js"></script>
+    <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
+    <![endif]-->
+</head>
+<body>
+<div class="container">
+    <div class="well" style="margin-top:30px;">
+        <form class="form-horizontal" action="{:addon_url('alisms/index/send')}" method="POST">
+            <fieldset>
+                <legend>阿里云通信短信发送</legend>
+                <div class="form-group">
+                    <label class="col-lg-2 control-label">手机号</label>
+                    <div class="col-lg-10">
+                        <input type="text" class="form-control" name="mobile" placeholder="手机号">
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="col-lg-2 control-label">消息模板ID</label>
+                    <div class="col-lg-10">
+                        <input type="text" class="form-control" name="template" placeholder="消息模板ID,从阿里云通信获得,通常是:SMS_114000000这种格式">
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="col-lg-2 control-label">签名</label>
+                    <div class="col-lg-10">
+                        <input type="text" class="form-control" name="sign" placeholder="消息模板(可以留空,留空使用后台插件管理中的配置)">
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="col-lg-2 control-label">模板变量参数</label>
+                    <div class="col-lg-10">
+                        <textarea name="param" class="form-control" cols="30" rows="10" placeholder='必须是JSON字符串,如{"name":"李明"}'></textarea>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <div class="col-lg-10 col-lg-offset-2">
+                        <button type="submit" class="btn btn-primary">发送</button>
+                        <button type="reset" class="btn btn-default">重置</button>
+                    </div>
+                </div>
+            </fieldset>
+        </form>
+    </div>
+</div>
+<!-- jQuery -->
+<script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
+
+<!-- Bootstrap Core JavaScript -->
+<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
+
+<script type="text/javascript">
+    $(function () {
+
+    });
+</script>
+</body>
+</html>

+ 145 - 0
addons/blog/Blog.php

@@ -0,0 +1,145 @@
+<?php
+
+namespace addons\blog;
+
+use addons\blog\library\FulltextSearch;
+use app\common\library\Menu;
+use think\Addons;
+
+/**
+ * 博客插件
+ */
+class Blog extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        $menu = [
+            [
+                'name'    => 'blog',
+                'title'   => '博客管理',
+                'sublist' => [
+                    [
+                        'name'    => 'blog/post',
+                        'title'   => '日志管理',
+                        'sublist' => [
+                            ['name' => 'blog/post/index', 'title' => '查看'],
+                            ['name' => 'blog/post/add', 'title' => '添加'],
+                            ['name' => 'blog/post/edit', 'title' => '修改'],
+                            ['name' => 'blog/post/del', 'title' => '删除'],
+                            ['name' => 'blog/post/multi', 'title' => '批量更新'],
+                        ]
+                    ],
+                    [
+                        'name'    => 'blog/category',
+                        'title'   => '分类管理',
+                        'sublist' => [
+                            ['name' => 'blog/category/index', 'title' => '查看'],
+                            ['name' => 'blog/category/add', 'title' => '添加'],
+                            ['name' => 'blog/category/edit', 'title' => '修改'],
+                            ['name' => 'blog/category/del', 'title' => '删除'],
+                            ['name' => 'blog/category/multi', 'title' => '批量更新'],
+                        ]
+                    ],
+                    [
+                        'name'    => 'blog/comment',
+                        'title'   => '评论管理',
+                        'icon'    => 'fa fa-comment',
+                        'sublist' => [
+                            ['name' => 'blog/comment/index', 'title' => '查看'],
+                            ['name' => 'blog/comment/add', 'title' => '添加'],
+                            ['name' => 'blog/comment/edit', 'title' => '修改'],
+                            ['name' => 'blog/comment/del', 'title' => '删除'],
+                            ['name' => 'blog/comment/multi', 'title' => '批量更新'],
+                        ]
+                    ],
+                    [
+                        'name'    => 'blog/block',
+                        'title'   => '区块管理',
+                        'icon'    => 'fa fa-th-large',
+                        'sublist' => [
+                            ['name' => 'blog/block/index', 'title' => '查看'],
+                            ['name' => 'blog/block/add', 'title' => '添加'],
+                            ['name' => 'blog/block/edit', 'title' => '修改'],
+                            ['name' => 'blog/block/del', 'title' => '删除'],
+                            ['name' => 'blog/block/multi', 'title' => '批量更新'],
+                        ]
+                    ]
+                ]
+            ]
+        ];
+        Menu::create($menu);
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete('blog');
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     */
+    public function enable()
+    {
+        Menu::enable('blog');
+    }
+
+    /**
+     * 插件禁用方法
+     */
+    public function disable()
+    {
+        Menu::disable('blog');
+    }
+
+    public function xunsearchConfigInit()
+    {
+        return FulltextSearch::config();
+    }
+
+    public function xunsearchIndexReset($project)
+    {
+        if (!$project['isaddon'] || $project['name'] != 'blog') {
+            return;
+        }
+        return FulltextSearch::reset();
+    }
+
+    /**
+     * 脚本替换
+     */
+    public function viewFilter(& $content)
+    {
+        $request = \think\Request::instance();
+        $dispatch = $request->dispatch();
+
+        if ($request->module() || !isset($dispatch['method'][0]) || $dispatch['method'][0] != '\think\addons\Route') {
+            return;
+        }
+        $addon = isset($dispatch['var']['addon']) ? $dispatch['var']['addon'] : $request->param('addon');
+        if ($addon != 'blog') {
+            return;
+        }
+        $style = '';
+        $script = '';
+        $result = preg_replace_callback("/<(script|style)\s(data\-render=\"(script|style)\")([\s\S]*?)>([\s\S]*?)<\/(script|style)>/i", function ($match) use (&$style, &$script) {
+            if (isset($match[1]) && in_array($match[1], ['style', 'script'])) {
+                ${$match[1]} .= str_replace($match[2], '', $match[0]);
+            }
+            return '';
+        }, $content);
+        $content = preg_replace_callback('/^\s+(\{__STYLE__\}|\{__SCRIPT__\})\s+$/m', function ($matches) use ($style, $script) {
+            return $matches[1] == '{__STYLE__}' ? $style : $script;
+        }, $result ? $result : $content);
+    }
+}

+ 144 - 0
addons/blog/application/admin/controller/blog/Ajax.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace app\admin\controller\blog;
+
+use addons\blog\library\aip\AipContentCensor;
+use addons\blog\library\SensitiveHelper;
+use addons\blog\library\Service;
+use app\common\controller\Backend;
+
+/**
+ * Ajax
+ *
+ * @icon fa fa-circle-o
+ * @internal
+ */
+class Ajax extends Backend
+{
+
+    /**
+     * 模型对象
+     */
+    protected $model = null;
+    protected $noNeedRight = ['*'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+    }
+
+    /**
+     * 获取模板列表
+     * @internal
+     */
+    public function get_template_list()
+    {
+        $files = [];
+        $keyValue = $this->request->request("keyValue");
+        if (!$keyValue) {
+            $type = $this->request->request("type");
+            $name = $this->request->request("name");
+            if ($name) {
+                //$files[] = ['name' => $name . '.html'];
+            }
+            //设置过滤方法
+            $this->request->filter(['strip_tags']);
+            $config = get_addon_config('blog');
+            $themeDir = ADDON_PATH . 'blog' . DS . 'view' . DS . $config['theme'] . DS;
+            $dh = opendir($themeDir);
+            while (false !== ($filename = readdir($dh))) {
+                if ($filename == '.' || $filename == '..') {
+                    continue;
+                }
+                if ($type) {
+                    $rule = $type == 'channel' ? '(channel|list)' : $type;
+                    if (!preg_match("/^{$rule}(.*)/i", $filename)) {
+                        continue;
+                    }
+                }
+                $files[] = ['name' => $filename];
+            }
+        } else {
+            $files[] = ['name' => $keyValue];
+        }
+        return $result = ['total' => count($files), 'list' => $files];
+    }
+
+    /**
+     * 检查内容是否包含违禁词
+     * @throws \Exception
+     */
+    public function check_content_islegal()
+    {
+        $config = get_addon_config('blog');
+        $content = $this->request->post('content');
+        if (!$content) {
+            $this->error(__('Please input your content'));
+        }
+        if ($config['audittype'] == 'local') {
+            // 敏感词过滤
+            $handle = SensitiveHelper::init()->setTreeByFile(ADDON_PATH . 'blog/data/words.dic');
+            //首先检测是否合法
+            $arr = $handle->getBadWord($content);
+            if ($arr) {
+                $this->error(__('The content is not legal'), null, $arr);
+            } else {
+                $this->success(__('The content is legal'));
+            }
+        } else {
+            $client = new AipContentCensor($config['aip_appid'], $config['aip_apikey'], $config['aip_secretkey']);
+            $result = $client->antiSpam($content);
+            if (isset($result['result']) && $result['result']['spam'] > 0) {
+                $arr = [];
+                foreach (array_merge($result['result']['review'], $result['result']['reject']) as $index => $item) {
+                    $arr[] = $item['hit'];
+                }
+                $this->error(__('The content is not legal'), null, $arr);
+            } else {
+                $this->success(__('The content is legal'));
+            }
+        }
+    }
+
+    /**
+     * 获取关键字
+     * @throws \Exception
+     */
+    public function get_content_keywords()
+    {
+        $config = get_addon_config('blog');
+        $title = $this->request->post('title');
+        $tags = $this->request->post('tags', '');
+        $content = $this->request->post('content');
+        if (!$content) {
+            $this->error(__('Please input your content'));
+        }
+        $keywords = Service::getContentTags($title);
+        $keywords = in_array($title, $keywords) ? [] : $keywords;
+        $keywords = array_filter(array_merge([$tags], $keywords));
+        $description = mb_substr(strip_tags($content), 0, 200);
+        $data = [
+            "keywords"    => implode(',', $keywords),
+            "description" => $description
+        ];
+        $this->success("提取成功", null, $data);
+    }
+
+    /**
+     * 获取标题拼音
+     */
+    public function get_title_pinyin()
+    {
+        $config = get_addon_config('blog');
+        $title = $this->request->post("title");
+        //分隔符
+        $delimiter = $this->request->post("delimiter", "");
+        $pinyin = new \Overtrue\Pinyin\Pinyin('Overtrue\Pinyin\MemoryFileDictLoader');
+        if ($title) {
+            $result = $pinyin->permalink($title, $delimiter);
+            $this->success("", null, ['pinyin' => $result]);
+        } else {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+    }
+}

+ 63 - 0
addons/blog/application/admin/controller/blog/Block.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace app\admin\controller\blog;
+
+use app\common\controller\Backend;
+
+/**
+ * 区块表
+ *
+ * @icon fa fa-th-large
+ */
+class Block extends Backend
+{
+
+    /**
+     * Block模型对象
+     */
+    protected $model = null;
+    protected $noNeedRight = ['selectpage_type'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\BlogBlock;
+        $this->view->assign("statusList", $this->model->getStatusList());
+    }
+
+    public function index()
+    {
+        $typeArr = \app\admin\model\BlogBlock::distinct('type')->column('type');
+        $this->view->assign('typeList', $typeArr);
+        $this->assignconfig('typeList', $typeArr);
+        return parent::index();
+    }
+
+    public function selectpage_type()
+    {
+        $list = [];
+        $word = (array)$this->request->request("q_word/a");
+        $field = $this->request->request('showField');
+        $keyValue = $this->request->request('keyValue');
+        if (!$keyValue) {
+            if (array_filter($word)) {
+                foreach ($word as $k => $v) {
+                    $list[] = ['id' => $v, $field => $v];
+                }
+            }
+            $typeArr = \app\admin\model\BlogBlock::column('type');
+            $typeArr = array_unique($typeArr);
+            foreach ($typeArr as $index => $item) {
+                $list[] = ['id' => $item, $field => $item];
+            }
+        } else {
+            $list[] = ['id' => $keyValue, $field => $keyValue];
+        }
+        return json(['total' => count($list), 'list' => $list]);
+    }
+
+    public function import()
+    {
+        return parent::import();
+    }
+}

+ 73 - 0
addons/blog/application/admin/controller/blog/Category.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace app\admin\controller\blog;
+
+use app\common\controller\Backend;
+use fast\Tree;
+use think\Controller;
+use think\Request;
+
+/**
+ * 博客分类管理
+ *
+ * @icon fa fa-circle-o
+ */
+class Category extends Backend
+{
+
+    protected $noNeedRight = ['selectpage', 'check_element_available'];
+
+    /**
+     * BlogCategory模型对象
+     */
+    protected $model = null;
+    protected $categorylist = [];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('BlogCategory');
+        $this->view->assign("flagList", $this->model->getFlagList());
+        $this->view->assign("statusList", $this->model->getStatusList());
+
+        $tree = Tree::instance();
+        $tree->init(collection($this->model->order('weigh desc,id desc')->select())->toArray(), 'pid');
+        $this->categorylist = $tree->getTreeList($tree->getTreeArray(0), 'name');
+        $categorydata = [0 => ['name' => __('None')]];
+        foreach ($this->categorylist as $k => $v) {
+            $categorydata[$v['id']] = $v;
+        }
+        $this->view->assign("parentList", $categorydata);
+
+    }
+
+    public function selectpage()
+    {
+        return parent::selectpage();
+    }
+
+    /**
+     * 检测元素是否可用
+     * @internal
+     */
+    public function check_element_available()
+    {
+        $id = $this->request->request('id');
+        $name = $this->request->request('name');
+        $value = $this->request->request('value');
+        $name = substr($name, 4, -1);
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if ($id) {
+            $this->model->where('id', '<>', $id);
+        }
+        $exist = $this->model->where($name, $value)->find();
+        if ($exist) {
+            $this->error(__('The data already exist'));
+        } else {
+            $this->success();
+        }
+    }
+
+}

+ 63 - 0
addons/blog/application/admin/controller/blog/Comment.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace app\admin\controller\blog;
+
+use app\common\controller\Backend;
+
+use think\Controller;
+use think\Request;
+
+/**
+ * 博客评论管理
+ *
+ * @icon fa fa-circle-o
+ */
+class Comment extends Backend
+{
+
+    /**
+     * BlogComment模型对象
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('BlogComment');
+        $this->view->assign("statusList", $this->model->getStatusList());
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            $this->relationSearch = TRUE;
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+
+            $total = $this->model
+                ->with('post')
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+            $list = $this->model
+                ->with('post')
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+}

+ 101 - 0
addons/blog/application/admin/controller/blog/Post.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace app\admin\controller\blog;
+
+use app\admin\model\BlogCategory;
+use app\common\controller\Backend;
+use fast\Tree;
+use think\Controller;
+use think\Request;
+
+/**
+ * 文章管理
+ *
+ * @icon fa fa-circle-o
+ */
+class Post extends Backend
+{
+
+    /**
+     * BlogPost模型对象
+     */
+    protected $model = null;
+    protected $categorylist = [];
+
+    protected $noNeedRight = ['selectpage', 'check_element_available'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('BlogPost');
+        $this->view->assign("flagList", $this->model->getFlagList());
+        $this->view->assign("statusList", $this->model->getStatusList());
+        $tree = Tree::instance();
+        $tree->init(collection(BlogCategory::order('weigh desc,id desc')->select())->toArray(), 'pid');
+        $this->categorylist = $tree->getTreeList($tree->getTreeArray(0), 'name');
+        $categorydata = [];
+        foreach ($this->categorylist as $k => $v) {
+            $categorydata[$v['id']] = $v;
+        }
+        $this->view->assign("categoryList", $categorydata);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            $this->relationSearch = true;
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+
+            $total = $this->model
+                ->with('category')
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+            $list = $this->model
+                ->with('category')
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 检测元素是否可用
+     * @internal
+     */
+    public function check_element_available()
+    {
+        $id = $this->request->request('id');
+        $name = $this->request->request('name');
+        $value = $this->request->request('value');
+        $name = substr($name, 4, -1);
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if ($id) {
+            $this->model->where('id', '<>', $id);
+        }
+        $exist = $this->model->where($name, $value)->find();
+        if ($exist) {
+            $this->error(__('The data already exist'));
+        } else {
+            $this->success();
+        }
+    }
+
+}

+ 13 - 0
addons/blog/application/admin/lang/zh-cn/blog/block.php

@@ -0,0 +1,13 @@
+<?php
+
+return [
+    'Type'       => '类型',
+    'Name'       => '名称',
+    'Title'      => '标题',
+    'Image'      => '图片',
+    'Url'        => '链接',
+    'Content'    => '内容',
+    'Createtime' => '添加时间',
+    'Updatetime' => '更新时间',
+    'Status'     => '状态'
+];

+ 17 - 0
addons/blog/application/admin/lang/zh-cn/blog/category.php

@@ -0,0 +1,17 @@
+<?php
+
+return [
+    'Pid'                    => '父分类ID',
+    'Name'                   => '分类名称',
+    'Nickname'               => '分类昵称',
+    'Flag'                   => '分类标志',
+    'Image'                  => '图片',
+    'Keywords'               => '关键字',
+    'Description'            => '描述',
+    'Diyname'                => '自定义名称',
+    'Createtime'             => '创建时间',
+    'Updatetime'             => '更新时间',
+    'Weigh'                  => '权重',
+    'Status'                 => '状态',
+    'The data already exist' => '已经存在',
+];

+ 18 - 0
addons/blog/application/admin/lang/zh-cn/blog/comment.php

@@ -0,0 +1,18 @@
+<?php
+
+return [
+    'Id'         => 'ID',
+    'Post'       => '日志',
+    'Pid'        => '父评论ID',
+    'Username'   => '用户名',
+    'Email'      => '邮箱',
+    'Website'    => '网址',
+    'Content'    => '内容',
+    'Comments'   => '评论数',
+    'Ip'         => 'IP',
+    'Useragent'  => '浏览器',
+    'Subscribe'  => '订阅',
+    'Createtime' => '创建时间',
+    'Updatetime' => '更新时间',
+    'Status'     => '状态'
+];

+ 23 - 0
addons/blog/application/admin/lang/zh-cn/blog/post.php

@@ -0,0 +1,23 @@
+<?php
+
+return [
+    'Id'                     => 'ID',
+    'Category'               => '分类',
+    'Flag'                   => '标志',
+    'Title'                  => '标题',
+    'Seotitle'               => 'SEO标题',
+    'Summary'                => '摘要',
+    'Content'                => '内容',
+    'Thumb'                  => '缩略图',
+    'Image'                  => '大图',
+    'Diyname'                => '自定义名称',
+    'Keywords'               => '关键字',
+    'Description'            => '描述',
+    'Views'                  => '点击',
+    'Comments'               => '评论数',
+    'Createtime'             => '创建时间',
+    'Updatetime'             => '更新时间',
+    'Weigh'                  => '权重',
+    'Status'                 => '状态',
+    'The data already exist' => '已经存在',
+];

+ 40 - 0
addons/blog/application/admin/model/BlogBlock.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class BlogBlock extends Model
+{
+
+    // 表名
+    protected $name = 'blog_block';
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+        'status_text'
+    ];
+
+    protected static function init()
+    {
+        self::afterInsert(function ($row) {
+            $row->save(['weigh' => $row['id']]);
+        });
+    }
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['status'];
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+}

+ 81 - 0
addons/blog/application/admin/model/BlogCategory.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace app\admin\model;
+
+use addons\blog\library\FulltextSearch;
+use think\Model;
+
+class BlogCategory extends Model
+{
+
+    // 表名
+    protected $name = 'blog_category';
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+        'url',
+        'fullurl',
+        'flag_text',
+        'status_text'
+    ];
+
+    protected static function init()
+    {
+        $config = get_addon_config('blog');
+        self::afterInsert(function ($row) {
+            $row->save(['weigh' => $row['id']]);
+        });
+        self::afterWrite(function ($row) use ($config) {
+            $changedData = $row->getChangedData();
+            if (isset($changedData['status']) && $changedData['status'] == 'normal') {
+                if ($config['baidupush']) {
+                    //推送到熊掌号+百度站长
+                    $urls = [$row->fullurl];
+                    \think\Hook::listen("baidupush", $urls);
+                }
+            }
+        });
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        $diyname = $data['diyname'] ? $data['diyname'] : $data['id'];
+        return addon_url('blog/category/index', [':id' => $data['id'], ':diyname' => $diyname], true);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        $diyname = $data['diyname'] ? $data['diyname'] : $data['id'];
+        return addon_url('blog/category/index', [':id' => $data['id'], ':diyname' => $diyname], true, true);
+    }
+
+    public function getFlagList()
+    {
+        return ['hot' => __('Hot'), 'index' => __('Index'), 'recommend' => __('Recommend')];
+    }
+
+    public function getFlagTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['flag'];
+        $valueArr = explode(',', $value);
+        $list = $this->getFlagList();
+        return implode(',', array_intersect_key($list, array_flip($valueArr)));
+    }
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['status'];
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+}

+ 39 - 0
addons/blog/application/admin/model/BlogComment.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class BlogComment extends Model
+{
+
+    // 表名
+    protected $name = 'blog_comment';
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+        'status_text'
+    ];
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['status'];
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+    public function post()
+    {
+        return $this->belongsTo('BlogPost', 'post_id')->setEagerlyType(0);
+    }
+
+}

+ 105 - 0
addons/blog/application/admin/model/BlogPost.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace app\admin\model;
+
+use addons\blog\library\FulltextSearch;
+use think\Model;
+
+class BlogPost extends Model
+{
+
+    // 表名
+    protected $name = 'blog_post';
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+        'url',
+        'fullurl',
+        'flag_text',
+        'status_text'
+    ];
+
+    protected static function init()
+    {
+        $config = get_addon_config('blog');
+        self::afterInsert(function ($row) {
+            $row->save(['weigh' => $row['id']]);
+        });
+        self::afterWrite(function ($row) use ($config) {
+            $changedData = $row->getChangedData();
+            if (isset($changedData['status']) && $changedData['status'] == 'normal') {
+                if ($config['baidupush']) {
+                    //推送到熊掌号+百度站长
+                    $urls = [$row->fullurl];
+                    \think\Hook::listen("baidupush", $urls);
+                }
+            }
+            if ($config['searchtype'] == 'xunsearch') {
+                //更新全文搜索
+                FulltextSearch::update($row->id);
+            }
+        });
+        self::afterDelete(function ($row) use ($config) {
+            if ($config['searchtype'] == 'xunsearch') {
+                //更新全文搜索
+                FulltextSearch::del($row);
+            }
+        });
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $catename = isset($this->category) && $this->category ? $this->category->diyname : 'all';
+        $cateid = isset($this->category) && $this->category ? $this->category->id : 0;
+        return addon_url('blog/post/index', [':id' => $data['id'], ':diyname' => $diyname, ':catename' => $catename, ':cateid' => $cateid]);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $catename = isset($this->category) && $this->category ? $this->category->diyname : 'all';
+        $cateid = isset($this->category) && $this->category ? $this->category->id : 0;
+        return addon_url('blog/post/index', [':id' => $data['id'], ':diyname' => $diyname, ':catename' => $catename, ':cateid' => $cateid], true, true);
+    }
+
+    public function getFlagList()
+    {
+        return ['hot' => __('Hot'), 'index' => __('Index'), 'recommend' => __('Recommend')];
+    }
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getFlagTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['flag'];
+        $valueArr = explode(',', $value);
+        $list = $this->getFlagList();
+        return implode(',', array_intersect_key($list, array_flip($valueArr)));
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['status'];
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+    protected function setFlagAttr($value)
+    {
+        return is_array($value) ? implode(',', $value) : $value;
+    }
+
+    public function category()
+    {
+        return $this->belongsTo('BlogCategory', 'category_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+
+}

+ 66 - 0
addons/blog/application/admin/view/blog/block/add.html

@@ -0,0 +1,66 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label for="c-type" class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-type" data-rule="required" class="form-control selectpage" data-source="blog/block/selectpage_type" placeholder="类型为自定义,可任意输入" data-primary-key="type" data-field="type" name="row[type]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-name" data-rule="required" class="form-control" name="row[name]" placeholder="用于模板中标签调用" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-image" class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="input-group">
+                <input id="c-image" data-rule="" class="form-control" size="50" 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="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" for="c-image"></span>
+            </div>
+            <ul class="row list-inline plupload-preview" id="p-image"></ul>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-url" class="control-label col-xs-12 col-sm-2">{:__('Url')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-url" data-rule="" class="form-control" name="row[url]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-content" data-rule="" class="form-control editor" rows="15" name="row[content]" cols="50"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            <div class="radio">
+                {foreach name="statusList" item="vo"}
+                <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="normal"}checked{/in} /> {$vo}</label> 
+                {/foreach}
+            </div>
+
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 72 - 0
addons/blog/application/admin/view/blog/block/edit.html

@@ -0,0 +1,72 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label for="c-type" class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-type" data-rule="required" class="form-control selectpage" data-source="blog/block/selectpage_type" placeholder="类型为自定义,可任意输入" data-primary-key="type" data-field="type" name="row[type]" type="text" value="{$row.type}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="{$row.name|htmlentities}" placeholder="用于模板中标签调用">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text" value="{$row.title|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-image" class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="input-group">
+                <input id="c-image" data-rule="" class="form-control" size="50" name="row[image]" type="text" value="{$row.image|htmlentities}">
+                <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="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" for="c-image"></span>
+            </div>
+            <ul class="row list-inline plupload-preview" id="p-image"></ul>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-url" class="control-label col-xs-12 col-sm-2">{:__('Url')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-url" data-rule="" class="form-control" name="row[url]" type="text" value="{$row.url|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-content" data-rule="" class="form-control editor" rows="15" name="row[content]" cols="50">{$row.content|htmlentities}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label>
+        <div class="col-xs-12 col-sm-4">
+            <input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="{$row.weigh}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            <div class="radio">
+                {foreach name="statusList" item="vo"}
+                <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label> 
+                {/foreach}
+            </div>
+
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 36 - 0
addons/blog/application/admin/view/blog/block/index.html

@@ -0,0 +1,36 @@
+<div class="panel panel-default panel-intro">
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs" data-field="type">
+            <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
+            {foreach name="typeList" item="vo"}
+            <li><a href="#t-{$vo}" data-value="{$vo}" data-toggle="tab">{$vo}</a></li>
+            {/foreach}
+        </ul>
+    </div>
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar('refresh,add,edit,del')}
+                        <div class="dropdown btn-group {:$auth->check('blog/block/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover" 
+                           data-operate-edit="{:$auth->check('blog/block/edit')}"
+                           data-operate-del="{:$auth->check('blog/block/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 92 - 0
addons/blog/application/admin/view/blog/category/add.html

@@ -0,0 +1,92 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label for="c-pid" class="control-label col-xs-12 col-sm-2">{:__('Pid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <select id="c-pid" data-rule="required" class="form-control selectpicker" name="row[pid]">
+                {foreach name="parentList" item="vo"}
+                <option value="{$key}" {in name="key" value=""}selected{/in}>{$vo.name}</option>
+                {/foreach}
+            </select>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-nickname" class="control-label col-xs-12 col-sm-2">{:__('Nickname')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nickname" data-rule="" class="form-control" name="row[nickname]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-diyname" class="control-label col-xs-12 col-sm-2">{:__('Diyname')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-diyname" data-rule="required;diyname" class="form-control" name="row[diyname]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-flag" class="control-label col-xs-12 col-sm-2">{:__('Flag')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            <select  id="c-flag" data-rule="" class="form-control selectpicker" multiple="" name="row[flag][]">
+                {foreach name="flagList" item="vo"}
+                    <option value="{$key}" {in name="key" value=""}selected{/in}>{$vo}</option>
+                {/foreach}
+            </select>
+
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-image" class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="input-group">
+                <input id="c-image" class="form-control" size="50" 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-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-multiple="false" data-mimetype="image/*" data-input-id="c-image"><i class="fa fa-list-ul"></i> {:__('Choose')}</button></span>
+                </div>
+                <span class="msg-box n-right"></span>
+            </div>
+            <ul class="row list-inline plupload-preview" id="p-image"></ul>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-keywords" class="control-label col-xs-12 col-sm-2">{:__('Keywords')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-keywords" class="form-control" name="row[keywords]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-description" class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-description" class="form-control" name="row[description]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="0">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            {foreach name="statusList" item="vo"}
+            <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="normal"}checked{/in} /> {$vo}</label>
+            {/foreach}
+
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 91 - 0
addons/blog/application/admin/view/blog/category/edit.html

@@ -0,0 +1,91 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+    <input type="hidden" id="category-id" value="{$row.id}" />
+    <div class="form-group">
+        <label for="c-pid" class="control-label col-xs-12 col-sm-2">{:__('Pid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <select id="c-pid" data-rule="required" class="form-control selectpicker" name="row[pid]">
+                {foreach name="parentList" item="vo"}
+                <option value="{$key}" {in name="key" value="$row.pid"}selected{/in}>{$vo.name}</option>
+                {/foreach}
+            </select>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="{$row.name|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-nickname" class="control-label col-xs-12 col-sm-2">{:__('Nickname')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nickname" data-rule="" class="form-control" name="row[nickname]" type="text" value="{$row.nickname|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-diyname" class="control-label col-xs-12 col-sm-2">{:__('Diyname')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-diyname" data-rule="required;diyname" class="form-control" name="row[diyname]" type="text" value="{$row.diyname|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-flag" class="control-label col-xs-12 col-sm-2">{:__('Flag')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            <select  id="c-flag" data-rule="" class="form-control selectpicker" multiple="" name="row[flag][]">
+                {foreach name="flagList" item="vo"}
+                    <option value="{$key}" {in name="key" value="$row.flag"}selected{/in}>{$vo}</option>
+                {/foreach}
+            </select>
+
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-image" class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="input-group">
+                <input id="c-image" class="form-control" size="50" name="row[image]" type="text" value="{$row.image|htmlentities}">
+                <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-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-multiple="false" data-mimetype="image/*" data-input-id="c-image"><i class="fa fa-list-ul"></i> {:__('Choose')}</button></span>
+                </div>
+                <span class="msg-box n-right"></span>
+            </div>
+            <ul class="row list-inline plupload-preview" id="p-image"></ul>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-keywords" class="control-label col-xs-12 col-sm-2">{:__('Keywords')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-keywords" class="form-control" name="row[keywords]" type="text" value="{$row.keywords|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-description" class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-description" class="form-control" name="row[description]" type="text" value="{$row.description|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="{$row.weigh|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            {foreach name="statusList" item="vo"}
+            <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label>
+            {/foreach}
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 28 - 0
addons/blog/application/admin/view/blog/category/index.html

@@ -0,0 +1,28 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar('refresh,add')}
+                        <div class="dropdown btn-group {:$auth->check('blog/category/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover" 
+                           data-operate-edit="{:$auth->check('blog/category/edit')}" 
+                           data-operate-del="{:$auth->check('blog/category/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 80 - 0
addons/blog/application/admin/view/blog/comment/add.html

@@ -0,0 +1,80 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label for="c-post_id" class="control-label col-xs-12 col-sm-2">{:__('Post')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-post_id" data-rule="required" data-source="blog/post/index" data-field="title" class="form-control selectpage" name="row[post_id]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-pid" class="control-label col-xs-12 col-sm-2">{:__('Pid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-pid" data-rule="required" class="form-control" name="row[pid]" type="number" value="0">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-username" class="control-label col-xs-12 col-sm-2">{:__('Username')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-username" data-rule="required" class="form-control" name="row[username]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-email" class="control-label col-xs-12 col-sm-2">{:__('Email')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-email" data-rule="required" class="form-control" name="row[email]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-website" class="control-label col-xs-12 col-sm-2">{:__('Website')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-website" class="form-control" name="row[website]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-content" data-rule="required" class="form-control" rows="5" name="row[content]" cols="50"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-comments" class="control-label col-xs-12 col-sm-2">{:__('Comments')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-comments" data-rule="required" class="form-control" name="row[comments]" type="number" value="0">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-ip" class="control-label col-xs-12 col-sm-2">{:__('Ip')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ip" class="form-control" name="row[ip]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-useragent" class="control-label col-xs-12 col-sm-2">{:__('Useragent')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-useragent" class="form-control" name="row[useragent]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-subscribe" class="control-label col-xs-12 col-sm-2">{:__('Subscribe')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-subscribe" data-rule="required" class="form-control" name="row[subscribe]" type="number" value="0">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+                        
+            {foreach name="statusList" item="vo"}
+            <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="normal"}checked{/in} /> {$vo}</label> 
+            {/foreach}
+
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 80 - 0
addons/blog/application/admin/view/blog/comment/edit.html

@@ -0,0 +1,80 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label for="c-post_id" class="control-label col-xs-12 col-sm-2">{:__('Post')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-post_id" data-rule="required" data-source="blog/post/index" data-field="title" class="form-control selectpage" name="row[post_id]" type="text" value="{$row.post_id}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-pid" class="control-label col-xs-12 col-sm-2">{:__('Pid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-pid" data-rule="required" class="form-control" name="row[pid]" type="number" value="{$row.pid}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-username" class="control-label col-xs-12 col-sm-2">{:__('Username')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-username" data-rule="required" class="form-control" name="row[username]" type="text" value="{$row.username|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-email" class="control-label col-xs-12 col-sm-2">{:__('Email')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-email" data-rule="required" class="form-control" name="row[email]" type="text" value="{$row.email|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-website" class="control-label col-xs-12 col-sm-2">{:__('Website')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-website" class="form-control" name="row[website]" type="text" value="{$row.website|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-content" data-rule="required" class="form-control" rows="5" name="row[content]" cols="50">{$row.content|htmlentities}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-comments" class="control-label col-xs-12 col-sm-2">{:__('Comments')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-comments" data-rule="required" class="form-control" name="row[comments]" type="number" value="{$row.comments}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-ip" class="control-label col-xs-12 col-sm-2">{:__('Ip')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ip" class="form-control" name="row[ip]" type="text" value="{$row.ip|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-useragent" class="control-label col-xs-12 col-sm-2">{:__('Useragent')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-useragent" class="form-control" name="row[useragent]" type="text" value="{$row.useragent|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-subscribe" class="control-label col-xs-12 col-sm-2">{:__('Subscribe')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-subscribe" data-rule="required" class="form-control" name="row[subscribe]" type="number" value="{$row.subscribe|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+                        
+            {foreach name="statusList" item="vo"}
+            <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label> 
+            {/foreach}
+
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 37 - 0
addons/blog/application/admin/view/blog/comment/index.html

@@ -0,0 +1,37 @@
+<div class="panel panel-default panel-intro">
+
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs" data-field="status">
+            <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
+            {foreach name="statusList" item="vo"}
+            <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
+            {/foreach}
+        </ul>
+    </div>
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar('refresh,delete')}
+                        <div class="dropdown btn-group {:$auth->check('blog/comment/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover" 
+                           data-operate-edit="{:$auth->check('blog/comment/edit')}" 
+                           data-operate-del="{:$auth->check('blog/comment/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 130 - 0
addons/blog/application/admin/view/blog/post/add.html

@@ -0,0 +1,130 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label for="c-category_id" class="control-label col-xs-12 col-sm-2">{:__('Category')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <select id="c-category_id" data-rule="required" class="form-control selectpicker" name="row[category_id]">
+                {foreach name="categoryList" item="vo"}
+                <option value="{$key}" {in name="key" value=""}selected{/in}>{$vo.name}</option>
+                {/foreach}
+            </select>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-flag" class="control-label col-xs-12 col-sm-2">{:__('Flag')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            <select  id="c-flag" data-rule="" class="form-control selectpicker" multiple="" name="row[flag][]">
+                {foreach name="flagList" item="vo"}
+                    <option value="{$key}" {in name="key" value=""}selected{/in}>{$vo}</option>
+                {/foreach}
+            </select>
+
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-diyname" class="control-label col-xs-12 col-sm-2">{:__('Diyname')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-diyname" data-rule="required;diyname" class="form-control" name="row[diyname]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-summary" class="control-label col-xs-12 col-sm-2">{:__('Summary')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-summary" data-rule="required" class="form-control editor" rows="5" name="row[summary]" cols="50"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-content" data-rule="required" class="form-control editor" rows="5" name="row[content]" cols="50"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-thumb" class="control-label col-xs-12 col-sm-2">{:__('Thumb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="input-group">
+                <input id="c-thumb" class="form-control" size="50" name="row[thumb]" type="text" value="">
+                <div class="input-group-addon no-border no-padding">
+                    <span><button type="button" id="plupload-thumb" class="btn btn-danger plupload" data-input-id="c-thumb" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-preview-id="p-thumb"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                    <span><button type="button" id="fachoose-thumb" class="btn btn-primary fachoose" data-multiple="false" data-mimetype="image/*" data-input-id="c-thumb"><i class="fa fa-list-ul"></i> {:__('Choose')}</button></span>
+                </div>
+                <span class="msg-box n-right"></span>
+            </div>
+            <ul class="row list-inline plupload-preview" id="p-thumb"></ul>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-image" class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="input-group">
+                <input id="c-image" class="form-control" size="50" 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-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-multiple="false" data-mimetype="image/*" data-input-id="c-image"><i class="fa fa-list-ul"></i> {:__('Choose')}</button></span>
+                </div>
+                <span class="msg-box n-right"></span>
+            </div>
+            <ul class="row list-inline plupload-preview" id="p-image"></ul>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-seotitle" class="control-label col-xs-12 col-sm-2">{:__('Seotitle')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-seotitle" class="form-control" name="row[seotitle]" type="text" value="" placeholder="为空时将使用文档标题">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-keywords" class="control-label col-xs-12 col-sm-2">{:__('Keywords')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-keywords" class="form-control" name="row[keywords]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-description" class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-description" class="form-control" name="row[description]"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-views" class="control-label col-xs-12 col-sm-2">{:__('Views')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-views" data-rule="required" class="form-control" name="row[views]" type="number" value="0">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-comments" class="control-label col-xs-12 col-sm-2">{:__('Comments')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-comments" data-rule="required" class="form-control" name="row[comments]" type="number" value="0">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="0">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            {foreach name="statusList" item="vo"}
+            <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="normal"}checked{/in} /> {$vo}</label>
+            {/foreach}
+
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 130 - 0
addons/blog/application/admin/view/blog/post/edit.html

@@ -0,0 +1,130 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+    <input type="hidden" id="post-id" value="{$row.id}" >
+    <div class="form-group">
+        <label for="c-category_id" class="control-label col-xs-12 col-sm-2">{:__('Category')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <select id="c-category_id" data-rule="required" class="form-control selectpicker" name="row[category_id]">
+                {foreach name="categoryList" item="vo"}
+                <option value="{$key}" {in name="key" value="$row.category_id"}selected{/in}>{$vo.name}</option>
+                {/foreach}
+            </select>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-flag" class="control-label col-xs-12 col-sm-2">{:__('Flag')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            <select  id="c-flag" data-rule="" class="form-control selectpicker" multiple="" name="row[flag][]">
+                {foreach name="flagList" item="vo"}
+                    <option value="{$key}" {in name="key" value="$row.flag"}selected{/in}>{$vo}</option>
+                {/foreach}
+            </select>
+
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text" value="{$row.title|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-diyname" class="control-label col-xs-12 col-sm-2">{:__('Diyname')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-diyname" data-rule="required;diyname" class="form-control" name="row[diyname]" type="text" value="{$row.diyname|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-summary" class="control-label col-xs-12 col-sm-2">{:__('Summary')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-summary" data-rule="required" class="form-control" rows="5" name="row[summary]" cols="50">{$row.summary|htmlentities}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-content" data-rule="required" class="form-control editor" rows="5" name="row[content]" cols="50">{$row.content|htmlentities}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-thumb" class="control-label col-xs-12 col-sm-2">{:__('Thumb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="input-group">
+                <input id="c-thumb" class="form-control" size="50" name="row[thumb]" type="text" value="{$row.thumb|htmlentities}">
+                <div class="input-group-addon no-border no-padding">
+                    <span><button type="button" id="plupload-thumb" class="btn btn-danger plupload" data-input-id="c-thumb" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-preview-id="p-thumb"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                    <span><button type="button" id="fachoose-thumb" class="btn btn-primary fachoose" data-multiple="false" data-mimetype="image/*" data-input-id="c-thumb"><i class="fa fa-list-ul"></i> {:__('Choose')}</button></span>
+                </div>
+                <span class="msg-box n-right"></span>
+            </div>
+            <ul class="row list-inline plupload-preview" id="p-thumb"></ul>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-image" class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="input-group">
+                <input id="c-image" class="form-control" size="50" name="row[image]" type="text" value="{$row.image|htmlentities}">
+                <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-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-multiple="false" data-mimetype="image/*" data-input-id="c-image"><i class="fa fa-list-ul"></i> {:__('Choose')}</button></span>
+                </div>
+                <span class="msg-box n-right"></span>
+            </div>
+            <ul class="row list-inline plupload-preview" id="p-image"></ul>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-seotitle" class="control-label col-xs-12 col-sm-2">{:__('Seotitle')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-seotitle" class="form-control" name="row[seotitle]" type="text" value="{$row.seotitle|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-keywords" class="control-label col-xs-12 col-sm-2">{:__('Keywords')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-keywords" class="form-control" name="row[keywords]" type="text" value="{$row.keywords|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-description" class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-description" class="form-control" name="row[description]">{$row.description|htmlentities}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-views" class="control-label col-xs-12 col-sm-2">{:__('Views')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-views" data-rule="required" class="form-control" name="row[views]" type="number" value="{$row.views|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-comments" class="control-label col-xs-12 col-sm-2">{:__('Comments')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-comments" data-rule="required" class="form-control" name="row[comments]" type="number" value="{$row.comments|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="{$row.weigh|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+
+            {foreach name="statusList" item="vo"}
+            <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label>
+            {/foreach}
+
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 37 - 0
addons/blog/application/admin/view/blog/post/index.html

@@ -0,0 +1,37 @@
+<div class="panel panel-default panel-intro">
+
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs" data-field="status">
+            <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
+            {foreach name="statusList" item="vo"}
+            <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
+            {/foreach}
+        </ul>
+    </div>
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar()}
+                        <div class="dropdown btn-group {:$auth->check('blog/post/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover" 
+                           data-operate-edit="{:$auth->check('blog/post/edit')}" 
+                           data-operate-del="{:$auth->check('blog/post/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

File diff suppressed because it is too large
+ 5 - 0
addons/blog/assets/default/bootstrap/css/bootstrap-grid.min.css


File diff suppressed because it is too large
+ 6 - 0
addons/blog/assets/default/bootstrap/css/bootstrap-reboot.min.css


File diff suppressed because it is too large
+ 5 - 0
addons/blog/assets/default/bootstrap/css/bootstrap.min.css


File diff suppressed because it is too large
+ 5 - 0
addons/blog/assets/default/bootstrap/js/bootstrap.bundle.min.js


File diff suppressed because it is too large
+ 5 - 0
addons/blog/assets/default/bootstrap/js/bootstrap.min.js


+ 754 - 0
addons/blog/assets/default/css/common.css

@@ -0,0 +1,754 @@
+/*global*/
+html, body {
+    height: 100%;
+    -webkit-font-smoothing: antialiased;
+    text-rendering: optimizeLegibility;
+    -moz-osx-font-smoothing: grayscale;
+    font-feature-settings: 'liga';
+    -webkit-text-size-adjust: 100%;
+    font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Ubuntu, Helvetica Neue, Helvetica, Arial, PingFang SC, Hiragino Sans GB, Microsoft YaHei UI, Microsoft YaHei, Source Han Sans CN, sans-serif;
+    font-weight: 400;
+    background: #f4f4f4;
+    font-size: 14px;
+    color: #616161;
+}
+
+body {
+    background-color: #f4f4f4;
+}
+
+a {
+    color: #333;
+}
+
+a:hover {
+    color: #18bc9d;
+    text-decoration: none;
+}
+
+.cl-header {
+    background-color: #fff;
+    /*height: 80px;*/
+    /*line-height: 80px;*/
+    box-shadow: 0 5px 15px rgba(153, 153, 153, 0.1)
+}
+
+.header-content {
+    position: relative;
+}
+
+.cl-logo {
+    position: absolute;
+    left: 0;
+    top: 0;
+    height: 80px;
+    width: 300px;
+    overflow: hidden;
+}
+
+.cl-logo img {
+    max-width: 100%;
+    max-height: 100%;
+}
+
+@media (min-width: 992px) {
+    .navbar-expand-lg .navbar-nav .nav-link {
+        padding-right: 1rem;
+        padding-left: 1rem;
+    }
+}
+
+.navbar-toggler:not(:disabled):not(.disabled):focus {
+    outline: none;
+}
+
+.navbar {
+    padding: 0;
+}
+
+.navbar-light .navbar-nav .nav-link {
+    font-size: 1.14rem;
+}
+
+.navbar-light .navbar-nav .active > .nav-link {
+    color: #18bc9d;
+    font-weight: bold;
+}
+
+.nav .nav-item a.active {
+    color: #18bc9d;
+    font-weight: bold;
+}
+
+.navbar .navbar-brand img {
+    max-width: 100%;
+    max-height: 100%;
+}
+
+#category .dropdown-menu li {
+    padding: 5px 10px;
+}
+
+#category .dropdown-menu li.active {
+    background: #eee;
+}
+
+.recommend-index {
+    margin-bottom: 20px;
+}
+
+/*left*/
+.cl-left {
+    margin: 20px 0;
+    padding: 0;
+}
+
+.recommend-top {
+    margin-bottom: 20px;
+}
+
+.recommend-bottom {
+    display: flex;
+    justify-content: space-between;
+}
+
+.recommend-item {
+    width: 49%;
+    /*height: 170px;*/
+    overflow: hidden;
+}
+
+.cl-artical-content, .recommend-panel, .comment {
+    background-color: #fff;
+    padding: 0 20px 20px 20px;
+}
+
+/*card*/
+.cl-card {
+    background-color: #fff;
+    padding: 1rem;
+    margin: 0 0 20px 0;
+}
+
+.cl-card .cl-card-image {
+    overflow: hidden;
+}
+
+.cl-card .cl-card-image img {
+    width: 100%;
+    max-width: 100%;
+}
+
+.cl-card .cl-card-more a {
+    color: #18bc9d;
+}
+
+.cl-card-main-title a:hover, .cl-card .cl-card-more a:hover {
+    text-decoration: underline;
+}
+
+.cl-card-main-title {
+    height: 2rem;
+    overflow: hidden;
+
+}
+
+.cl-card-main-title a {
+    font-size: 1.4rem;
+    color: #333;
+    font-weight: normal;
+}
+
+.cl-card-main-title a:hover {
+    text-decoration: underline;
+}
+
+.cl-card-main-info {
+    font-size: 1rem;
+    color: #999;
+    height: 3rem;
+    overflow: hidden;
+    margin: 0.5rem 0 2rem 0;
+}
+
+.img-zoom {
+    overflow: hidden;
+    display: inline-block;
+}
+
+.img-zoom img {
+    -webkit-transition: all 0.3s;
+    -moz-transition: all 0.3s;
+    -o-transition: all 0.3s;
+    transition: all 0.3s;
+}
+
+.img-zoom:hover img {
+    -webkit-transform: scale(1.1);
+    -moz-transform: scale(1.1);
+    -o-transform: scale(1.1);
+    -ms-transform: scale(1.1);
+    transform: scale(1.1);
+}
+
+.object-responsive {
+    position: relative;
+    display: block;
+    height: 0;
+    padding: 0;
+    overflow: hidden;
+}
+
+.object-responsive img {
+    position: absolute;
+    object-fit: cover;
+    width: 100%;
+    height: 100%;
+    border: 0;
+}
+
+.object-responsive-16by9 {
+    padding-bottom: 56.25%;
+}
+
+.object-responsive-4by3 {
+    padding-bottom: 75%;
+}
+
+.object-responsive-square {
+    padding-bottom: 100%;
+}
+
+/*page*/
+.pagination .page-item.active .page-link {
+    background-color: #18bc9d;
+    border-color: #18bc9d;
+    color: #fff;
+}
+
+.pagination .page-item .page-link {
+    color: #666;
+}
+
+.pagination {
+    margin: 20px 0 0 0;
+}
+
+.pagination li.disabled .page-link {
+    background-color: #eee;
+    color: #999;
+}
+
+.pagination li.disabled {
+    cursor: not-allowed;
+}
+
+/*right*/
+.cl-right {
+    margin: 20px 0;
+}
+
+.right-card-main {
+    background-color: #fff;
+    margin: 0 0 20px 0;
+}
+
+.right-card-main .img-responsive {
+    width: 100%;
+}
+
+.cl-code {
+    width: 80%;
+    margin: 10px auto;
+    padding: 0 0 10px 0;
+}
+
+.right-card-title {
+    font-size: 1.2rem;
+    color: #666;
+    font-weight: bold;
+    padding: 15px 0 10px 10px;
+}
+
+.cl-code p {
+    color: #999;
+    line-height: 1.2rem;
+    margin-top: 10px;
+    text-align: center;
+}
+
+/*right-recommended*/
+.right-recommended {
+    padding: 10px;
+    height: 120px;
+    overflow: hidden;
+}
+
+.right-recommended-list {
+    list-style: none;
+    padding: 0 10px 20px 10px;
+    box-sizing: border-box;
+}
+
+.right-recommended-list i {
+    color: #999;
+    margin-right: 4px;
+}
+
+.right-recommended-list li {
+    width: 100%;
+    /*border-bottom: 1px dashed #eaeaea;*/
+    color: #666;
+    height: 40px;
+    line-height: 40px;
+    overflow: hidden;
+}
+
+/*label*/
+.label {
+    list-style: none;
+    display: flex;
+    flex-wrap: wrap;
+    padding: 10px;
+
+}
+
+.label li a {
+    display: block;
+    border: 1px solid #dbdbdb;
+    padding: 2px 10px;
+    margin: 0 8px 8px 0;
+    color: #666;
+}
+
+.label li a:hover {
+    background-color: #18bc9d;
+    border-color: #18bc9d;
+    color: #fff;
+}
+
+/*footer*/
+.cl-footer {
+    background-color: #fbfbfb;
+    text-align: center;
+    padding: 2rem 0 1rem 0;
+    margin: 1rem 0 0 0;
+    border-top: 1px solid #f1f1f1;
+}
+
+.cl-footer-link a {
+    font-size: 20px;
+    padding: 1rem 0;
+    color: #666;
+}
+
+.cl-copyright {
+    color: #666;
+    font-size: 14px;
+    line-height: 0.8em;
+}
+
+/*artical*/
+.cl-artical {
+    margin-bottom: 15px;
+}
+
+.cl-artical-title {
+    padding: 20px 0 10px 0;
+}
+
+.cl-card-tag {
+    color: #999;
+    font-size: 1rem;
+}
+
+.cl-artical h1, .cl-artical h2, .cl-artical h3, .cl-artical h4, .cl-artical h5 {
+    padding: 1.5rem 0 0.5rem 0;
+}
+
+.cl-artical h1 {
+    font-size: 2rem;
+}
+
+.cl-artical h2 {
+    font-size: 1.75rem;
+}
+
+.cl-artical h3 {
+    font-size: 1.5rem;
+}
+
+.cl-artical h4 {
+    font-size: 1.25rem;
+}
+
+.cl-artical h5 {
+    font-size: 1rem;
+}
+
+.cl-artical {
+    padding: 1.5rem 0 0 0;
+    color: #666;
+}
+
+.cl-artical blockquote {
+    font-size: 18px;
+    padding: 2rem 1rem 2rem 2rem;
+    border-left: 4px solid #3F6600;
+    background-color: #f3f3f3;
+    /*font-style: italic;*/
+    font-weight: bold;
+    line-height: 1.8em;
+}
+
+.cl-artical blockquote:before {
+    content: " “ ";
+    font-size: 60px;
+    vertical-align: bottom;
+    color: #3F6600;
+}
+
+.cl-artical blockquote span {
+    font-size: 36px
+}
+
+.cl-artical p img {
+    vertical-align: middle;
+    width: 100%;
+    max-width: 100%;
+    height: auto;
+}
+
+.cl-artical p {
+    padding: 0.5rem 0 0.5rem 0;
+    line-height: 1.8rem;
+}
+
+.cl-content-info {
+    color: #999;
+}
+
+/*comment panel*/
+.comment-panel {
+    padding-bottom: 2rem;
+    margin-top: 2rem;
+    border-bottom: 1px solid #dee2e6;
+    position: relative;
+}
+
+.comment-secondary-panel {
+    padding-bottom: 0.5rem;
+    margin-top: 1rem;
+    padding-top: 1rem;
+    background-color: #f4f4f4;
+    position: relative;
+}
+
+.triangle-icon {
+    position: absolute;
+    left: 50px;
+    top: -30px;
+    color: #f4f4f4;
+    font-size: 60px;
+
+}
+
+.comment-panel .comment-panel-portrait {
+    position: absolute;
+    top: 0;
+    left: 0;
+}
+
+.comment-panel-portrait img {
+    width: 60px;
+    height: 60px;
+}
+
+.comment-panel .comment-panel-content {
+    padding: 0 0 0 70px;
+    width: 100%;
+}
+
+.comment-panel .comment-secondary-panel .comment-panel-content {
+    padding: 0 0 10px 20px;
+    width: 100%;
+}
+
+.comment-panel-content-item div {
+    display: inline;
+    padding: 0 0.5rem 0 0;
+    font-size: 14px;
+    color: #999;
+}
+
+.comment-panel-content-item .comment-author {
+    font-size: 16px;
+    color: #333;
+    font-weight: bold;
+}
+
+.comment-panel .comment-panel-content .comment-panel-content-main {
+    margin-top: 0.5rem;
+    color: #666;
+}
+
+.comment-panel .comment-panel-secondary {
+    border: 1px solid #e5e5e5;
+    padding: 0.5rem 0.5rem 0 0.5rem;
+    margin: 0.5rem 0;
+    background-color: #fffffb;
+}
+
+.comment-panel .comment-reply {
+    position: absolute;
+    right: 5px;
+    top: 2px;
+    color: #eee;
+}
+
+.comment-panel .comment-reply a {
+    color: #999;
+}
+
+@media (min-width: 576px) {
+    .ll-panel {
+        max-width: 30%;
+    }
+}
+
+/*comment form*/
+.cl-comment-from {
+    padding: 10px 0 0 0;
+}
+
+.cl-comment-from div {
+    height: 3rem;
+    border: 1px solid #dfdfdf;
+}
+
+.cl-comment-from button {
+    float: right;
+    border: none;
+    background-color: #18bc9d;
+    color: #fff;
+    font-size: 16px;
+    padding: 0.5rem 1rem;
+    cursor: pointer;
+    margin-top: 10px;
+}
+
+.cl-comment-from button:hover {
+    background-color: #f38d00;
+}
+
+.cl-comment-from .input {
+    border: 1px solid #ced4da;
+    padding: 0.3rem 0.5rem;
+    border-radius: 2px;
+}
+
+.cl-comment-from > div > textarea {
+    border: none;
+    width: 100%;
+    height: 100%;
+    line-height: 3rem;
+    padding: 0 1rem;
+    box-sizing: border-box;
+}
+
+.cl-comment-from > div > textarea:focus, .cl-comment-from button:focus {
+    outline: none;
+}
+
+/*recommend aritical*/
+.recommend-panel {
+    margin: 20px 0;
+}
+
+.recommend-panel a.recommend-panel-link {
+    display: block;
+}
+
+.recommend-panel-bottom {
+    padding: 0.5rem;
+    font-size: 14px;
+}
+
+.ll-title {
+    margin: 3rem 0 1rem 0;
+    color: #333;
+}
+
+.recommend-panel-top {
+    overflow: hidden;
+    height: 12rem;
+}
+
+.recommend-panel-top img, .recommend-item img, .cl-card-image img {
+    width: 100%;
+    max-width: 100%;
+}
+
+#comments h3, #respond h3 {
+    font-size: 1.2rem;
+    margin-top: 5px;
+}
+
+#respond h3 a {
+    font-size: 1rem;
+    color: #007bff;
+}
+
+.comment-panel #respond {
+    margin-top: 1rem;
+    padding: 0 0 0 70px;
+}
+
+.floatbar {
+    width: 50px;
+    position: fixed;
+    right: 0;
+    bottom: 40px;
+    z-index: 999;
+}
+
+.floatbar a {
+    display: block;
+    padding: 5px 10px;
+    font-size: 30px;
+    opacity: .5;
+}
+
+.floatbar a:hover {
+    opacity: 1;
+}
+
+.pagination {
+    padding-left: 0;
+    margin: 17px 0;
+    border-radius: 3px
+}
+
+.pagination > li {
+    display: inline
+}
+
+.pagination > li > a, .pagination > li > span {
+    position: relative;
+    float: left;
+    padding: 6px 12px;
+    line-height: 1.42857143;
+    text-decoration: none;
+    color: #2c3e50;
+    background-color: #fff;
+    border: 1px solid #ddd;
+    margin-left: -1px
+}
+
+.pagination > li:first-child > a, .pagination > li:first-child > span {
+    margin-left: 0;
+    border-bottom-left-radius: 3px;
+    border-top-left-radius: 3px
+}
+
+.pagination > li:last-child > a, .pagination > li:last-child > span {
+    border-bottom-right-radius: 3px;
+    border-top-right-radius: 3px
+}
+
+.pagination > li > a:focus, .pagination > li > a:hover, .pagination > li > span:focus, .pagination > li > span:hover {
+    z-index: 2;
+    color: #11181f;
+    background-color: #eee;
+    border-color: #ddd
+}
+
+.pagination > .active > a, .pagination > .active > a:focus, .pagination > .active > a:hover, .pagination > .active > span, .pagination > .active > span:focus, .pagination > .active > span:hover {
+    z-index: 3;
+    color: #fff;
+    background-color: #2c3e50;
+    border-color: #2c3e50;
+    cursor: default
+}
+
+.pagination > .disabled > a, .pagination > .disabled > a:focus, .pagination > .disabled > a:hover, .pagination > .disabled > span, .pagination > .disabled > span:focus, .pagination > .disabled > span:hover {
+    color: #777;
+    background-color: #fff;
+    border-color: #ddd;
+    cursor: not-allowed
+}
+
+.pager .pagination {
+    margin: 0
+}
+
+.pager li {
+    margin: 0 .4em;
+    display: inline-block
+}
+
+.pager li:first-child > a, .pager li:first-child > span, .pager li:last-child > a, .pager li:last-child > span {
+    padding: .5em 1.2em
+}
+
+.pager li > a, .pager li > span {
+    border: 1px solid #e6e6e6;
+    border-radius: .25em;
+    padding: .5em .93em;
+    font-size: 14px
+}
+
+/* 搜索建议 */
+.autocomplete-suggestions {
+    text-align: left;
+    cursor: default;
+    background: #fff;
+    border: 1px solid rgba(0, 0, 0, 0.15);
+    border-radius: 2px;
+    -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+    -moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+    background-clip: padding-box;
+    position: absolute;
+    display: none;
+    z-index: 1036;
+    max-height: 254px;
+    overflow: hidden;
+    overflow-y: auto;
+    box-sizing: border-box;
+}
+
+.autocomplete-suggestions .autocomplete-suggestion {
+    padding: 5px 12px;
+}
+
+.autocomplete-suggestions .autocomplete-suggestion:hover {
+    background: #f0f0f0;
+}
+
+/* 加载更多 */
+.loadmore {
+    width: 80%;
+    margin: 1.5em auto;
+    line-height: 1.6em;
+    font-size: 14px;
+    text-align: center;
+}
+
+.loadmore-tips {
+    display: inline-block;
+    vertical-align: middle;
+}
+
+.loadmore-line {
+    border-top: 1px solid #E5E5E5;
+    margin-top: 2.4em;
+}
+
+.loadmore-line .loadmore-tips {
+    position: relative;
+    top: -0.9em;
+    padding: 0 .55em;
+    background-color: #FFFFFF;
+    color: #808080;
+}

BIN
addons/blog/assets/default/example/12.jpg


BIN
addons/blog/assets/default/example/13.jpg


BIN
addons/blog/assets/default/example/15.jpg


BIN
addons/blog/assets/default/example/16.jpg


BIN
addons/blog/assets/default/example/21.jpg


+ 71 - 0
addons/blog/assets/default/js/common.js

@@ -0,0 +1,71 @@
+$(function () {
+
+    $('.radio label').each(function () {
+        var e = $(this);
+        e.click(function () {
+            e.closest('.radio').find("label").removeClass("active");
+            e.addClass("active");
+        });
+    });
+    $('.checkbox label').each(function () {
+        var e = $(this);
+        e.click(function () {
+            if (e.find('input').is(':checked')) {
+                e.addClass("active");
+            } else {
+                e.removeClass("active");
+            }
+        });
+    });
+
+    $('.icon-navicon').each(function () {
+        var e = $(this);
+        var target = e.attr("data-target");
+        e.click(function () {
+            $(target).slideToggle().toggleClass("nav-navicon");
+        });
+    });
+
+    // 加载更多
+    $(document).on("click", ".btn-loadmore", function () {
+        var that = this;
+        var page = parseInt($(this).data("page"));
+        page++;
+        $(that).prop("disabled", true);
+        $.ajax({
+            url: $(that).attr("href"),
+            success: function (data, ret) {
+                $(data).insertBefore($(that).parent());
+                $(that).remove();
+                return false;
+            },
+            error: function (data) {
+
+            }
+        });
+        return false;
+    });
+
+    //自动加载更多
+    $(window).scroll(function () {
+        var loadmore = $(".btn-loadmore");
+        if (loadmore.size() > 0 && !loadmore.prop("disabled")) {
+            if ($(window).scrollTop() - loadmore.height() > loadmore.offset().top - $(window).height()) {
+                loadmore.trigger("click");
+            }
+        }
+    });
+    setTimeout(function () {
+        if ($(window).scrollTop() > 0) {
+            $(window).trigger("scroll");
+        }
+    }, 500);
+
+    // 回到顶部
+    $(document).on('click', ".gotop", function (e) {
+        e.preventDefault();
+        $('html,body').animate({
+            scrollTop: 0
+        }, 700);
+    });
+});

+ 215 - 0
addons/blog/assets/default/js/jquery.autocomplete.js

@@ -0,0 +1,215 @@
+/*
+	jQuery autoComplete v1.0.7
+    Copyright (c) 2014 Simon Steinberger / Pixabay
+    GitHub: https://github.com/Pixabay/jQuery-autoComplete
+	License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+(function ($) {
+    $.fn.autoComplete = function (options) {
+        var o = $.extend({}, $.fn.autoComplete.defaults, options);
+
+        // public methods
+        if (typeof options == 'string') {
+            this.each(function () {
+                var that = $(this);
+                if (options == 'destroy') {
+                    $(window).off('resize.autocomplete', that.updateSC);
+                    that.off('blur.autocomplete focus.autocomplete keydown.autocomplete keyup.autocomplete');
+                    if (that.data('autocomplete'))
+                        that.attr('autocomplete', that.data('autocomplete'));
+                    else
+                        that.removeAttr('autocomplete');
+                    $(that.data('sc')).remove();
+                    that.removeData('sc').removeData('autocomplete');
+                }
+            });
+            return this;
+        }
+
+        return this.each(function () {
+            var that = $(this);
+            // sc = 'suggestions container'
+            that.sc = $('<div class="autocomplete-suggestions ' + o.menuClass + '"></div>');
+            that.data('sc', that.sc).data('autocomplete', that.attr('autocomplete'));
+            that.attr('autocomplete', 'off');
+            that.cache = {};
+            that.last_val = '';
+
+            that.updateSC = function (resize, next) {
+                that.sc.css({
+                    top: that.offset().top + that.outerHeight() - (that.sc.css("position") == "fixed" ? $(window).scrollTop() : 0),
+                    left: that.offset().left,
+                    width: that.outerWidth()
+                });
+                if (!resize) {
+                    that.sc.show();
+                    if (!that.sc.maxHeight) that.sc.maxHeight = parseInt(that.sc.css('max-height'));
+                    if (!that.sc.suggestionHeight) that.sc.suggestionHeight = $('.autocomplete-suggestion', that.sc).first().outerHeight();
+                    if (that.sc.suggestionHeight)
+                        if (!next) that.sc.scrollTop(0);
+                        else {
+                            var scrTop = that.sc.scrollTop(), selTop = next.offset().top - that.sc.offset().top;
+                            if (selTop + that.sc.suggestionHeight - that.sc.maxHeight > 0)
+                                that.sc.scrollTop(selTop + that.sc.suggestionHeight + scrTop - that.sc.maxHeight);
+                            else if (selTop < 0)
+                                that.sc.scrollTop(selTop + scrTop);
+                        }
+                }
+            }
+            $(window).on('resize.autocomplete', that.updateSC);
+
+            that.sc.appendTo('body');
+
+            that.on('click', function () {
+                if ($(this).val().length > 0 && that.sc.is(":hidden")) {
+                    setTimeout(function () {
+                        that.sc.show();
+                    }, 100);
+                }
+            });
+
+            that.sc.on('mouseleave', '.autocomplete-suggestion', function () {
+                $('.autocomplete-suggestion.selected').removeClass('selected');
+            });
+
+            that.sc.on('mouseenter', '.autocomplete-suggestion', function () {
+                $('.autocomplete-suggestion.selected').removeClass('selected');
+                $(this).addClass('selected');
+            });
+
+            that.sc.on('mousedown click', '.autocomplete-suggestion', function (e) {
+                var item = $(this), v = item.data('val');
+                if (v || item.hasClass('autocomplete-suggestion')) { // else outside click
+                    that.val(v);
+                    o.onSelect(e, v, item);
+                    that.sc.hide();
+                }
+                return false;
+            });
+
+            that.on('blur.autocomplete', function () {
+                try {
+                    over_sb = $('.autocomplete-suggestions:hover').length;
+                } catch (e) {
+                    over_sb = 0;
+                } // IE7 fix :hover
+                if (!over_sb) {
+                    that.last_val = that.val();
+                    that.sc.hide();
+                    setTimeout(function () {
+                        that.sc.hide();
+                    }, 350); // hide suggestions on fast input
+                } else if (!that.is(':focus')) setTimeout(function () {
+                    that.focus();
+                }, 20);
+            });
+
+            if (!o.minChars) that.on('focus.autocomplete', function () {
+                that.last_val = '\n';
+                that.trigger('keyup.autocomplete');
+            });
+
+            function suggest(data) {
+                var val = that.val();
+                that.cache[val] = data;
+                if (data.length && val.length >= o.minChars) {
+                    var s = '';
+                    if (data.length > 0) {
+                        s += typeof o.header === 'function' ? o.header.call(data, o, that) : o.header;
+                        for (var i = 0; i < data.length; i++) s += o.renderItem(data[i], val);
+                        s += typeof o.footer === 'function' ? o.footer.call(data, o, that) : o.footer;
+                    }
+                    that.sc.html(s);
+                    that.updateSC(0);
+                } else
+                    that.sc.hide();
+            }
+
+            that.on('keydown.autocomplete', function (e) {
+                // down (40), up (38)
+                if ((e.which == 40 || e.which == 38) && that.sc.html()) {
+                    var next, sel = $('.autocomplete-suggestion.selected', that.sc);
+                    if (!sel.length) {
+                        next = (e.which == 40) ? $('.autocomplete-suggestion', that.sc).first() : $('.autocomplete-suggestion', that.sc).last();
+                        that.val(next.addClass('selected').data('val'));
+                    } else {
+                        next = (e.which == 40) ? sel.next('.autocomplete-suggestion') : sel.prev('.autocomplete-suggestion');
+                        if (next.length) {
+                            sel.removeClass('selected');
+                            that.val(next.addClass('selected').data('val'));
+                        } else {
+                            sel.removeClass('selected');
+                            that.val(that.last_val);
+                            next = 0;
+                        }
+                    }
+                    that.updateSC(0, next);
+                    return false;
+                }
+                // esc
+                else if (e.which == 27) that.val(that.last_val).sc.hide();
+                // enter or tab
+                else if (e.which == 13 || e.which == 9) {
+                    var sel = $('.autocomplete-suggestion.selected', that.sc);
+                    if (sel.length && that.sc.is(':visible')) {
+                        o.onSelect(e, sel.data('val'), sel);
+                        setTimeout(function () {
+                            that.sc.hide();
+                        }, 20);
+                    }
+                }
+            });
+
+            that.on('keyup.autocomplete', function (e) {
+                if (!~$.inArray(e.which, [13, 27, 35, 36, 37, 38, 39, 40])) {
+                    var val = that.val();
+                    if (val.length >= o.minChars) {
+                        if (val != that.last_val) {
+                            that.last_val = val;
+                            clearTimeout(that.timer);
+                            if (o.cache) {
+                                if (val in that.cache) {
+                                    suggest(that.cache[val]);
+                                    return;
+                                }
+                                // no requests if previous suggestions were empty
+                                for (var i = 1; i < val.length - o.minChars; i++) {
+                                    var part = val.slice(0, val.length - i);
+                                    if (part in that.cache && !that.cache[part].length) {
+                                        suggest([]);
+                                        return;
+                                    }
+                                }
+                            }
+                            that.timer = setTimeout(function () {
+                                o.source(val, suggest)
+                            }, o.delay);
+                        }
+                    } else {
+                        that.last_val = val;
+                        that.sc.hide();
+                    }
+                }
+            });
+        });
+    }
+
+    $.fn.autoComplete.defaults = {
+        source: 0,
+        minChars: 3,
+        delay: 150,
+        cache: 1,
+        menuClass: '',
+        header: '',
+        footer: '',
+        renderItem: function (item, search) {
+            // escape special characters
+            search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+            var re = new RegExp("(" + search.split(' ').join('|') + ")", "gi");
+            return '<div class="autocomplete-suggestion" data-val="' + item + '">' + item.replace(re, "<b>$1</b>") + '</div>';
+        },
+        onSelect: function (e, term, item) {
+        }
+    };
+}(jQuery));

+ 152 - 0
addons/blog/assets/default/js/post.js

@@ -0,0 +1,152 @@
+var len = function (str) {
+    if (!str)
+        return 0;
+    var length = 0;
+    for (var i = 0; i < str.length; i++) {
+        if (str.charCodeAt(i) >= 0x4e00 && str.charCodeAt(i) <= 0x9fa5) {
+            length += 2;
+        } else {
+            length++;
+        }
+    }
+    return length;
+};
+var ci, si;
+$(function () {
+    $userinfo = localStorage.getItem("commentuserinfo");
+    $userinfo = JSON.parse($userinfo);
+    if ($userinfo && typeof $userinfo.username !== 'undefined') {
+        $("#username").val($userinfo.username);
+        $("#email").val($userinfo.email);
+        $("#website").val($userinfo.website);
+    }
+    var loadcomment = function (page) {
+        $("#respond h3 a").trigger("click");
+        $.ajax({
+            url: getcommentlist_url,
+            data: {post_id: post_id, page: page},
+            type: 'GET',
+            success: function (data) {
+                $(".commentlist").html(data);
+                $('html, body').animate({scrollTop: $('#comments').position().top}, 'slow');
+            }, error: function () {
+
+            }
+        });
+    };
+    $(document).on("click", ".pager a", function () {
+        var page = $(this).text();
+        loadcomment(page);
+        return false;
+    });
+    $(document).on("click", "#submit", function () {
+        var rememberme = $("input[name=rememberme]:checked").val() ? 1 : 0;
+        var btn = $(this);
+        var tips = $("#actiontips");
+        tips.removeClass();
+        var username = $("#username").val();
+        var email = $("#email").val();
+        var website = $("#website").val();
+        var content = $("#commentcontent").val();
+        if (len(username) < 3 || len(email) < 3 || len(content) < 3) {
+            tips.addClass("error").html("姓名、Email、评论内容长度不正确!最少3个字符").fadeIn().change();
+            return false;
+        }
+        btn.attr("disabled", "disabled");
+        tips.html('正在提交...');
+        $.ajax({
+            url: postcomment_url,
+            type: 'POST',
+            data: $("#postform").serialize(),
+            dataType: 'json',
+            success: function (json) {
+                btn.removeAttr("disabled");
+                if (json.code == 1) {
+                    if (rememberme) {
+                        localStorage.setItem("commentuserinfo", JSON.stringify({username: username, email: email, website: website}));
+                    } else {
+                        localStorage.removeItem("commentuserinfo");
+                    }
+                    $("#pid").val(0);
+                    tips.addClass("success").html(json.msg).fadeIn(300).change();
+                    $("#commentcontent").val('');
+                    loadcomment(1);
+                    $("#commentcount").text(parseInt($("#commentcount").text()) + 1);
+                } else {
+                    tips.addClass("error").html(json.msg).fadeIn().change();
+                }
+                if (json.data && json.data.token) {
+                    $("#postform input[name='__token__']").val(json.data.token);
+                }
+            },
+            error: function () {
+                btn.removeAttr("disabled");
+                tips.addClass("error").html("评论失败!请刷新页面重试!").fadeIn();
+            }
+        });
+        return false;
+    });
+    $("#commentcontent").on("keydown", function (e) {
+        if (e.ctrlKey && e.which == 13) {
+            $("#submit").click();
+        }
+    });
+    $("#actiontips").on("change", function () {
+        clearTimeout(si);
+        si = setTimeout(function () {
+            $("#actiontips").fadeOut();
+        }, 8000);
+    });
+    $(document).on("keyup change", "#commentcontent", function () {
+        var max = 1000;
+        var c = $(this).val();
+        var length = len(c);
+        var t = $("#actiontips");
+        if (max >= length) {
+            t.removeClass().show().addClass("loading").html("你还可以输入 <font color=green>" + (Math.floor((max - length) / 2)) + "</font> 字");
+            $("#submit").removeAttr("disabled");
+        } else {
+            t.removeClass().show().addClass("loading").html("你已经超出 <font color=red>" + (Math.ceil((length - max) / 2)) + "</font> 字");
+            $("#submit").attr("disabled", "disabled");
+        }
+    });
+    $(".commentlist dl dd div,.commentlist dl dd dl dd").on({
+        mouseenter: function () {
+            clearTimeout(ci);
+            var _this = this;
+            ci = setTimeout(function () {
+                $(_this).find("small:first").find("a").stop(true, true).fadeIn();
+            }, 300);
+        },
+        mouseleave: function () {
+            $(this).find("small:first").find("a").stop(true, true).fadeOut();
+        }
+    });
+    $(document).on("click", ".reply", function () {
+        $("#pid").val($(this).attr("rel"));
+        $(this).parent().parent().append($("div#respond").detach());
+        $("#respond h3 a").show();
+        $("#commentcontent").focus().val($(this).attr("title"));
+    });
+    $(document).on("click", "#respond h3 a", function () {
+        $(".commentlist").after($("div#respond").detach());
+        $(this).hide();
+    });
+    $(document).on("click", ".expandall a", function () {
+        $(this).parent().parent().find("dl.hide").fadeIn();
+        $(this).fadeOut();
+    });
+    //超过指定宽度
+    var nc = $(".entry");
+    if (nc.size() > 0) {
+        var nw = nc.width();
+        $("img", nc).each(function (i, obj) {
+            var iw = $(obj).removeAttr("height").width();
+            if (iw > nw) {
+                $(obj).width(nw).css("cursor", "pointer").attr("title", "点击查看大图片").bind('click', function () {
+                    window.open($(obj).attr("src"));
+                });
+            }
+        });
+    }
+});

BIN
addons/blog/assets/img/avatar.png


BIN
addons/blog/assets/img/logo.jpg


BIN
addons/blog/assets/img/qrcode.png


BIN
addons/blog/assets/img/thumb.png


+ 384 - 0
addons/blog/config.php

@@ -0,0 +1,384 @@
+<?php
+
+return array (
+  0 => 
+  array (
+    'name' => 'name',
+    'title' => '博客名称',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '趣味编程',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  1 => 
+  array (
+    'name' => 'enname',
+    'title' => '博客英文名称',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'Wisdom acceleration',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  2 => 
+  array (
+    'name' => 'theme',
+    'title' => '皮肤名称',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'default',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  3 => 
+  array (
+    'name' => 'keywords',
+    'title' => '关键字',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '任天堂switch加速器代理 ns加速器 ns游戏下载加速器 网络加速 ns商店游戏下载流量包 秒杀改DNS',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  4 => 
+  array (
+    'name' => 'description',
+    'title' => '描述',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '任天堂switch加速器代理 ns加速器 ns游戏下载加速器 网络加速 ns商店游戏下载流量包 秒杀改DNS',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  5 => 
+  array (
+    'name' => 'intro',
+    'title' => '个人简介',
+    'type' => 'text',
+    'content' => 
+    array (
+    ),
+    'value' => '任天堂switch加速器代理 ns加速器 ns游戏下载加速器 网络加速 ns商店游戏下载流量包 秒杀改DNS',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  6 => 
+  array (
+    'name' => 'listpagesize',
+    'title' => '列表每页显示数量',
+    'type' => 'number',
+    'content' => 
+    array (
+    ),
+    'value' => '10',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  7 => 
+  array (
+    'name' => 'commentpagesize',
+    'title' => '评论每页显示数量',
+    'type' => 'number',
+    'content' => 
+    array (
+    ),
+    'value' => '10',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  8 => 
+  array (
+    'name' => 'avatar',
+    'title' => '头像',
+    'type' => 'image',
+    'content' => 
+    array (
+    ),
+    'value' => '/assets/addons/blog/img/avatar.png',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  9 => 
+  array (
+    'name' => 'donate',
+    'title' => '打赏图片',
+    'type' => 'image',
+    'content' => 
+    array (
+    ),
+    'value' => '/assets/addons/blog/img/qrcode.png',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  10 => 
+  array (
+    'name' => 'logo',
+    'title' => 'Logo图片',
+    'type' => 'image',
+    'content' => 
+    array (
+    ),
+    'value' => '/uploads/20200521/11c6b61ba1df524d1bb939dc97b70dd1.jpg',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  11 => 
+  array (
+    'name' => 'background',
+    'title' => '背景图片',
+    'type' => 'image',
+    'content' => 
+    array (
+    ),
+    'value' => 'https://cdn.fastadmin.net/uploads/20180507/1a81b9aaa3d52367b02b844e6437cf74.jpg',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  12 => 
+  array (
+    'name' => 'copyright',
+    'title' => '底部版权信息',
+    'type' => 'text',
+    'content' => 
+    array (
+    ),
+    'value' => 'Copyright @ 2017~2019 Theme design by jeanstudio',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  13 => 
+  array (
+    'name' => 'domain',
+    'title' => '绑定二级域名前缀',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  14 => 
+  array (
+    'name' => 'iscommentaudit',
+    'title' => '发表评论审核',
+    'type' => 'radio',
+    'content' => 
+    array (
+      1 => '全部审核',
+      0 => '无需审核',
+      -1 => '仅含有过滤词时审核',
+    ),
+    'value' => '-1',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  15 => 
+  array (
+    'name' => 'audittype',
+    'title' => '审核方式',
+    'type' => 'radio',
+    'content' => 
+    array (
+      'local' => '本地',
+      'baiduyun' => '百度云',
+    ),
+    'value' => 'local',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '如果启用百度云,请输入百度云AI平台应用的AK和SK',
+    'ok' => '',
+    'extend' => '',
+  ),
+  16 => 
+  array (
+    'name' => 'aip_appid',
+    'title' => '百度AI平台应用Appid',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '百度云AI开放平台应用AppId',
+    'ok' => '',
+    'extend' => '',
+  ),
+  17 => 
+  array (
+    'name' => 'aip_apikey',
+    'title' => '百度AI平台应用Apikey',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '百度云AI开放平台应用ApiKey',
+    'ok' => '',
+    'extend' => '',
+  ),
+  18 => 
+  array (
+    'name' => 'aip_secretkey',
+    'title' => '百度AI平台应用Secretkey',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => '',
+    'rule' => '',
+    'msg' => '',
+    'tip' => '百度云AI开放平台应用Secretkey',
+    'ok' => '',
+    'extend' => '',
+  ),
+  19 => 
+  array (
+    'name' => 'searchtype',
+    'title' => '搜索方式',
+    'type' => 'radio',
+    'content' => 
+    array (
+      'local' => '本地搜索,采用Like(无需配置,效率低)',
+      'xunsearch' => '采用Xunsearch全文搜索(需安装插件+配置)',
+    ),
+    'value' => 'local',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '如果启用Xunsearch全文搜索,需安装Xunsearch插件并配置Xunsearch服务器',
+    'ok' => '',
+    'extend' => '',
+  ),
+  20 => 
+  array (
+    'name' => 'baidupush',
+    'title' => '百度熊掌号+百度站长推送',
+    'type' => 'radio',
+    'content' => 
+    array (
+      1 => '开启',
+      0 => '关闭',
+    ),
+    'value' => '0',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '如果开启百度熊掌+百度站长推送,将在文章发布时自动进行推送',
+    'ok' => '',
+    'extend' => '',
+  ),
+  21 => 
+  array (
+    'name' => 'pagemode',
+    'title' => '分页模式',
+    'type' => 'radio',
+    'content' => 
+    array (
+      'normal' => '普通分页模式',
+      'infinite' => '采用无限加载更多模式',
+    ),
+    'value' => 'infinite',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  22 => 
+  array (
+    'name' => 'commentavatarmode',
+    'title' => '评论头像模式',
+    'type' => 'radio',
+    'content' => 
+    array (
+      'letter' => '根据名称使用字母头像',
+      'gravatar' => '根据Email使用Gravatar头像',
+    ),
+    'value' => 'letter',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  23 => 
+  array (
+    'name' => 'rewrite',
+    'title' => '伪静态',
+    'type' => 'array',
+    'content' => 
+    array (
+    ),
+    'value' => 
+    array (
+      'index/index' => '/blog$',
+      'archive/index' => '/blog/archive',
+      'search/index' => '/blog/search',
+      'category/index' => '/blog/c/[:diyname]',
+      'post/index' => '/blog/a/[:catename]/[:diyname]',
+    ),
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+);

+ 20 - 0
addons/blog/controller/Ajax.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace addons\blog\controller;
+
+use think\addons\Controller;
+
+/**
+ * Ajax
+ */
+class Ajax extends Controller
+{
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+    }
+
+}

+ 30 - 0
addons/blog/controller/Archive.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace addons\blog\controller;
+
+use addons\blog\model\Post;
+
+/**
+ * 博客归档
+ */
+class Archive extends Base
+{
+
+    public function index()
+    {
+        $postlist = Post::where('status', 'normal')
+            ->with(['category'])
+            ->field('id,title,createtime,diyname,category_id')
+            ->order("createtime", "desc")
+            ->cache(3600 * 365)
+            ->select();
+        $yearlist = [];
+        foreach ($postlist as $k => $v) {
+            $yearlist[date("Y", $v['createtime'])][] = ['id' => $v['id'], 'title' => $v['title'], 'url' => $v['url']];
+        }
+        $this->view->assign('yearlist', $yearlist);
+        $this->view->assign('title', '日志归档');
+        return $this->view->fetch('/archive');
+    }
+
+}

+ 29 - 0
addons/blog/controller/Base.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace addons\blog\controller;
+
+use think\addons\Controller;
+use think\Config;
+
+/**
+ * Blog控制器基类
+ */
+class Base extends Controller
+{
+
+    // 初始化
+    public function __construct()
+    {
+        parent::__construct();
+        $config = get_addon_config('blog');
+        // 设定主题模板目录
+        $this->view->engine->config('view_path', $this->view->engine->config('view_path') . $config['theme'] . DS);
+        // 加载自定义标签库
+        $this->view->engine->config('taglib_pre_load', 'addons\blog\taglib\Blog');
+        $config['indexurl'] = addon_url('blog/index/index', [], false);
+        $categorylist = \addons\blog\model\Category::where('status', 'normal')->order('weigh desc,id desc')->cache(true)->select();
+        $this->view->assign("categorylist", $categorylist);
+        Config::set('blog', $config);
+    }
+
+}

+ 53 - 0
addons/blog/controller/Category.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace addons\blog\controller;
+
+use addons\blog\model\Category as CategoryModel;
+use addons\blog\model\Comment;
+use addons\blog\model\Post;
+use think\Paginator;
+
+/**
+ * 博客分类
+ */
+class Category extends Base
+{
+
+    public function index()
+    {
+        $diyname = $this->request->param('diyname');
+        if ($diyname && !is_numeric($diyname)) {
+            $category = CategoryModel::getByDiyname($diyname);
+        } else {
+            $id = $diyname ? $diyname : $this->request->param('id', '');
+            $category = CategoryModel::get($id);
+        }
+        if (!$category || $category['status'] != 'normal') {
+            $this->error("分类未找到");
+        }
+
+        $postlist = Post::where(['status' => 'normal'])
+            ->where('category_id', $category['id'])
+            ->with('category')
+            ->order('weigh desc,id desc')
+            ->paginate($this->view->config['listpagesize'], false, ['type' => '\\addons\\blog\\library\\Bootstrap']);
+
+        $page = Paginator::getCurrentPage();
+        $urls = $postlist->getUrlRange($page - 1, $page + 1);
+        $prevurl = $page == 1 ? '' : array_shift($urls);
+        $nexturl = $page == $postlist->lastPage() ? '' : array_pop($urls);
+
+        $this->view->assign("postlist", $postlist);
+        $this->view->assign('prevurl', $prevurl);
+        $this->view->assign('nexturl', $nexturl);
+        $this->view->assign('category', $category);
+        $this->view->assign('title', $category['name']);
+        $this->view->assign('keywords', $category['keywords']);
+        $this->view->assign('description', $category['description']);
+        if ($this->request->isAjax()) {
+            return $this->view->fetch('/common/postlist');
+        }
+        return $this->view->fetch('/category');
+    }
+
+}

+ 73 - 0
addons/blog/controller/Comment.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace addons\blog\controller;
+
+use addons\blog\model\Comment as CommentModel;
+use addons\blog\library\CommentException;
+use think\Exception;
+
+/**
+ * 博客评论
+ */
+class Comment extends Base
+{
+
+    public function index()
+    {
+        $post_id = $this->request->get('post_id');
+        $commentlist = CommentModel::where(['post_id' => $post_id, 'pid' => 0, 'status' => 'normal'])
+            ->with('sublist')
+            ->order('id desc')
+            ->paginate($this->view->config['commentpagesize']);
+        $this->view->assign("commentlist", $commentlist);
+        return $this->view->fetch('/common/commentlist');
+    }
+
+    /**
+     * 发表评论
+     */
+    public function post()
+    {
+        $result = false;
+        $message = '';
+        try {
+            $params = $this->request->post();
+            $result = CommentModel::postComment($params);
+        } catch (CommentException $e) {
+            if ($e->getCode() == 1) {
+                $result = true;
+            }
+            $message = $e->getMessage();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+        }
+        if ($result) {
+            $this->success(__($message), null, ['token' => $this->request->token()]);
+        } else {
+            $this->error(__($message), null, ['token' => $this->request->token()]);
+        }
+    }
+
+    /**
+     * 取消评论订阅
+     */
+    public function unsubscribe()
+    {
+        $id = (int)$this->request->param('id');
+        $key = $this->request->param('key');
+        $comment = Comment::get($id);
+        if (!$comment) {
+            $this->error("日志评论未找到");
+        }
+        if ($key !== md5($comment['id'] . $comment['email'])) {
+            $this->error("无法进行该操作");
+        }
+        if (!$comment['subscribe']) {
+            $this->error("评论已经取消订阅,请勿重复操作");
+        }
+        $comment->subscribe = 0;
+        $comment->save();
+        $this->success('取消评论订阅成功');
+    }
+
+}

+ 38 - 0
addons/blog/controller/Index.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace addons\blog\controller;
+
+use addons\blog\model\Post;
+use think\Paginator;
+
+/**
+ * 博客首页
+ */
+class Index extends Base
+{
+
+    public function index()
+    {
+        $postlist = Post::where(['status' => 'normal'])
+            ->with('category')
+            ->order('weigh desc,id desc')
+            ->paginate($this->view->config['listpagesize'], false, ['type' => '\\addons\\blog\\library\\Bootstrap']);
+        $page = Paginator::getCurrentPage();
+        $urls = $postlist->getUrlRange($page - 1, $page + 1);
+        $prevurl = $page == 1 ? '' : array_shift($urls);
+        $nexturl = $page == $postlist->lastPage() ? '' : array_pop($urls);
+
+        $this->view->assign("postlist", $postlist);
+        $this->view->assign('prevurl', $prevurl);
+        $this->view->assign('nexturl', $nexturl);
+        if ($this->request->isAjax()) {
+            return $this->view->fetch('/common/postlist');
+        }
+
+        $config = get_addon_config('blog');
+        $this->view->assign('keywords', $config['keywords']);
+        $this->view->assign('description', $config['description']);
+        return $this->view->fetch('/index');
+    }
+
+}

+ 47 - 0
addons/blog/controller/Post.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace addons\blog\controller;
+
+use addons\blog\model\Category;
+use addons\blog\model\Comment;
+use addons\blog\model\Post as PostModel;
+
+/**
+ * 博客详情
+ */
+class Post extends Base
+{
+
+    public function index()
+    {
+        $diyname = $this->request->param('diyname');
+        if ($diyname && !is_numeric($diyname)) {
+            $post = PostModel::getByDiyname($diyname);
+
+        } else {
+            $id = $diyname ? $diyname : $this->request->param('id', '');
+            $post = PostModel::get($id);
+        }
+        if (!$post || $post['status'] != 'normal') {
+            $this->error("日志未找到");
+        }
+        $post->setInc('views');
+
+        $category = Category::get($post['category_id']);
+
+        $commentlist = Comment::where(['post_id' => $post->id, 'pid' => 0, 'status' => 'normal'])
+            ->with('sublist')
+            ->order('id desc')
+            ->paginate($this->view->config['commentpagesize']);
+
+        $post->category = $category;
+        $this->view->assign("post", $post);
+        $this->view->assign("category", $category);
+        $this->view->assign("commentlist", $commentlist);
+        $this->view->assign("title", isset($post['seotitle']) && $post['seotitle'] ? $post['seotitle'] : $post['title']);
+        $this->view->assign("keywords", $post['keywords']);
+        $this->view->assign("description", $post['description']);
+        return $this->view->fetch('/post');
+    }
+
+}

+ 140 - 0
addons/blog/controller/Search.php

@@ -0,0 +1,140 @@
+<?php
+
+namespace addons\blog\controller;
+
+use addons\blog\library\FulltextSearch;
+
+/**
+ * 搜索控制器
+ * Class Search
+ * @package addons\blog\controller
+ */
+class Search extends Base
+{
+    public function index()
+    {
+        $config = get_addon_config('blog');
+        if ($config['searchtype'] == 'xunsearch') {
+            return $this->xunsearch();
+        }
+        $q = $this->request->get('q', $this->request->get('search', ''));
+        $filterlist = [];
+        $orderlist = [];
+
+        $orderby = $this->request->get('orderby', '');
+        $orderway = $this->request->get('orderway', '', 'strtolower');
+        $params = ['q' => $q];
+        if ($orderby) {
+            $params['orderby'] = $orderby;
+        }
+        if ($orderway) {
+            $params['orderway'] = $orderway;
+        }
+
+        $sortrank = [
+            ['name' => 'default', 'field' => 'weigh', 'title' => __('Default')],
+            ['name' => 'views', 'field' => 'views', 'title' => __('Views')],
+            ['name' => 'id', 'field' => 'id', 'title' => __('Post date')],
+        ];
+
+        $orderby = $orderby && in_array($orderby, ['default', 'id', 'views']) ? $orderby : 'default';
+        $orderway = $orderway ? $orderway : 'desc';
+        foreach ($sortrank as $k => $v) {
+            $url = '?' . http_build_query(array_merge($params, ['orderby' => $v['name'], 'orderway' => ($orderway == 'desc' ? 'asc' : 'desc')]));
+            $v['active'] = $orderby == $v['name'] ? true : false;
+            $v['orderby'] = $orderway;
+            $v['url'] = $url;
+            $orderlist[] = $v;
+        }
+        $orderby = $orderby == 'default' ? 'weigh' : $orderby;
+
+        $postlist = \addons\blog\model\Post::where('status', 'normal')
+            ->where('title', 'like', "%{$q}%")
+            ->with(['category'])
+            ->order($orderby, $orderway)
+            ->paginate($this->view->config['listpagesize'], false, ['type' => '\\addons\\blog\\library\\Bootstrap']);
+        $postlist->appends($params);
+
+        $this->view->assign("orderlist", $orderlist);
+        $this->view->assign("postlist", $postlist);
+        $this->view->assign('title', __("Search for %s", $q));
+        if ($this->request->isAjax()) {
+            return $this->view->fetch('/common/postlist');
+        }
+        return $this->view->fetch('/search');
+    }
+
+    /**
+     * Xunsearch搜索
+     * @return string
+     * @throws \think\Exception
+     */
+    public function xunsearch()
+    {
+        $orderList = [
+            'relevance'       => '默认排序',
+            'createtime_desc' => '发布时间从新到旧',
+            'createtime_asc'  => '发布时间从旧到新',
+            'views_desc'      => '浏览次数从多到少',
+            'views_asc'       => '浏览次数从少到多',
+            'comments_desc'   => '评论次数从多到少',
+            'comments_asc'    => '评论次数从少到多',
+        ];
+
+        $q = $this->request->get('q', $this->request->get('search', ''));
+        $page = $this->request->get('page/d', '1', 'trim');
+        $order = $this->request->get('order', '', 'trim');
+        $fulltext = $this->request->get('fulltext/d', '1', 'trim');
+        $fuzzy = $this->request->get('fuzzy/d', '0', 'trim');
+        $synonyms = $this->request->get('synonyms/d', '0', 'trim');
+
+        $total_begin = microtime(true);
+        $search = null;
+        $pagesize = 10;
+
+        $result = FulltextSearch::search($q, $page, $pagesize, $order, $fulltext, $fuzzy, $synonyms);
+
+        // 计算总耗时
+        $total_cost = microtime(true) - $total_begin;
+
+        //获取热门搜索
+        $hot = FulltextSearch::hot();
+
+        $data = [
+            'q'           => $q,
+            'error'       => '',
+            'total'       => $result['total'],
+            'count'       => $result['count'],
+            'search_cost' => $result['microseconds'],
+            'docs'        => $result['list'],
+            'pager'       => $result['pager'],
+            'corrected'   => $result['corrected'],
+            'highlight'   => $result['highlight'],
+            'related'     => $result['related'],
+            'search'      => $search,
+            'fulltext'    => $fulltext,
+            'synonyms'    => $synonyms,
+            'fuzzy'       => $fuzzy,
+            'order'       => $order,
+            'orderList'   => $orderList,
+            'hot'         => $hot,
+            'total_cost'  => $total_cost,
+        ];
+
+        \think\Config::set('blog.title', __("Search for %s", $q));
+        $this->view->assign("title", $q);
+        $this->view->assign($data);
+        return $this->view->fetch('/xunsearch');
+    }
+
+    public function suggestion()
+    {
+        $q = trim($this->request->get('q', ''));
+        $terms = [];
+        $config = get_addon_config('blog');
+        if ($config['searchtype'] == 'xunsearch') {
+            $terms = FulltextSearch::suggestion($q);
+        }
+        return json($terms);
+    }
+}

+ 43 - 0
addons/blog/controller/Sitemap.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace addons\blog\controller;
+
+use think\Config;
+
+/**
+ * Sitemap控制器
+ * Class Sitemap
+ * @package addons\blog\controller
+ */
+class Sitemap extends Base
+{
+    protected $noNeedLogin = ['*'];
+    protected $options = [
+        'item_key'  => '',
+        'root_node' => 'urlset',
+        'item_node' => 'url',
+        'root_attr' => 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:mobile="http://www.baidu.com/schemas/sitemap-mobile/1/"'
+    ];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        Config::set('default_return_type', 'xml');
+    }
+
+    /**
+     * Sitemap
+     */
+    public function index()
+    {
+        $postList = \addons\blog\model\Post::where('status', 'normal')->cache(3600)->field('id,category_id,createtime')->paginate(500000);
+        $list = [];
+        foreach ($postList as $index => $item) {
+            $list[] = [
+                'loc'      => $item->fullurl,
+                'priority' => 0.8
+            ];
+        }
+        return xml($list, 200, [], $this->options);
+    }
+}

+ 42 - 0
addons/blog/controller/wxapp/Archive.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace addons\blog\controller\wxapp;
+
+use addons\blog\model\Post;
+
+/**
+ * 归档
+ */
+class Archive extends Base
+{
+
+    protected $noNeedLogin = '*';
+
+    /**
+     * 首页
+     */
+    public function index()
+    {
+        $postlist = Post::where('status', 'normal')
+            ->field('id,title,createtime')
+            ->cache(3600 * 365)
+            ->order('weigh desc,id desc')
+            ->select();
+        $list = [];
+        foreach ($postlist as $k => $v) {
+            $list[date("Y", $v['createtime'])][] = ['id' => $v['id'], 'title' => $v['title']];
+        }
+        $archiveList = [];
+        foreach ($list as $index => $item) {
+            $archiveList[] = [
+                'title'    => $index . '年',
+                'postList' => $item
+            ];
+        }
+        $data = [
+            'archiveList' => $archiveList
+        ];
+        $this->success('', $data);
+    }
+
+}

+ 23 - 0
addons/blog/controller/wxapp/Base.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace addons\blog\controller\wxapp;
+
+use app\common\controller\Api;
+use think\Lang;
+
+class Base extends Api
+{
+
+    protected $noNeedLogin = ['*'];
+    protected $noNeedRight = ['*'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+
+        //这里手动载入语言包
+        Lang::load(ROOT_PATH . '/addons/blog/lang/zh-cn.php');
+        Lang::load(APP_PATH . '/index/lang/zh-cn/user.php');
+    }
+
+}

+ 61 - 0
addons/blog/controller/wxapp/Comment.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace addons\blog\controller\wxapp;
+
+use addons\blog\library\CommentException;
+use addons\blog\model\Comment as CommentModel;
+use think\Config;
+use think\Exception;
+
+/**
+ * 评论
+ */
+class Comment extends Base
+{
+
+    protected $noNeedLogin = ['*'];
+
+    /**
+     * 评论列表
+     */
+    public function index()
+    {
+        $post_id = (int)$this->request->request('post_id/d');
+        $page = (int)$this->request->request('page/d', 1);
+        Config::set('paginate.page', $page);
+        $commentList = \addons\blog\model\Comment::where('post_id', $post_id)
+            ->order('createtime', 'desc')
+            ->where('status', 'normal')
+            ->page($page)->select();
+        foreach ($commentList as $index => $item) {
+            $item->visible(['id', 'username', 'avatar', 'content', 'comments']);
+        }
+        $this->success('', ['commentList' => $commentList]);
+    }
+
+    /**
+     * 发表评论
+     */
+    public function post()
+    {
+        $result = false;
+        $message = '';
+        try {
+            $params = $this->request->post();
+            $result = CommentModel::postComment($params);
+        } catch (CommentException $e) {
+            if ($e->getCode() == 1) {
+                $result = true;
+            }
+            $message = $e->getMessage();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+        }
+        if ($result) {
+            $this->success(__($message ? $message : "发布成功"), ['token' => $this->request->token()]);
+        } else {
+            $this->error(__($message ? $message : "发布失败"), ['token' => $this->request->token()]);
+        }
+    }
+
+}

+ 46 - 0
addons/blog/controller/wxapp/Common.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace addons\blog\controller\wxapp;
+
+use addons\blog\model\Category;
+use think\Config;
+
+/**
+ * 公共
+ */
+class Common extends Base
+{
+
+    protected $noNeedLogin = '*';
+
+    /**
+     * 初始化
+     */
+    public function init()
+    {
+
+        //首页Tab列表
+        $tabList = [['id' => 0, 'title' => '全部']];
+        $channelList = Category::where('status', 'normal')
+            ->field('id,pid,name,nickname,diyname')
+            ->order('weigh desc,id desc')
+            ->select();
+        foreach ($channelList as $index => $item) {
+            $tabList[] = ['id' => $item['id'], 'title' => $item['name']];
+        }
+
+        //配置信息
+        $upload = Config::get('upload');
+        $upload['cdnurl'] = $upload['cdnurl'] ? $upload['cdnurl'] : cdnurl('', true);
+        $upload['uploadurl'] = $upload['uploadurl'] == 'ajax/upload' ? cdnurl('/ajax/upload', true) : $upload['cdnurl'];
+        $config = [
+            'upload' => $upload
+        ];
+
+        $data = [
+            'tabList' => $tabList,
+            'config'  => $config
+        ];
+        $this->success('', $data);
+    }
+}

+ 57 - 0
addons/blog/controller/wxapp/Index.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace addons\blog\controller\wxapp;
+
+use addons\blog\model\Category;
+use addons\blog\model\Post;
+
+/**
+ * 首页
+ */
+class Index extends Base
+{
+
+    protected $noNeedLogin = '*';
+
+    /**
+     * 首页
+     */
+    public function index()
+    {
+        //焦点图
+        $bannerList = Post::where('status', 'normal')
+            ->where("FIND_IN_SET( 'index',`flag`)")
+            ->field('id,title,image,createtime')
+            ->limit(4)
+            ->select();
+
+        $tabList = [
+            ['id' => 0, 'title' => '全部'],
+        ];
+        $categoryList = Category::where('status', 'normal')
+            ->field('id,pid,name,nickname,diyname')
+            ->order('weigh desc,id desc')
+            ->cache(false)
+            ->select();
+        foreach ($categoryList as $index => $item) {
+            $tabList[] = ['id' => $item['id'], 'title' => $item['name']];
+        }
+        $postList = Post::
+        with('category')
+            ->where('status', 'normal')
+            ->field('id,category_id,title,image,summary,createtime')
+            ->page(1)
+            ->order('weigh desc,id desc')
+            ->select();
+        foreach ($postList as $index => &$item) {
+            $item['summary'] = trim(strip_tags($item['summary']));
+        }
+        $data = [
+            'bannerList' => $bannerList,
+            'tabList'    => $tabList,
+            'postList'   => $postList,
+        ];
+        $this->success('', $data);
+    }
+
+}

+ 28 - 0
addons/blog/controller/wxapp/Page.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace addons\blog\controller\wxapp;
+
+/**
+ * 单页
+ */
+class Page extends Base
+{
+
+    protected $noNeedLogin = ['*'];
+
+    /**
+     * 关于我
+     */
+    public function aboutme()
+    {
+        $config = get_addon_config('blog');
+        $my = [
+            'avatar' => cdnurl($config['avatar'], true),
+            'name'   => $config['name'],
+            'enname' => $config['enname'],
+            'intro'  => $config['intro'],
+        ];
+        $this->success('', ['my' => $my]);
+    }
+
+}

+ 70 - 0
addons/blog/controller/wxapp/Post.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace addons\blog\controller\wxapp;
+
+use addons\blog\model\Category;
+use addons\blog\model\Post as PostModel;
+use addons\blog\model\Comment;
+
+/**
+ * 日志
+ */
+class Post extends Base
+{
+
+    protected $noNeedLogin = ['*'];
+
+    /**
+     * 日志列表
+     */
+    public function index()
+    {
+        $category = (int)$this->request->request('category/d');
+        $page = (int)$this->request->request('page/d', 1);
+
+        $page = max(1, $page);
+
+        $postList = PostModel::with('category')
+            ->where('status', 'normal')
+            ->where($category ? "category_id='{$category}'" : "1=1")
+            ->field('id,category_id,title,summary,diyname,image,createtime')
+            ->page($page)
+            ->order('weigh desc,id desc')
+            ->select();
+        foreach ($postList as $index => &$item) {
+            $item['summary'] = mb_substr(trim(strip_tags($item['summary'])), 0, 100);
+        }
+        $this->success('', ['postList' => $postList]);
+    }
+
+    /**
+     * 日志详情
+     */
+    public function detail()
+    {
+        $id = $this->request->request('id/d');
+        $post = PostModel::get($id);
+        if (!$post || $post['status'] == 'hidden') {
+            $this->error(__('No specified article found'));
+        }
+        $category = Category::get($post['category_id']);
+        if (!$category) {
+            $this->error(__('No specified channel found'));
+        }
+        $post->setInc("views", 1);
+
+        $commentList = Comment::where('post_id', $post->id)
+            ->order('createtime', 'desc')
+            ->where('status', 'normal')
+            ->limit(10)
+            ->select();
+        foreach ($commentList as $index => $item) {
+            $item->visible(['id', 'username', 'avatar', 'content', 'comments']);
+        }
+        $category->visible(['id', 'name']);
+        $post->visible(['id', 'title', 'comments', 'content', 'description', 'image', 'summary', 'thumb', 'url', 'views']);
+        $this->request->token();
+        $this->success('', ['postInfo' => $post, 'categoryInfo' => $category, 'commentList' => $commentList]);
+    }
+
+}

+ 1 - 0
addons/blog/data/words.dic

@@ -0,0 +1 @@
+一行一个过滤词

+ 8 - 0
addons/blog/info.ini

@@ -0,0 +1,8 @@
+name = blog
+title = 简洁响应式博客系统
+intro = 响应式博客插件,包含日志、评论、分类、归档等
+author = Karson
+website = https://www.fastadmin.net
+version = 1.1.6
+state = 1
+url = /addons/blog

File diff suppressed because it is too large
+ 135 - 0
addons/blog/install.sql


+ 13 - 0
addons/blog/lang/zh-cn.php

@@ -0,0 +1,13 @@
+<?php
+
+return [
+    '%d second%s ago' => '%d秒前',
+    '%d minute%s ago' => '%d分钟前',
+    '%d hour%s ago'   => '%d小时前',
+    '%d day%s ago'    => '%d天前',
+    '%d week%s ago'   => '%d周前',
+    '%d month%s ago'  => '%d月前',
+    '%d year%s ago'   => '%d年前',
+    'Search for %s'   => '查找 “%s”',
+    'Search more %s'  => '查找更多 “%s”',
+];

+ 215 - 0
addons/blog/library/Bootstrap.php

@@ -0,0 +1,215 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: zhangyajun <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace addons\blog\library;
+
+use think\Paginator;
+
+class Bootstrap extends Paginator
+{
+
+    /**
+     * 上一页按钮
+     * @param string $text
+     * @return string
+     */
+    protected function getPreviousButton($text = "&laquo;")
+    {
+        if ($this->currentPage() <= 1) {
+            return $this->getDisabledTextWrapper($text);
+        }
+
+        $url = $this->url(
+            $this->currentPage() - 1
+        );
+
+        return $this->getPageLinkWrapper($url, $text);
+    }
+
+    /**
+     * 下一页按钮
+     * @param string $text
+     * @return string
+     */
+    protected function getNextButton($text = '&raquo;')
+    {
+        if (!$this->hasMore) {
+            return $this->getDisabledTextWrapper($text);
+        }
+
+        $url = $this->url($this->currentPage() + 1);
+
+        return $this->getPageLinkWrapper($url, $text);
+    }
+
+    /**
+     * 页码按钮
+     * @return string
+     */
+    protected function getLinks()
+    {
+        if ($this->simple) {
+            return '';
+        }
+
+        $block = [
+            'first'  => null,
+            'slider' => null,
+            'last'   => null
+        ];
+
+        $side = 3;
+        $window = $side * 2;
+
+        if ($this->lastPage < $window + 6) {
+            $block['first'] = $this->getUrlRange(1, $this->lastPage);
+        } elseif ($this->currentPage <= $window) {
+            $block['first'] = $this->getUrlRange(1, $window + 2);
+            $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+        } elseif ($this->currentPage > ($this->lastPage - $window)) {
+            $block['first'] = $this->getUrlRange(1, 2);
+            $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage);
+        } else {
+            $block['first'] = $this->getUrlRange(1, 2);
+            $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side);
+            $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+        }
+
+        $html = '';
+
+        if (is_array($block['first'])) {
+            $html .= $this->getUrlLinks($block['first']);
+        }
+
+        if (is_array($block['slider'])) {
+            $html .= $this->getDots();
+            $html .= $this->getUrlLinks($block['slider']);
+        }
+
+        if (is_array($block['last'])) {
+            $html .= $this->getDots();
+            $html .= $this->getUrlLinks($block['last']);
+        }
+
+        return $html;
+    }
+
+    /**
+     * 渲染分页html
+     * @return mixed
+     */
+    public function render($params = null)
+    {
+        if (is_array($params)) {
+            if (isset($params['type'])) {
+                $this->simple = $params['type'] === 'simple';
+            }
+        }
+        if ($this->hasPages()) {
+            if ($this->simple) {
+                return sprintf(
+                    '<ul class="pager">%s %s</ul>',
+                    $this->getPreviousButton(),
+                    $this->getNextButton()
+                );
+            } else {
+                return sprintf(
+                    '<ul class="pagination">%s %s %s</ul>',
+                    $this->getPreviousButton(),
+                    $this->getLinks(),
+                    $this->getNextButton()
+                );
+            }
+        }
+    }
+
+    public function getNextPage()
+    {
+        return $this->currentPage + 1;
+    }
+
+    /**
+     * 生成一个可点击的按钮
+     *
+     * @param  string $url
+     * @param  int    $page
+     * @return string
+     */
+    protected function getAvailablePageWrapper($url, $page)
+    {
+        return '<li><a href="' . htmlentities($url) . '">' . $page . '</a></li>';
+    }
+
+    /**
+     * 生成一个禁用的按钮
+     *
+     * @param  string $text
+     * @return string
+     */
+    protected function getDisabledTextWrapper($text)
+    {
+        return '<li class="disabled"><span>' . $text . '</span></li>';
+    }
+
+    /**
+     * 生成一个激活的按钮
+     *
+     * @param  string $text
+     * @return string
+     */
+    protected function getActivePageWrapper($text)
+    {
+        return '<li class="active"><span>' . $text . '</span></li>';
+    }
+
+    /**
+     * 生成省略号按钮
+     *
+     * @return string
+     */
+    protected function getDots()
+    {
+        return $this->getDisabledTextWrapper('...');
+    }
+
+    /**
+     * 批量生成页码按钮.
+     *
+     * @param  array $urls
+     * @return string
+     */
+    protected function getUrlLinks(array $urls)
+    {
+        $html = '';
+
+        foreach ($urls as $page => $url) {
+            $html .= $this->getPageLinkWrapper($url, $page);
+        }
+
+        return $html;
+    }
+
+    /**
+     * 生成普通页码按钮
+     *
+     * @param  string $url
+     * @param  int    $page
+     * @return string
+     */
+    protected function getPageLinkWrapper($url, $page)
+    {
+        if ($page == $this->currentPage()) {
+            return $this->getActivePageWrapper($page);
+        }
+
+        return $this->getAvailablePageWrapper($url, $page);
+    }
+}

+ 18 - 0
addons/blog/library/CommentException.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace addons\blog\library;
+
+
+use think\Exception;
+use Throwable;
+
+class CommentException extends Exception
+{
+    public function __construct($message = "", $code = 0, $data = [])
+    {
+        $this->message = $message;
+        $this->code = $code;
+        $this->data = $data;
+    }
+
+}

+ 133 - 0
addons/blog/library/FulltextSearch.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace addons\blog\library;
+
+use addons\xunsearch\library\Xunsearch;
+use think\Config;
+use think\Exception;
+use think\View;
+
+class FulltextSearch
+{
+
+    public static function config()
+    {
+        $data = [
+            [
+                'name'   => 'blog',
+                'title'  => '简洁博客系统',
+                'fields' => [
+                    ['name' => 'pid', 'type' => 'id', 'title' => '主键'],
+                    ['name' => 'id', 'type' => 'numeric', 'title' => 'ID'],
+                    ['name' => 'title', 'type' => 'title', 'title' => '标题'],
+                    ['name' => 'content', 'type' => 'body', 'title' => '内容',],
+                    ['name' => 'type', 'type' => 'string', 'title' => '类型', 'index' => 'self'],
+                    ['name' => 'url', 'type' => 'string', 'title' => '链接',],
+                    ['name' => 'createtime', 'type' => 'date', 'title' => '发布日期',],
+                    ['name' => 'views', 'type' => 'numeric', 'title' => '浏览次数',],
+                    ['name' => 'comments', 'type' => 'numeric', 'title' => '评论次数',],
+                ]
+            ]
+        ];
+        return $data;
+    }
+
+    /**
+     * 重置搜索索引数据库
+     */
+    public static function reset()
+    {
+        \addons\blog\model\Post::where('status', 'normal')->chunk(100, function ($list) {
+            foreach ($list as $item) {
+                self::add($item);
+            }
+        });
+        return true;
+    }
+
+    /**
+     * 添加索引
+     * @param $row
+     */
+    public static function add($row)
+    {
+        self::update($row, true);
+    }
+
+    /**
+     * 更新索引
+     * @param      $row
+     * @param bool $add
+     */
+    public static function update($row, $add = false)
+    {
+        if (is_numeric($row)) {
+            $row = \addons\blog\model\Post::get($row);
+            if (!$row) {
+                return;
+            }
+        }
+        if (isset($row['status']) && $row['status'] != 'normal') {
+            self::del($row);
+            return;
+        }
+        $data = [];
+        if ($row instanceof \addons\blog\model\Post || $row instanceof \app\admin\model\BlogPost) {
+            $data['id'] = isset($row['id']) ? $row['id'] : 0;
+            $data['title'] = isset($row['title']) ? $row['title'] : '';
+            $data['content'] = isset($row['content']) ? strip_tags($row['content']) : '';
+            $data['type'] = isset($row['type']) ? $row['type'] : '';
+            $data['url'] = isset($row['fullurl']) ? $row['fullurl'] : addon_url('blog/post/index', [':id' => $data['id']], true, true);
+            $data['createtime'] = isset($row['createtime']) ? $row['createtime'] : '';
+            $data['views'] = isset($row['views']) ? $row['views'] : '0';
+            $data['comments'] = isset($row['comments']) ? $row['comments'] : '0';
+            $data['pid'] = "p" . $data['id'];
+        }
+        if ($data) {
+            Xunsearch::instance('blog')->update($data, $add);
+        }
+    }
+
+    /**
+     * 删除
+     * @param $row
+     */
+    public static function del($row)
+    {
+        $pid = "p" . (is_numeric($row) ? $row : ($row && isset($row['id']) ? $row['id'] : 0));
+        if ($pid) {
+            Xunsearch::instance('blog')->del($pid);
+        }
+
+    }
+
+    /**
+     * 获取搜索结果
+     * @return array
+     */
+    public static function search($q, $page = 1, $pagesize = 20, $order = '', $fulltext = true, $fuzzy = false, $synonyms = false)
+    {
+        return Xunsearch::instance('blog')->search($q, $page, $pagesize, $order, $fulltext, $fuzzy, $synonyms);
+    }
+
+    /**
+     * 获取建议搜索关键字
+     * @param string $q     关键字
+     * @param int    $limit 返回条数
+     */
+    public static function suggestion($q, $limit = 10)
+    {
+        return Xunsearch::instance('blog')->suggestion($q, $limit);
+    }
+
+    /**
+     * 获取搜索热门关键字
+     * @return array
+     * @throws \XSException
+     */
+    public static function hot()
+    {
+        return Xunsearch::instance('blog')->getXS()->search->getHotQuery();
+    }
+
+}

+ 180 - 0
addons/blog/library/HashMap.php

@@ -0,0 +1,180 @@
+<?php
+
+/**
+ * php构建哈希表类.
+ * User: wanghui
+ * Date: 17/3/9
+ * Time: 上午9:10
+ **/
+
+namespace addons\blog\library;
+
+class HashMap
+{
+    /**
+     * 哈希表变量
+     *
+     * @var array|null
+     */
+    protected $hashTable = array();
+
+    public function __construct()
+    {
+    }
+
+    /**
+     * 向HashMap中添加一个键值对
+     *
+     * @param $key
+     * @param $value
+     * @return mixed|null
+     */
+    public function put($key, $value)
+    {
+        if (!array_key_exists($key, $this->hashTable)) {
+            $this->hashTable[$key] = $value;
+            return null;
+        }
+        $_temp = $this->hashTable[$key];
+        $this->hashTable[$key] = $value;
+        return $_temp;
+    }
+
+    /**
+     * 根据key获取对应的value
+     *
+     * @param $key
+     * @return mixed|null
+     */
+    public function get($key)
+    {
+        if (array_key_exists($key, $this->hashTable)) {
+            return $this->hashTable[$key];
+        }
+        return null;
+    }
+
+    /**
+     * 删除指定key的键值对
+     *
+     * @param $key
+     * @return mixed|null
+     */
+    public function remove($key)
+    {
+        $temp_table = array();
+        if (array_key_exists($key, $this->hashTable)) {
+            $tempValue = $this->hashTable[$key];
+            while ($curValue = current($this->hashTable)) {
+                if (!(key($this->hashTable) == $key)) {
+                    $temp_table[key($this->hashTable)] = $curValue;
+                }
+                next($this->hashTable);
+            }
+            $this->hashTable = null;
+            $this->hashTable = $temp_table;
+            return $tempValue;
+        }
+        return null;
+    }
+
+    /**
+     * 获取HashMap的所有键值
+     *
+     * @return array
+     */
+    public function keys()
+    {
+        return array_keys($this->hashTable);
+    }
+
+    /**
+     * 获取HashMap的所有value值
+     *
+     * @return array
+     */
+    public function values()
+    {
+        return array_values($this->hashTable);
+    }
+
+    /**
+     * 将一个HashMap的值全部put到当前HashMap中
+     *
+     * @param $map
+     */
+    public function putAll($map)
+    {
+        if (!$map->isEmpty() && $map->size() > 0) {
+            $keys = $map->keys();
+            foreach ($keys as $key) {
+                $this->put($key, $map->get($key));
+            }
+        }
+
+        return;
+    }
+
+    /**
+     * 移除HashMap中所有元素
+     *
+     * @return bool
+     */
+    public function removeAll()
+    {
+        $this->hashTable = null;
+        return true;
+    }
+
+    /**
+     * 判断HashMap中是否包含指定的值
+     *
+     * @param $value
+     * @return bool
+     */
+    public function containsValue($value)
+    {
+        while ($curValue = current($this->H_table)) {
+            if ($curValue == $value) {
+                return true;
+            }
+            next($this->hashTable);
+        }
+        return false;
+    }
+
+    /**
+     * 判断HashMap中是否包含指定的键key
+     *
+     * @param $key
+     * @return bool
+     */
+    public function containsKey($key)
+    {
+        if (array_key_exists($key, $this->hashTable)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 获取HashMap中元素个数
+     *
+     * @return int
+     */
+    public function size()
+    {
+        return count($this->hashTable);
+    }
+
+    /**
+     * 判断HashMap是否为空
+     *
+     * @return bool
+     */
+    public function isEmpty()
+    {
+        return (count($this->hashTable) == 0);
+    }
+}

+ 296 - 0
addons/blog/library/SensitiveHelper.php

@@ -0,0 +1,296 @@
+<?php
+
+/**
+ * 敏感词类库.
+ * User: wanghui
+ * Date: 17/3/9
+ * Time: 上午9:11
+ */
+
+namespace addons\blog\library;
+
+class SensitiveHelper
+{
+    /**
+     * 待检测语句长度
+     *
+     * @var int
+     */
+    protected $contentLength = 0;
+
+    /**
+     * 敏感词单例
+     *
+     * @var object|null
+     */
+    private static $_instance = null;
+
+    /**
+     * 铭感词库树
+     *
+     * @var HashMap|null
+     */
+    protected $wordTree = null;
+
+    /**
+     * 存放待检测语句铭感词
+     *
+     * @var array|null
+     */
+    protected static $badWordList = null;
+
+    /**
+     * 获取单例
+     *
+     * @return self
+     */
+    public static function init()
+    {
+        if (!self::$_instance instanceof self) {
+            self::$_instance = new self();
+        }
+        return self::$_instance;
+    }
+
+    /**
+     * 构建铭感词树【文件模式】
+     *
+     * @param string $filepath
+     * @return $this
+     * @throws \Exception
+     */
+    public function setTreeByFile($filepath = '')
+    {
+        if (!file_exists($filepath)) {
+            throw new \Exception('词库文件不存在');
+        }
+
+        // 词库树初始化
+        $this->wordTree = new HashMap();
+
+        foreach ($this->yieldToReadFile($filepath) as $word) {
+            $this->buildWordToTree(trim($word));
+        }
+
+        return $this;
+    }
+
+
+    /**
+     * 构建铭感词树【数组模式】
+     *
+     * @param null $sensitiveWords
+     * @return $this
+     * @throws \Exception
+     */
+    public function setTree($sensitiveWords = null)
+    {
+        if (empty($sensitiveWords)) {
+            throw new \Exception('词库不能为空');
+        }
+
+        $this->wordTree = new HashMap();
+
+        foreach ($sensitiveWords as $word) {
+            $this->buildWordToTree($word);
+        }
+        return $this;
+    }
+
+    /**
+     * 检测文字中的敏感词
+     *
+     * @param string $content 待检测内容
+     * @param int $matchType 匹配类型 [默认为最小匹配规则]
+     * @param int $wordNum 需要获取的敏感词数量 [默认获取全部]
+     * @return array
+     */
+    public function getBadWord($content, $matchType = 1, $wordNum = 0)
+    {
+        $this->contentLength = mb_strlen($content, 'utf-8');
+        $badWordList = array();
+        for ($length = 0; $length < $this->contentLength; $length++) {
+            $matchFlag = 0;
+            $flag = false;
+            $tempMap = $this->wordTree;
+            for ($i = $length; $i < $this->contentLength; $i++) {
+                $keyChar = mb_substr($content, $i, 1, 'utf-8');
+
+                // 获取指定节点树
+                $nowMap = $tempMap->get($keyChar);
+
+                // 不存在节点树,直接返回
+                if (empty($nowMap)) {
+                    break;
+                }
+
+                // 存在,则判断是否为最后一个
+                $tempMap = $nowMap;
+
+                // 找到相应key,偏移量+1
+                $matchFlag++;
+
+                // 如果为最后一个匹配规则,结束循环,返回匹配标识数
+                if (false === $nowMap->get('ending')) {
+                    continue;
+                }
+
+                $flag = true;
+
+                // 最小规则,直接退出
+                if (1 === $matchType) {
+                    break;
+                }
+            }
+
+            if (!$flag) {
+                $matchFlag = 0;
+            }
+
+            // 找到相应key
+            if ($matchFlag <= 0) {
+                continue;
+            }
+
+            $badWordList[] = mb_substr($content, $length, $matchFlag, 'utf-8');
+
+            // 有返回数量限制
+            if ($wordNum > 0 && count($badWordList) == $wordNum) {
+                return $badWordList;
+            }
+
+            // 需匹配内容标志位往后移
+            $length = $length + $matchFlag - 1;
+        }
+        return $badWordList;
+    }
+
+
+    /**
+     * 替换敏感字字符
+     *
+     * @param $content
+     * @param $replaceChar
+     * @param string $sTag
+     * @param string $eTag
+     * @param int $matchType
+     * @return mixed
+     */
+    public function replace($content, $replaceChar = '', $sTag = '', $eTag = '', $matchType = 1)
+    {
+        if (empty($content)) {
+            throw new \Exception('请填写检测的内容');
+        }
+
+        if (empty(self::$badWordList)) {
+            $badWordList = $this->getBadWord($content, $matchType);
+        } else {
+            $badWordList = self::$badWordList;
+        }
+
+        // 未检测到敏感词,直接返回
+        if (empty($badWordList)) {
+            return $content;
+        }
+
+        foreach ($badWordList as $badWord) {
+            if ($sTag || $eTag) {
+                $replaceChar = $sTag . $badWord . $eTag;
+            }
+            $content = str_replace($badWord, $replaceChar, $content);
+        }
+        return $content;
+    }
+
+    /**
+     * 被检测内容是否合法,合法返回true,非法返回false
+     * @param $content
+     * @return bool
+     */
+    public function islegal($content)
+    {
+        $this->contentLength = mb_strlen($content, 'utf-8');
+
+        for ($length = 0; $length < $this->contentLength; $length++) {
+            $matchFlag = 0;
+
+            $tempMap = $this->wordTree;
+            for ($i = $length; $i < $this->contentLength; $i++) {
+                $keyChar = mb_substr($content, $i, 1, 'utf-8');
+
+                // 获取指定节点树
+                $nowMap = $tempMap->get($keyChar);
+
+                // 不存在节点树,直接返回
+                if (empty($nowMap)) {
+                    break;
+                }
+
+                // 找到相应key,偏移量+1
+                $tempMap = $nowMap;
+                $matchFlag++;
+
+                // 如果为最后一个匹配规则,结束循环,返回匹配标识数
+                if (false === $nowMap->get('ending')) {
+                    continue;
+                }
+
+                return false;
+            }
+
+            // 找到相应key
+            if ($matchFlag <= 0) {
+                continue;
+            }
+
+            // 需匹配内容标志位往后移
+            $length = $length + $matchFlag - 1;
+        }
+        return true;
+    }
+
+    protected function yieldToReadFile($filepath)
+    {
+        $fp = fopen($filepath, 'r');
+        while (!feof($fp)) {
+            yield fgets($fp);
+        }
+        fclose($fp);
+    }
+
+    // 将单个敏感词构建成树结构
+    protected function buildWordToTree($word = '')
+    {
+        if ('' === $word) {
+            return;
+        }
+        $tree = $this->wordTree;
+
+        $wordLength = mb_strlen($word, 'utf-8');
+        for ($i = 0; $i < $wordLength; $i++) {
+            $keyChar = mb_substr($word, $i, 1, 'utf-8');
+
+            // 获取子节点树结构
+            $tempTree = $tree->get($keyChar);
+
+            if ($tempTree) {
+                $tree = $tempTree;
+            } else {
+                // 设置标志位
+                $newTree = new HashMap();
+                $newTree->put('ending', false);
+
+                // 添加到集合
+                $tree->put($keyChar, $newTree);
+                $tree = $newTree;
+            }
+
+            // 到达最后一个节点
+            if ($i == $wordLength - 1) {
+                $tree->put('ending', true);
+            }
+        }
+
+        return;
+    }
+}

+ 59 - 0
addons/blog/library/Service.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace addons\blog\library;
+
+use addons\blog\library\aip\AipContentCensor;
+use addons\blog\library\SensitiveHelper;
+
+class Service
+{
+    /**
+     * 检测内容是否合法
+     * @param $content
+     * @return bool
+     */
+    public static function isContentLegal($content)
+    {
+        $config = get_addon_config('blog');
+        if ($config['audittype'] == 'local') {
+            // 敏感词过滤
+            $handle = SensitiveHelper::init()->setTreeByFile(ADDON_PATH . 'blog/data/words.dic');
+            //首先检测是否合法
+            $isLegal = $handle->islegal($content);
+            return $isLegal ? true : false;
+        } else {
+            $client = new AipContentCensor($config['aip_appid'], $config['aip_apikey'], $config['aip_secretkey']);
+            $result = $client->antiSpam($content);
+            if (isset($result['result']) && $result['result']['spam'] > 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 内容关键字自动加链接
+     */
+    public static function autolinks($value)
+    {
+        $links = [];
+
+        $value = preg_replace_callback('~(<a .*?>.*?</a>|<.*?>)~i', function ($match) use (&$links) {
+            return '<' . array_push($links, $match[1]) . '>';
+        }, $value);
+
+        $config = get_addon_config('blog');
+        $autolinks = $config['autolinks'];
+        $value = preg_replace_callback('/(' . implode('|', array_keys($autolinks)) . ')/i', function ($match) use ($autolinks) {
+            if (!isset($autolinks[$match[1]])) {
+                return $match[0];
+            } else {
+                return '<a href="' . $autolinks[$match[1]] . '" target="_blank">' . $match[0] . '</a>';
+            }
+        }, $value);
+        return preg_replace_callback('/<(\d+)>/', function ($match) use (&$links) {
+            return $links[$match[1] - 1];
+        }, $value);
+    }
+
+}

+ 27 - 0
addons/blog/library/aip/AipContentCensor.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace addons\blog\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+/**
+ * 内容审核
+ */
+class AipContentCensor extends AipImageCensor
+{
+
+}

+ 224 - 0
addons/blog/library/aip/AipImageCensor.php

@@ -0,0 +1,224 @@
+<?php
+
+namespace addons\blog\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+
+use addons\blog\library\aip\lib\AipBase;
+
+/**
+ * 黄反识别
+ */
+class AipImageCensor extends AipBase
+{
+
+    /**
+     * antiporn api url
+     * @var string
+     */
+    private $antiPornUrl = 'https://aip.baidubce.com/rest/2.0/antiporn/v1/detect';
+
+    /**
+     * antiporn gif api url
+     * @var string
+     */
+    private $antiPornGifUrl = 'https://aip.baidubce.com/rest/2.0/antiporn/v1/detect_gif';
+
+    /**
+     * antiterror api url
+     * @var string
+     */
+    private $antiTerrorUrl = 'https://aip.baidubce.com/rest/2.0/antiterror/v1/detect';
+
+    /**
+     * @var string
+     */
+    private $faceAuditUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/face_audit';
+
+    /**
+     * @var string
+     */
+    private $imageCensorCombUrl = 'https://aip.baidubce.com/api/v1/solution/direct/img_censor';
+
+    /**
+     * @var string
+     */
+    private $imageCensorUserDefinedUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/img_censor/user_defined';
+
+    /**
+     * @var string
+     */
+    private $antiSpamUrl = 'https://aip.baidubce.com/rest/2.0/antispam/v2/spam';
+
+    /**
+     * @param  string $image 图像读取
+     * @return array
+     */
+    public function antiPorn($image)
+    {
+
+        $data = array();
+        $data['image'] = base64_encode($image);
+
+        return $this->request($this->antiPornUrl, $data);
+    }
+
+    /**
+     * @param  string $image 图像读取
+     * @return array
+     */
+    public function multi_antiporn($images)
+    {
+
+        $data = array();
+        foreach ($images as $image) {
+            $data[] = array(
+                'image' => base64_encode($image),
+            );
+        }
+
+        return $this->multi_request($this->antiPornUrl, $data);
+    }
+
+    /**
+     * @param  string $image 图像读取
+     * @return array
+     */
+    public function antiPornGif($image)
+    {
+
+        $data = array();
+        $data['image'] = base64_encode($image);
+
+        return $this->request($this->antiPornGifUrl, $data);
+    }
+
+    /**
+     * @param  string $image 图像读取
+     * @return array
+     */
+    public function antiTerror($image)
+    {
+
+        $data = array();
+        $data['image'] = base64_encode($image);
+
+        return $this->request($this->antiTerrorUrl, $data);
+    }
+
+    /**
+     * @param  string $images 图像读取
+     * @return array
+     */
+    public function faceAudit($images, $configId = '')
+    {
+
+        // 非数组则处理为数组
+        if (!is_array($images)) {
+            $images = array(
+                $images,
+            );
+        }
+
+        $data = array(
+            'configId' => $configId,
+        );
+
+        $isUrl = substr(trim($images[0]), 0, 4) === 'http';
+        if (!$isUrl) {
+            $arr = array();
+
+            foreach ($images as $image) {
+                $arr[] = base64_encode($image);
+            }
+            $data['images'] = implode(',', $arr);
+        } else {
+            $urls = array();
+
+            foreach ($images as $url) {
+                $urls[] = urlencode($url);
+            }
+
+            $data['imgUrls'] = implode(',', $urls);
+        }
+
+        return $this->request($this->faceAuditUrl, $data);
+    }
+
+    /**
+     * @param  string $image 图像读取
+     * @return array
+     */
+    public function imageCensorComb($image, $scenes = 'antiporn', $options = array())
+    {
+
+        $scenes = !is_array($scenes) ? explode(',', $scenes) : $scenes;
+
+        $data = array(
+            'scenes' => $scenes,
+        );
+
+        $isUrl = substr(trim($image), 0, 4) === 'http';
+        if (!$isUrl) {
+            $data['image'] = base64_encode($image);
+        } else {
+            $data['imgUrl'] = $image;
+        }
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->imageCensorCombUrl, json_encode($data), array(
+            'Content-Type' => 'application/json',
+        ));
+    }
+
+    /**
+     * @param  string $image 图像
+     * @return array
+     */
+    public function imageCensorUserDefined($image)
+    {
+
+        $data = array();
+
+        $isUrl = substr(trim($image), 0, 4) === 'http';
+        if (!$isUrl) {
+            $data['image'] = base64_encode($image);
+        } else {
+            $data['imgUrl'] = $image;
+        }
+
+        return $this->request($this->imageCensorUserDefinedUrl, $data);
+    }
+
+    /**
+     * @param  string $content
+     * @return array
+     */
+    public function antiSpam($content, $options = array())
+    {
+
+        $data = array();
+        $data['content'] = $content;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->antiSpamUrl, $data);
+    }
+
+}

+ 429 - 0
addons/blog/library/aip/AipNlp.php

@@ -0,0 +1,429 @@
+<?php
+
+namespace addons\blog\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+use addons\blog\library\aip\lib\AipBase;
+
+class AipNlp extends AipBase
+{
+
+    /**
+     * 词法分析 lexer api url
+     * @var string
+     */
+    private $lexerUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/lexer';
+
+    /**
+     * 词法分析(定制版) lexer_custom api url
+     * @var string
+     */
+    private $lexerCustomUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/lexer_custom';
+
+    /**
+     * 依存句法分析 dep_parser api url
+     * @var string
+     */
+    private $depParserUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/depparser';
+
+    /**
+     * 词向量表示 word_embedding api url
+     * @var string
+     */
+    private $wordEmbeddingUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/word_emb_vec';
+
+    /**
+     * DNN语言模型 dnnlm_cn api url
+     * @var string
+     */
+    private $dnnlmCnUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/dnnlm_cn';
+
+    /**
+     * 词义相似度 word_sim_embedding api url
+     * @var string
+     */
+    private $wordSimEmbeddingUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/word_emb_sim';
+
+    /**
+     * 短文本相似度 simnet api url
+     * @var string
+     */
+    private $simnetUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/simnet';
+
+    /**
+     * 评论观点抽取 comment_tag api url
+     * @var string
+     */
+    private $commentTagUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/comment_tag';
+
+    /**
+     * 情感倾向分析 sentiment_classify api url
+     * @var string
+     */
+    private $sentimentClassifyUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify';
+
+    /**
+     * 文章标签 keyword api url
+     * @var string
+     */
+    private $keywordUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/keyword';
+
+    /**
+     * 文章分类 topic api url
+     * @var string
+     */
+    private $topicUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/topic';
+
+    /**
+     * 文本纠错 ecnet api url
+     * @var string
+     */
+    private $ecnetUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/ecnet';
+
+    /**
+     * 对话情绪识别接口 emotion api url
+     * @var string
+     */
+    private $emotionUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/emotion';
+
+    /**
+     * 新闻摘要接口 news_summary api url
+     * @var string
+     */
+    private $newsSummaryUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/news_summary';
+
+    /**
+     * 格式化结果
+     * @param $content string
+     * @return mixed
+     */
+    protected function proccessResult($content)
+    {
+        return json_decode(mb_convert_encoding($content, 'UTF8', 'GBK'), true, 512, JSON_BIGINT_AS_STRING);
+    }
+
+    /**
+     * 词法分析接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过65536字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function lexer($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->lexerUrl, $data);
+    }
+
+    /**
+     * 词法分析(定制版)接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过65536字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function lexerCustom($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->lexerCustomUrl, $data);
+    }
+
+    /**
+     * 依存句法分析接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过256字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        mode 模型选择。默认值为0,可选值mode=0(对应web模型);mode=1(对应query模型)
+     * @return array
+     */
+    public function depParser($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->depParserUrl, $data);
+    }
+
+    /**
+     * 词向量表示接口
+     *
+     * @param string $word    - 文本内容(GBK编码),最大64字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function wordEmbedding($word, $options = array())
+    {
+
+        $data = array();
+
+        $data['word'] = $word;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->wordEmbeddingUrl, $data);
+    }
+
+    /**
+     * DNN语言模型接口
+     *
+     * @param string $text    - 文本内容(GBK编码),最大512字节,不需要切词
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function dnnlm($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->dnnlmCnUrl, $data);
+    }
+
+    /**
+     * 词义相似度接口
+     *
+     * @param string $word1   - 词1(GBK编码),最大64字节
+     * @param string $word2   - 词1(GBK编码),最大64字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        mode 预留字段,可选择不同的词义相似度模型。默认值为0,目前仅支持mode=0
+     * @return array
+     */
+    public function wordSimEmbedding($word1, $word2, $options = array())
+    {
+
+        $data = array();
+
+        $data['word_1'] = $word1;
+        $data['word_2'] = $word2;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->wordSimEmbeddingUrl, $data);
+    }
+
+    /**
+     * 短文本相似度接口
+     *
+     * @param string $text1   - 待比较文本1(GBK编码),最大512字节
+     * @param string $text2   - 待比较文本2(GBK编码),最大512字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        model 默认为"BOW",可选"BOW"、"CNN"与"GRNN"
+     * @return array
+     */
+    public function simnet($text1, $text2, $options = array())
+    {
+
+        $data = array();
+
+        $data['text_1'] = $text1;
+        $data['text_2'] = $text2;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->simnetUrl, $data);
+    }
+
+    /**
+     * 评论观点抽取接口
+     *
+     * @param string $text    - 评论内容(GBK编码),最大10240字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        type 评论行业类型,默认为4(餐饮美食)
+     * @return array
+     */
+    public function commentTag($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->commentTagUrl, $data);
+    }
+
+    /**
+     * 情感倾向分析接口
+     *
+     * @param string $text    - 文本内容(GBK编码),最大102400字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function sentimentClassify($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->sentimentClassifyUrl, $data);
+    }
+
+    /**
+     * 文章标签接口
+     *
+     * @param string $title   - 篇章的标题,最大80字节
+     * @param string $content - 篇章的正文,最大65535字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function keyword($title, $content, $options = array())
+    {
+
+        $data = array();
+
+        $data['title'] = $title;
+        $data['content'] = $content;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->keywordUrl, $data);
+    }
+
+    /**
+     * 文章分类接口
+     *
+     * @param string $title   - 篇章的标题,最大80字节
+     * @param string $content - 篇章的正文,最大65535字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function topic($title, $content, $options = array())
+    {
+
+        $data = array();
+
+        $data['title'] = $title;
+        $data['content'] = $content;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->topicUrl, $data);
+    }
+
+    /**
+     * 文本纠错接口
+     *
+     * @param string $text    - 待纠错文本,输入限制511字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function ecnet($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->ecnetUrl, $data);
+    }
+
+    /**
+     * 对话情绪识别接口接口
+     *
+     * @param string $text    - 待识别情感文本,输入限制512字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        scene default(默认项-不区分场景),talk(闲聊对话-如度秘聊天等),task(任务型对话-如导航对话等),customer_service(客服对话-如电信/银行客服等)
+     * @return array
+     */
+    public function emotion($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->emotionUrl, $data);
+    }
+
+    /**
+     * 新闻摘要接口接口
+     *
+     * @param string  $content       - 字符串(限200字符数)字符串仅支持GBK编码,长度需小于200字符数(即400字节),请输入前确认字符数没有超限,若字符数超长会返回错误。标题在算法中具有重要的作用,若文章确无标题,输入参数的“标题”字段为空即可
+     * @param integer $maxSummaryLen - 此数值将作为摘要结果的最大长度。例如:原文长度1000字,本参数设置为150,则摘要结果的最大长度是150字;推荐最优区间:200-500字
+     * @param array   $options       - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                               title 字符串(限200字符数)字符串仅支持GBK编码,长度需小于200字符数(即400字节),请输入前确认字符数没有超限,若字符数超长会返回错误。标题在算法中具有重要的作用,若文章确无标题,输入参数的“标题”字段为空即可
+     * @return array
+     */
+    public function newsSummary($content, $maxSummaryLen, $options = array())
+    {
+
+        $data = array();
+
+        $data['content'] = $content;
+        $data['max_summary_len'] = $maxSummaryLen;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->newsSummaryUrl, $data);
+    }
+}

+ 628 - 0
addons/blog/library/aip/AipOcr.php

@@ -0,0 +1,628 @@
+<?php
+
+namespace addons\blog\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+use addons\blog\library\aip\lib\AipBase;
+
+class AipOcr extends AipBase
+{
+
+    /**
+     * 通用文字识别 general_basic api url
+     * @var string
+     */
+    private $generalBasicUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic';
+
+    /**
+     * 通用文字识别(高精度版) accurate_basic api url
+     * @var string
+     */
+    private $accurateBasicUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic';
+
+    /**
+     * 通用文字识别(含位置信息版) general api url
+     * @var string
+     */
+    private $generalUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general';
+
+    /**
+     * 通用文字识别(含位置高精度版) accurate api url
+     * @var string
+     */
+    private $accurateUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/accurate';
+
+    /**
+     * 通用文字识别(含生僻字版) general_enhanced api url
+     * @var string
+     */
+    private $generalEnhancedUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_enhanced';
+
+    /**
+     * 网络图片文字识别 web_image api url
+     * @var string
+     */
+    private $webImageUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/webimage';
+
+    /**
+     * 身份证识别 idcard api url
+     * @var string
+     */
+    private $idcardUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/idcard';
+
+    /**
+     * 银行卡识别 bankcard api url
+     * @var string
+     */
+    private $bankcardUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard';
+
+    /**
+     * 驾驶证识别 driving_license api url
+     * @var string
+     */
+    private $drivingLicenseUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/driving_license';
+
+    /**
+     * 行驶证识别 vehicle_license api url
+     * @var string
+     */
+    private $vehicleLicenseUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/vehicle_license';
+
+    /**
+     * 车牌识别 license_plate api url
+     * @var string
+     */
+    private $licensePlateUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/license_plate';
+
+    /**
+     * 营业执照识别 business_license api url
+     * @var string
+     */
+    private $businessLicenseUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/business_license';
+
+    /**
+     * 通用票据识别 receipt api url
+     * @var string
+     */
+    private $receiptUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/receipt';
+
+    /**
+     * 自定义模版文字识别 custom api url
+     * @var string
+     */
+    private $customUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/iocr/recognise';
+
+    /**
+     * 表格文字识别同步接口 form api url
+     * @var string
+     */
+    private $formUrl = 'https://aip.baidubce.com/rest/2.0/ocr/v1/form';
+
+    /**
+     * 表格文字识别 table_recognize api url
+     * @var string
+     */
+    private $tableRecognizeUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/form_ocr/request';
+
+    /**
+     * 表格识别结果 table_result_get api url
+     * @var string
+     */
+    private $tableResultGetUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/form_ocr/get_request_result';
+
+
+    /**
+     * 通用文字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function basicGeneral($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalBasicUrl, $data);
+    }
+
+    /**
+     * 通用文字识别接口
+     *
+     * @param string $url     - 图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式,当image字段存在时url字段失效
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function basicGeneralUrl($url, $options = array())
+    {
+
+        $data = array();
+
+        $data['url'] = $url;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalBasicUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(高精度版)接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function basicAccurate($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->accurateBasicUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含位置信息版)接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        vertexes_location 是否返回文字外接多边形顶点位置,不支持单字位置。默认为false
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function general($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含位置信息版)接口
+     *
+     * @param string $url     - 图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式,当image字段存在时url字段失效
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        vertexes_location 是否返回文字外接多边形顶点位置,不支持单字位置。默认为false
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function generalUrl($url, $options = array())
+    {
+
+        $data = array();
+
+        $data['url'] = $url;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含位置高精度版)接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        vertexes_location 是否返回文字外接多边形顶点位置,不支持单字位置。默认为false
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function accurate($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->accurateUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含生僻字版)接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function enhancedGeneral($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalEnhancedUrl, $data);
+    }
+
+    /**
+     * 通用文字识别(含生僻字版)接口
+     *
+     * @param string $url     - 图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式,当image字段存在时url字段失效
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        language_type 识别语言类型,默认为CHN_ENG。可选值包括:<br>- CHN_ENG:中英文混合;<br>- ENG:英文;<br>- POR:葡萄牙语;<br>- FRE:法语;<br>- GER:德语;<br>- ITA:意大利语;<br>- SPA:西班牙语;<br>- RUS:俄语;<br>- JAP:日语;<br>- KOR:韩语;
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     *                        probability 是否返回识别结果中每一行的置信度
+     * @return array
+     */
+    public function enhancedGeneralUrl($url, $options = array())
+    {
+
+        $data = array();
+
+        $data['url'] = $url;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->generalEnhancedUrl, $data);
+    }
+
+    /**
+     * 网络图片文字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     * @return array
+     */
+    public function webImage($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->webImageUrl, $data);
+    }
+
+    /**
+     * 网络图片文字识别接口
+     *
+     * @param string $url     - 图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式,当image字段存在时url字段失效
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        detect_language 是否检测语言,默认不检测。当前支持(中文、英语、日语、韩语)
+     * @return array
+     */
+    public function webImageUrl($url, $options = array())
+    {
+
+        $data = array();
+
+        $data['url'] = $url;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->webImageUrl, $data);
+    }
+
+    /**
+     * 身份证识别接口
+     *
+     * @param string $image      - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param string $idCardSide - front:身份证含照片的一面;back:身份证带国徽的一面
+     * @param array  $options    - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                           detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                           detect_risk 是否开启身份证风险类型(身份证复印件、临时身份证、身份证翻拍、修改过的身份证)功能,默认不开启,即:false。可选值:true-开启;false-不开启
+     * @return array
+     */
+    public function idcard($image, $idCardSide, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+        $data['id_card_side'] = $idCardSide;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->idcardUrl, $data);
+    }
+
+    /**
+     * 银行卡识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function bankcard($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->bankcardUrl, $data);
+    }
+
+    /**
+     * 驾驶证识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     * @return array
+     */
+    public function drivingLicense($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->drivingLicenseUrl, $data);
+    }
+
+    /**
+     * 行驶证识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     *                        accuracy normal 使用快速服务,1200ms左右时延;缺省或其它值使用高精度服务,1600ms左右时延
+     * @return array
+     */
+    public function vehicleLicense($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->vehicleLicenseUrl, $data);
+    }
+
+    /**
+     * 车牌识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        multi_detect 是否检测多张车牌,默认为false,当置为true的时候可以对一张图片内的多张车牌进行识别
+     * @return array
+     */
+    public function licensePlate($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->licensePlateUrl, $data);
+    }
+
+    /**
+     * 营业执照识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function businessLicense($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->businessLicenseUrl, $data);
+    }
+
+    /**
+     * 通用票据识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        recognize_granularity 是否定位单字符位置,big:不定位单字符位置,默认值;small:定位单字符位置
+     *                        probability 是否返回识别结果中每一行的置信度
+     *                        accuracy normal 使用快速服务,1200ms左右时延;缺省或其它值使用高精度服务,1600ms左右时延
+     *                        detect_direction 是否检测图像朝向,默认不检测,即:false。朝向是指输入图像是正常方向、逆时针旋转90/180/270度。可选值包括:<br>- true:检测朝向;<br>- false:不检测朝向。
+     * @return array
+     */
+    public function receipt($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->receiptUrl, $data);
+    }
+
+    /**
+     * 自定义模版文字识别接口
+     *
+     * @param string $image        - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param string $templateSign - 您在自定义文字识别平台制作的模版的ID
+     * @param array  $options      - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function custom($image, $templateSign, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+        $data['templateSign'] = $templateSign;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->customUrl, $data);
+    }
+
+    /**
+     * 表格文字识别同步接口接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function form($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->formUrl, $data);
+    }
+
+    /**
+     * 表格文字识别接口
+     *
+     * @param string $image   - 图像数据,base64编码,要求base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/png/bmp格式
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function tableRecognitionAsync($image, $options = array())
+    {
+
+        $data = array();
+
+        $data['image'] = base64_encode($image);
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->tableRecognizeUrl, $data);
+    }
+
+    /**
+     * 表格识别结果接口
+     *
+     * @param string $requestId - 发送表格文字识别请求时返回的request id
+     * @param array  $options   - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                          result_type 期望获取结果的类型,取值为“excel”时返回xls文件的地址,取值为“json”时返回json格式的字符串,默认为”excel”
+     * @return array
+     */
+    public function getTableRecognitionResult($requestId, $options = array())
+    {
+
+        $data = array();
+
+        $data['request_id'] = $requestId;
+
+        $data = array_merge($data, $options);
+
+        return $this->request($this->tableResultGetUrl, $data);
+    }
+
+    /**
+     * 同步请求
+     * @param  string $image 图像读取
+     * @param  options 接口可选参数
+     * @return array
+     */
+    public function tableRecognition($image, $options = array(), $timeout = 10000)
+    {
+        $result = $this->tableRecognitionAsync($image);
+        if (isset($result['error_code'])) {
+            return $result;
+        }
+        $requestId = $result['result'][0]['request_id'];
+        $count = ceil($timeout / 1000);
+        for ($i = 0; $i < $count; $i++) {
+            $result = $this->getTableRecognitionResult($requestId, $options);
+            // 完成
+            if ($result['result']['ret_code'] == 3) {
+                break;
+            }
+            sleep(1);
+        }
+        return $result;
+    }
+
+}
+

+ 401 - 0
addons/blog/library/aip/lib/AipBase.php

@@ -0,0 +1,401 @@
+<?php
+
+namespace addons\blog\library\aip\lib;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+use Exception;
+
+/**
+ * Aip Base 基类
+ */
+class AipBase
+{
+
+    /**
+     * 获取access token url
+     * @var string
+     */
+    protected $accessTokenUrl = 'https://aip.baidubce.com/oauth/2.0/token';
+
+    /**
+     * 反馈接口
+     * @var string
+     */
+    protected $reportUrl = 'https://aip.baidubce.com/rpc/2.0/feedback/v1/report';
+
+    /**
+     * appId
+     * @var string
+     */
+    protected $appId = '';
+
+    /**
+     * apiKey
+     * @var string
+     */
+    protected $apiKey = '';
+
+    /**
+     * secretKey
+     * @var string
+     */
+    protected $secretKey = '';
+
+    /**
+     * 权限
+     * @var array
+     */
+    protected $scope = 'brain_all_scope';
+
+    /**
+     * @param string $appId
+     * @param string $apiKey
+     * @param string $secretKey
+     */
+    public function __construct($appId, $apiKey, $secretKey)
+    {
+        $this->appId = trim($appId);
+        $this->apiKey = trim($apiKey);
+        $this->secretKey = trim($secretKey);
+        $this->isCloudUser = null;
+        $this->client = new AipHttpClient();
+        $this->version = '2_2_2';
+        $this->proxies = array();
+    }
+
+    /**
+     * 查看版本
+     * @return string
+     *
+     */
+    public function getVersion()
+    {
+        return $this->version;
+    }
+
+    /**
+     * 连接超时
+     * @param int $ms 毫秒
+     */
+    public function setConnectionTimeoutInMillis($ms)
+    {
+        $this->client->setConnectionTimeoutInMillis($ms);
+    }
+
+    /**
+     * 响应超时
+     * @param int $ms 毫秒
+     */
+    public function setSocketTimeoutInMillis($ms)
+    {
+        $this->client->setSocketTimeoutInMillis($ms);
+    }
+
+    /**
+     * 代理
+     * @param array $proxy
+     * @return string
+     *
+     */
+    public function setProxies($proxies)
+    {
+        $this->client->setConf($proxies);
+    }
+
+    /**
+     * 处理请求参数
+     * @param  string $url
+     * @param array   $params
+     * @param array   $data
+     * @param array   $headers
+     */
+    protected function proccessRequest($url, &$params, &$data, $headers)
+    {
+        $params['aipSdk'] = 'php';
+        $params['aipSdkVersion'] = $this->version;
+    }
+
+    /**
+     * Api 请求
+     * @param  string $url
+     * @param  mixed  $data
+     * @return mixed
+     */
+    protected function request($url, $data, $headers = array())
+    {
+        try {
+            $result = $this->validate($url, $data);
+            if ($result !== true) {
+                return $result;
+            }
+
+            $params = array();
+            $authObj = $this->auth();
+
+            if ($this->isCloudUser === false) {
+                $params['access_token'] = $authObj['access_token'];
+            }
+
+            // 特殊处理
+            $this->proccessRequest($url, $params, $data, $headers);
+
+            $headers = $this->getAuthHeaders('POST', $url, $params, $headers);
+            $response = $this->client->post($url, $data, $params, $headers);
+
+            $obj = $this->proccessResult($response['content']);
+
+            if (!$this->isCloudUser && isset($obj['error_code']) && $obj['error_code'] == 110) {
+                $authObj = $this->auth(true);
+                $params['access_token'] = $authObj['access_token'];
+                $response = $this->client->post($url, $data, $params, $headers);
+                $obj = $this->proccessResult($response['content']);
+            }
+
+            if (empty($obj) || !isset($obj['error_code'])) {
+                $this->writeAuthObj($authObj);
+            }
+        } catch (Exception $e) {
+            return array(
+                'error_code' => 'SDK108',
+                'error_msg'  => 'connection or read data timeout',
+            );
+        }
+
+        return $obj;
+    }
+
+    /**
+     * Api 多个并发请求
+     * @param  string $url
+     * @param  mixed  $data
+     * @return mixed
+     */
+    protected function multi_request($url, $data)
+    {
+        try {
+            $params = array();
+            $authObj = $this->auth();
+            $headers = $this->getAuthHeaders('POST', $url);
+
+            if ($this->isCloudUser === false) {
+                $params['access_token'] = $authObj['access_token'];
+            }
+
+            $responses = $this->client->multi_post($url, $data, $params, $headers);
+
+            $is_success = false;
+            foreach ($responses as $response) {
+                $obj = $this->proccessResult($response['content']);
+
+                if (empty($obj) || !isset($obj['error_code'])) {
+                    $is_success = true;
+                }
+
+                if (!$this->isCloudUser && isset($obj['error_code']) && $obj['error_code'] == 110) {
+                    $authObj = $this->auth(true);
+                    $params['access_token'] = $authObj['access_token'];
+                    $responses = $this->client->post($url, $data, $params, $headers);
+                    break;
+                }
+            }
+
+            if ($is_success) {
+                $this->writeAuthObj($authObj);
+            }
+
+            $objs = array();
+            foreach ($responses as $response) {
+                $objs[] = $this->proccessResult($response['content']);
+            }
+
+        } catch (Exception $e) {
+            return array(
+                'error_code' => 'SDK108',
+                'error_msg'  => 'connection or read data timeout',
+            );
+        }
+
+        return $objs;
+    }
+
+    /**
+     * 格式检查
+     * @param  string $url
+     * @param  array  $data
+     * @return mix
+     */
+    protected function validate($url, &$data)
+    {
+        return true;
+    }
+
+    /**
+     * 格式化结果
+     * @param $content string
+     * @return mixed
+     */
+    protected function proccessResult($content)
+    {
+        return json_decode($content, true);
+    }
+
+    /**
+     * 返回 access token 路径
+     * @return string
+     */
+    private function getAuthFilePath()
+    {
+        return dirname(__FILE__) . DIRECTORY_SEPARATOR . md5($this->apiKey);
+    }
+
+    /**
+     * 写入本地文件
+     * @param  array $obj
+     * @return void
+     */
+    private function writeAuthObj($obj)
+    {
+        if ($obj === null || (isset($obj['is_read']) && $obj['is_read'] === true)) {
+            return;
+        }
+
+        $obj['time'] = time();
+        $obj['is_cloud_user'] = $this->isCloudUser;
+        @file_put_contents($this->getAuthFilePath(), json_encode($obj));
+    }
+
+    /**
+     * 读取本地缓存
+     * @return array
+     */
+    private function readAuthObj()
+    {
+        $content = @file_get_contents($this->getAuthFilePath());
+        if ($content !== false) {
+            $obj = json_decode($content, true);
+            $this->isCloudUser = $obj['is_cloud_user'];
+            $obj['is_read'] = true;
+            if ($this->isCloudUser || $obj['time'] + $obj['expires_in'] - 30 > time()) {
+                return $obj;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 认证
+     * @param bool $refresh 是否刷新
+     * @return array
+     */
+    private function auth($refresh = false)
+    {
+
+        //非过期刷新
+        if (!$refresh) {
+            $obj = $this->readAuthObj();
+            if (!empty($obj)) {
+                return $obj;
+            }
+        }
+
+        $response = $this->client->get($this->accessTokenUrl, array(
+            'grant_type'    => 'client_credentials',
+            'client_id'     => $this->apiKey,
+            'client_secret' => $this->secretKey,
+        ));
+
+        $obj = json_decode($response['content'], true);
+
+        $this->isCloudUser = !$this->isPermission($obj);
+        return $obj;
+    }
+
+    /**
+     * 判断认证是否有权限
+     * @param  array $authObj
+     * @return boolean
+     */
+    protected function isPermission($authObj)
+    {
+        if (empty($authObj) || !isset($authObj['scope'])) {
+            return false;
+        }
+
+        $scopes = explode(' ', $authObj['scope']);
+
+        return in_array($this->scope, $scopes);
+    }
+
+    /**
+     * @param  string $method HTTP method
+     * @param  string $url
+     * @param  array  $param  参数
+     * @return array
+     */
+    private function getAuthHeaders($method, $url, $params = array(), $headers = array())
+    {
+
+        //不是云的老用户则不用在header中签名 认证
+        if ($this->isCloudUser === false) {
+            return $headers;
+        }
+
+        $obj = parse_url($url);
+        if (!empty($obj['query'])) {
+            foreach (explode('&', $obj['query']) as $kv) {
+                if (!empty($kv)) {
+                    list($k, $v) = explode('=', $kv, 2);
+                    $params[$k] = $v;
+                }
+            }
+        }
+
+        //UTC 时间戳
+        $timestamp = gmdate('Y-m-d\TH:i:s\Z');
+        $headers['Host'] = isset($obj['port']) ? sprintf('%s:%s', $obj['host'], $obj['port']) : $obj['host'];
+        $headers['x-bce-date'] = $timestamp;
+
+        //签名
+        $headers['authorization'] = AipSampleSigner::sign(array(
+            'ak' => $this->apiKey,
+            'sk' => $this->secretKey,
+        ), $method, $obj['path'], $headers, $params, array(
+            'timestamp'     => $timestamp,
+            'headersToSign' => array_keys($headers),
+        ));
+
+        return $headers;
+    }
+
+    /**
+     * 反馈
+     *
+     * @param array $feedbacks
+     * @return array
+     */
+    public function report($feedback)
+    {
+
+        $data = array();
+
+        $data['feedback'] = $feedback;
+
+        return $this->request($this->reportUrl, $data);
+    }
+}

+ 227 - 0
addons/blog/library/aip/lib/AipHttpClient.php

@@ -0,0 +1,227 @@
+<?php
+
+namespace addons\blog\library\aip\lib;
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+/**
+ * Http Client
+ */
+class AipHttpClient
+{
+
+    /**
+     * HttpClient
+     * @param array $headers HTTP header
+     */
+    public function __construct($headers = array())
+    {
+        $this->headers = $this->buildHeaders($headers);
+        $this->connectTimeout = 60000;
+        $this->socketTimeout = 60000;
+        $this->conf = array();
+    }
+
+    /**
+     * 连接超时
+     * @param int $ms 毫秒
+     */
+    public function setConnectionTimeoutInMillis($ms)
+    {
+        $this->connectTimeout = $ms;
+    }
+
+    /**
+     * 响应超时
+     * @param int $ms 毫秒
+     */
+    public function setSocketTimeoutInMillis($ms)
+    {
+        $this->socketTimeout = $ms;
+    }
+
+    /**
+     * 配置
+     * @param array $conf
+     */
+    public function setConf($conf)
+    {
+        $this->conf = $conf;
+    }
+
+    /**
+     * 请求预处理
+     * @param resource $ch
+     */
+    public function prepare($ch)
+    {
+        foreach ($this->conf as $key => $value) {
+            curl_setopt($ch, $key, $value);
+        }
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $data    HTTP POST BODY
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function post($url, $data = array(), $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $ch = curl_init();
+        $this->prepare($ch);
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_POST, 1);
+        curl_setopt($ch, CURLOPT_HEADER, false);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
+        curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+        $content = curl_exec($ch);
+        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+        if ($code === 0) {
+            throw new Exception(curl_error($ch));
+        }
+
+        curl_close($ch);
+        return array(
+            'code'    => $code,
+            'content' => $content,
+        );
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $datas   HTTP POST BODY
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function multi_post($url, $datas = array(), $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $chs = array();
+        $result = array();
+        $mh = curl_multi_init();
+        foreach ($datas as $data) {
+            $ch = curl_init();
+            $chs[] = $ch;
+            $this->prepare($ch);
+            curl_setopt($ch, CURLOPT_URL, $url);
+            curl_setopt($ch, CURLOPT_POST, 1);
+            curl_setopt($ch, CURLOPT_HEADER, false);
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
+            curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+            curl_multi_add_handle($mh, $ch);
+        }
+
+        $running = null;
+        do {
+            curl_multi_exec($mh, $running);
+            usleep(100);
+        } while ($running);
+
+        foreach ($chs as $ch) {
+            $content = curl_multi_getcontent($ch);
+            $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+            $result[] = array(
+                'code'    => $code,
+                'content' => $content,
+            );
+            curl_multi_remove_handle($mh, $ch);
+        }
+        curl_multi_close($mh);
+
+        return $result;
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function get($url, $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $ch = curl_init();
+        $this->prepare($ch);
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_HEADER, false);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+        curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+        $content = curl_exec($ch);
+        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+        if ($code === 0) {
+            throw new Exception(curl_error($ch));
+        }
+
+        curl_close($ch);
+        return array(
+            'code'    => $code,
+            'content' => $content,
+        );
+    }
+
+    /**
+     * 构造 header
+     * @param  array $headers
+     * @return array
+     */
+    private function buildHeaders($headers)
+    {
+        $result = array();
+        foreach ($headers as $k => $v) {
+            $result[] = sprintf('%s:%s', $k, $v);
+        }
+        return $result;
+    }
+
+    /**
+     *
+     * @param  string $url
+     * @param  array  $params 参数
+     * @return string
+     */
+    private function buildUrl($url, $params)
+    {
+        if (!empty($params)) {
+            $str = http_build_query($params);
+            return $url . (strpos($url, '?') === false ? '?' : '&') . $str;
+        } else {
+            return $url;
+        }
+    }
+}

+ 181 - 0
addons/blog/library/aip/lib/AipHttpUtil.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace addons\blog\library\aip\lib;
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* Licensed under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License. You may obtain a copy of
+* the License at
+*
+* Http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations under
+* the License.
+*/
+
+/**
+ * BCE Util
+ */
+class AipHttpUtil
+{
+    // 根据RFC 3986,除了:
+    //   1.大小写英文字符
+    //   2.阿拉伯数字
+    //   3.点'.'、波浪线'~'、减号'-'以及下划线'_'
+    // 以外都要编码
+    public static $PERCENT_ENCODED_STRINGS;
+
+    //填充编码数组
+    public static function __init()
+    {
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS = array();
+        for ($i = 0; $i < 256; ++$i) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[$i] = sprintf("%%%02X", $i);
+        }
+
+        //a-z不编码
+        foreach (range('a', 'z') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //A-Z不编码
+        foreach (range('A', 'Z') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //0-9不编码
+        foreach (range('0', '9') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //以下4个字符不编码
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('-')] = '-';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('.')] = '.';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('_')] = '_';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('~')] = '~';
+    }
+
+    /**
+     * 在uri编码中不能对'/'编码
+     * @param  string $path
+     * @return string
+     */
+    public static function urlEncodeExceptSlash($path)
+    {
+        return str_replace("%2F", "/", AipHttpUtil::urlEncode($path));
+    }
+
+    /**
+     * 使用编码数组编码
+     * @param  string $path
+     * @return string
+     */
+    public static function urlEncode($value)
+    {
+        $result = '';
+        for ($i = 0; $i < strlen($value); ++$i) {
+            $result .= AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($value[$i])];
+        }
+        return $result;
+    }
+
+    /**
+     * 生成标准化QueryString
+     * @param  array $parameters
+     * @return array
+     */
+    public static function getCanonicalQueryString(array $parameters)
+    {
+        //没有参数,直接返回空串
+        if (count($parameters) == 0) {
+            return '';
+        }
+
+        $parameterStrings = array();
+        foreach ($parameters as $k => $v) {
+            //跳过Authorization字段
+            if (strcasecmp('Authorization', $k) == 0) {
+                continue;
+            }
+            if (!isset($k)) {
+                throw new \InvalidArgumentException(
+                    "parameter key should not be null"
+                );
+            }
+            if (isset($v)) {
+                //对于有值的,编码后放在=号两边
+                $parameterStrings[] = AipHttpUtil::urlEncode($k)
+                    . '=' . AipHttpUtil::urlEncode((string)$v);
+            } else {
+                //对于没有值的,只将key编码后放在=号的左边,右边留空
+                $parameterStrings[] = AipHttpUtil::urlEncode($k) . '=';
+            }
+        }
+        //按照字典序排序
+        sort($parameterStrings);
+
+        //使用'&'符号连接它们
+        return implode('&', $parameterStrings);
+    }
+
+    /**
+     * 生成标准化uri
+     * @param  string $path
+     * @return string
+     */
+    public static function getCanonicalURIPath($path)
+    {
+        //空路径设置为'/'
+        if (empty($path)) {
+            return '/';
+        } else {
+            //所有的uri必须以'/'开头
+            if ($path[0] == '/') {
+                return AipHttpUtil::urlEncodeExceptSlash($path);
+            } else {
+                return '/' . AipHttpUtil::urlEncodeExceptSlash($path);
+            }
+        }
+    }
+
+    /**
+     * 生成标准化http请求头串
+     * @param  array $headers
+     * @return array
+     */
+    public static function getCanonicalHeaders($headers)
+    {
+        //如果没有headers,则返回空串
+        if (count($headers) == 0) {
+            return '';
+        }
+
+        $headerStrings = array();
+        foreach ($headers as $k => $v) {
+            //跳过key为null的
+            if ($k === null) {
+                continue;
+            }
+            //如果value为null,则赋值为空串
+            if ($v === null) {
+                $v = '';
+            }
+            //trim后再encode,之后使用':'号连接起来
+            $headerStrings[] = AipHttpUtil::urlEncode(strtolower(trim($k))) . ':' . AipHttpUtil::urlEncode(trim($v));
+        }
+        //字典序排序
+        sort($headerStrings);
+
+        //用'\n'把它们连接起来
+        return implode("\n", $headerStrings);
+    }
+}
+
+AipHttpUtil::__init();
+
+
+

+ 181 - 0
addons/blog/library/aip/lib/AipSampleSigner.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace addons\blog\library\aip\lib;
+
+
+class AipSampleSigner
+{
+
+    const BCE_AUTH_VERSION = "bce-auth-v1";
+    const BCE_PREFIX = 'x-bce-';
+
+    //不指定headersToSign情况下,默认签名http头,包括:
+    //    1.host
+    //    2.content-length
+    //    3.content-type
+    //    4.content-md5
+    public static $defaultHeadersToSign;
+
+    public static function __init()
+    {
+        AipSampleSigner::$defaultHeadersToSign = array(
+            "host",
+            "content-length",
+            "content-type",
+            "content-md5",
+        );
+    }
+
+    /**
+     * 签名
+     * @param  array  $credentials
+     * @param  string $httpMethod
+     * @param  string $path
+     * @param  array  $headers
+     * @param  string $params
+     * @param  array  $options
+     * @return string
+     */
+    public static function sign(
+        array $credentials,
+        $httpMethod,
+        $path,
+        $headers,
+        $params,
+        $options = array()
+    )
+    {
+        //设定签名有效时间
+        if (!isset($options[AipSignOption::EXPIRATION_IN_SECONDS])) {
+            //默认值1800秒
+            $expirationInSeconds = AipSignOption::DEFAULT_EXPIRATION_IN_SECONDS;
+        } else {
+            $expirationInSeconds = $options[AipSignOption::EXPIRATION_IN_SECONDS];
+        }
+
+        //解析ak sk
+        $accessKeyId = $credentials['ak'];
+        $secretAccessKey = $credentials['sk'];
+
+        //设定时间戳,注意:如果自行指定时间戳需要为UTC时间
+        if (!isset($options[AipSignOption::TIMESTAMP])) {
+            //默认值当前时间
+            $timestamp = gmdate('Y-m-d\TH:i:s\Z');
+        } else {
+            $timestamp = $options[AipSignOption::TIMESTAMP];
+        }
+
+        //生成authString
+        $authString = AipSampleSigner::BCE_AUTH_VERSION . '/' . $accessKeyId . '/'
+            . $timestamp . '/' . $expirationInSeconds;
+
+        //使用sk和authString生成signKey
+        $signingKey = hash_hmac('sha256', $authString, $secretAccessKey);
+
+        //生成标准化URI
+        $canonicalURI = AipHttpUtil::getCanonicalURIPath($path);
+
+        //生成标准化QueryString
+        $canonicalQueryString = AipHttpUtil::getCanonicalQueryString($params);
+
+        //填充headersToSign,也就是指明哪些header参与签名
+        $headersToSign = null;
+        if (isset($options[AipSignOption::HEADERS_TO_SIGN])) {
+            $headersToSign = $options[AipSignOption::HEADERS_TO_SIGN];
+        }
+
+        //生成标准化header
+        $canonicalHeader = AipHttpUtil::getCanonicalHeaders(
+            AipSampleSigner::getHeadersToSign($headers, $headersToSign)
+        );
+
+        //整理headersToSign,以';'号连接
+        $signedHeaders = '';
+        if ($headersToSign !== null) {
+            $signedHeaders = strtolower(
+                trim(implode(";", $headersToSign))
+            );
+        }
+
+        //组成标准请求串
+        $canonicalRequest = "$httpMethod\n$canonicalURI\n"
+            . "$canonicalQueryString\n$canonicalHeader";
+
+        //使用signKey和标准请求串完成签名
+        $signature = hash_hmac('sha256', $canonicalRequest, $signingKey);
+
+        //组成最终签名串
+        $authorizationHeader = "$authString/$signedHeaders/$signature";
+
+        return $authorizationHeader;
+    }
+
+    /**
+     * 根据headsToSign过滤应该参与签名的header
+     * @param  array $headers
+     * @param  array $headersToSign
+     * @return array
+     */
+    public static function getHeadersToSign($headers, $headersToSign)
+    {
+
+        //value被trim后为空串的header不参与签名
+        $filter_empty = function ($v) {
+            return trim((string)$v) !== '';
+        };
+        $headers = array_filter($headers, $filter_empty);
+
+        //处理headers的key:去掉前后的空白并转化成小写
+        $trim_and_lower = function ($str) {
+            return strtolower(trim($str));
+        };
+        $temp = array();
+        $process_keys = function ($k, $v) use (&$temp, $trim_and_lower) {
+            $temp[$trim_and_lower($k)] = $v;
+        };
+        array_map($process_keys, array_keys($headers), $headers);
+        $headers = $temp;
+
+        //取出headers的key以备用
+        $header_keys = array_keys($headers);
+
+        $filtered_keys = null;
+        if ($headersToSign !== null) {
+            //如果有headersToSign,则根据headersToSign过滤
+
+            //预处理headersToSign:去掉前后的空白并转化成小写
+            $headersToSign = array_map($trim_and_lower, $headersToSign);
+
+            //只选取在headersToSign里面的header
+            $filtered_keys = array_intersect_key($header_keys, $headersToSign);
+
+        } else {
+            //如果没有headersToSign,则根据默认规则来选取headers
+            $filter_by_default = function ($k) {
+                return AipSampleSigner::isDefaultHeaderToSign($k);
+            };
+            $filtered_keys = array_filter($header_keys, $filter_by_default);
+        }
+
+        //返回需要参与签名的header
+        return array_intersect_key($headers, array_flip($filtered_keys));
+    }
+
+    /**
+     * 检查header是不是默认参加签名的:
+     * 1.是host、content-type、content-md5、content-length之一
+     * 2.以x-bce开头
+     * @param  array $header
+     * @return boolean
+     */
+    public static function isDefaultHeaderToSign($header)
+    {
+        $header = strtolower(trim($header));
+        if (in_array($header, AipSampleSigner::$defaultHeadersToSign)) {
+            return true;
+        }
+        return substr_compare($header, AipSampleSigner::BCE_PREFIX, 0, strlen(AipSampleSigner::BCE_PREFIX)) == 0;
+    }
+}
+
+AipSampleSigner::__init();

+ 19 - 0
addons/blog/library/aip/lib/AipSignOption.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace addons\blog\library\aip\lib;
+
+
+class AipSignOption
+{
+    const EXPIRATION_IN_SECONDS = 'expirationInSeconds';
+
+    const HEADERS_TO_SIGN = 'headersToSign';
+
+    const TIMESTAMP = 'timestamp';
+
+    const DEFAULT_EXPIRATION_IN_SECONDS = 1800;
+
+    const MIN_EXPIRATION_IN_SECONDS = 300;
+
+    const MAX_EXPIRATION_IN_SECONDS = 129600;
+}

+ 109 - 0
addons/blog/model/Block.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace addons\blog\model;
+
+use think\Model;
+
+/**
+ * 区块模型
+ */
+class Block extends Model
+{
+    protected $name = "blog_block";
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = '';
+    protected $updateTime = '';
+    // 追加属性
+    protected $append = [
+    ];
+    protected static $config = [];
+
+    protected static function init()
+    {
+        $config = get_addon_config('blog');
+        self::$config = $config;
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ? $value : '/assets/addons/blog/img/thumb.png';
+        return cdnurl($value);
+    }
+
+    /**
+     * 获取区块列表
+     * @param $params
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getBlockList($params)
+    {
+        $name = empty($params['name']) ? '' : $params['name'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'id' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $cache = !isset($params['cache']) ? true : (int)$params['cache'];
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $cache = !$cache ? false : $cache;
+
+        $where = ['status' => 'normal'];
+        if ($name !== '') {
+            $where['name'] = $name;
+        }
+        $order = $orderby == 'rand' ? 'rand()' : (in_array($orderby, ['name', 'id', 'createtime', 'updatetime', 'weigh']) ? "{$orderby} {$orderway}" : "id {$orderway}");
+
+        $list = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->order($order)
+            ->limit($limit)
+            ->cache($cache)
+            ->select();
+        self::render($list, $imgwidth, $imgheight);
+        return $list;
+    }
+
+    public static function render(&$list, $imgwidth, $imgheight)
+    {
+        $width = $imgwidth ? 'width="' . $imgwidth . '"' : '';
+        $height = $imgheight ? 'height="' . $imgheight . '"' : '';
+        foreach ($list as $k => &$v) {
+            $v['hasimage'] = $v->getData('image') ? true : false;
+            $v['textlink'] = '<a href="' . $v['url'] . '">' . $v['title'] . '</a>';
+            $v['imglink'] = '<a href="' . $v['url'] . '"><img src="' . $v['image'] . '" border="" ' . $width . ' ' . $height . ' /></a>';
+            $v['img'] = '<img src="' . $v['image'] . '" border="" ' . $width . ' ' . $height . ' />';
+        }
+        return $list;
+    }
+
+    public static function getBlockContent($params)
+    {
+        $field = isset($params['id']) ? 'id' : 'name';
+        $value = isset($params[$field]) ? $params[$field] : '';
+        $cache = !isset($params['cache']) ? true : (int)$params['cache'];
+        $row = self::where($field, $value)
+            ->where('status', 'normal')
+            ->cache($cache)
+            ->find();
+        $result = '';
+        if ($row) {
+            if ($row['content']) {
+                $result = $row['content'];
+            } elseif ($row['image']) {
+                $result = '<img src="' . $row['image'] . '" class="img-responsive"/>';
+            } else {
+                $result = $row['title'];
+            }
+            if ($row['url'] && !$row['content']) {
+                $result = $row['url'] ? '<a href="' . (preg_match("/^https?:\/\/(.*)/i", $row['url']) ? $row['url'] : url($row['url'])) . '">' . $result . '</a>' : $result;
+            }
+        }
+        return $result;
+    }
+}

+ 47 - 0
addons/blog/model/Category.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace addons\blog\model;
+
+use think\Model;
+
+class Category extends Model
+{
+
+    // 表名
+    protected $name = 'blog_category';
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+        'url'
+    ];
+
+    protected static function init()
+    {
+        self::afterInsert(function ($row) {
+            $row->save(['weigh' => $row['id']]);
+        });
+    }
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['status'];
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        $diyname = $data['diyname'] ? $data['diyname'] : $data['id'];
+        return addon_url('blog/category/index', [':id' => $data['id'], ':diyname' => $diyname]);
+    }
+
+}

+ 173 - 0
addons/blog/model/Comment.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace addons\blog\model;
+
+use addons\blog\library\Service;
+use addons\blog\library\CommentException;
+use app\common\library\Email;
+use think\Exception;
+use think\Model;
+use think\Validate;
+
+class Comment extends Model
+{
+
+    // 表名
+    protected $name = 'blog_comment';
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+        'create_date'
+    ];
+    protected $config;
+
+    public function initialize()
+    {
+        $this->config = get_addon_config('blog');
+        parent::initialize();
+    }
+
+    public function getCreateDateAttr($value, $data)
+    {
+        return human_date($data['createtime']);
+    }
+
+    public function getAvatarAttr($value, $data)
+    {
+        if ($this->config['commentavatarmode'] == 'letter') {
+            return letter_avatar($data['username']);
+        } else {
+            return isset($data['avatar']) && $data['avatar'] ? $data['avatar'] : "https://secure.gravatar.com/avatar/" . md5($data['email']) . "?s=96&d=&r=X";
+        }
+    }
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['status'];
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+    public static function postComment($params)
+    {
+        $config = get_addon_config('blog');
+        $request = request();
+        $request->filter('trim,strip_tags,htmlspecialchars');
+        $post_id = (int)$request->post("post_id");
+        $pid = intval($request->post("pid", 0));
+        $username = $request->post("username");
+        $email = $request->post("email", "");
+        $website = $request->post("website", "");
+        $content = $request->post("content");
+        $avatar = $request->post("avatar", "");
+        $subscribe = intval($request->post("subscribe", 0));
+        $useragent = substr($request->server('HTTP_USER_AGENT', ''), 0, 255);
+        $ip = $request->ip();
+        $website = $website != '' && substr($website, 0, 7) != 'http://' && substr($website, 0, 8) != 'https://' ? "http://" . $website : $website;
+        $content = nl2br($content);
+        $token = $request->post('__token__');
+
+        $post = Post::get($post_id, ['category']);
+        if (!$post || $post['status'] == 'hidden') {
+            throw new Exception("日志未找到");
+        }
+
+        //审核状态
+        $status = 'normal';
+        if ($config['iscommentaudit'] == 1) {
+            $status = 'hidden';
+        } elseif ($config['iscommentaudit'] == 0) {
+            $status = 'normal';
+        } elseif ($config['iscommentaudit'] == -1) {
+            if (!Service::isContentLegal($content)) {
+                $status = 'hidden';
+            }
+        }
+
+        $rule = [
+            'pid|父ID'      => 'require|number',
+            'username|用户名' => 'require|chsDash|length:3,30',
+            'email|邮箱'     => 'email|length:3,30',
+            'website|网站'   => 'url|length:3,50',
+            'content|内容'   => 'require|length:3,250',
+            '__token__'    => 'require|token',
+        ];
+        $data = [
+            'pid'       => $pid,
+            'username'  => $username,
+            'email'     => $email,
+            'website'   => $website,
+            'content'   => $content,
+            '__token__' => $token,
+        ];
+        $validate = new Validate($rule);
+        $result = $validate->check($data);
+        if (!$result) {
+            throw new Exception($validate->getError());
+        }
+
+        $lastcomment = self::get(['post_id' => $post_id, 'email' => $email, 'ip' => $ip]);
+        if ($lastcomment && time() - $lastcomment['createtime'] < 30) {
+            throw new Exception("对不起!您发表评论的速度过快!请稍微休息一下,喝杯咖啡");
+        }
+        if ($lastcomment && $lastcomment['content'] == $content) {
+            throw new Exception("您可能连续了相同的评论,请不要重复提交");
+        }
+        $data = [
+            'pid'       => $pid,
+            'post_id'   => $post_id,
+            'username'  => $username,
+            'email'     => $email,
+            'content'   => $content,
+            'avatar'    => $avatar,
+            'ip'        => $ip,
+            'useragent' => $useragent,
+            'subscribe' => (int)$subscribe,
+            'website'   => $website,
+            'status'    => $status
+        ];
+        self::create($data);
+
+        //发送通知
+        if ($status === 'hidden') {
+            throw new CommentException("发表评论成功,但评论需要显示审核后才会展示", 1);
+        } else {
+            $post->setInc('comments');
+            if ($pid) {
+                //查找父评论,是否并发邮件通知
+                $parent = self::get($pid);
+                if ($parent && $parent['subscribe'] && Validate::is($parent['email'], 'email') && $status == 'normal') {
+                    $title = "{$parent['username']},您发表在《{$post['title']}》上的评论有了新回复 - {$config['name']}";
+                    $post_url = $post->fullurl;
+                    $unsubscribe_url = addon_url("blog/comment/unsubscribe", ['id' => $parent['id'], 'key' => md5($parent['id'] . $parent['email'])]);
+                    $content = "亲爱的{$parent['username']}:<br />您于" . date("Y-m-d H:i:s") .
+                        "在《<a href='{$post_url}' target='_blank'>{$post['title']}</a>》上发表的评论<br /><blockquote>{$parent['content']}</blockquote>" .
+                        "<br />{$username}发表了回复,内容是<br /><blockquote>{$content}</blockquote><br />您可以<a href='{$post_url}'>点击查看评论详情</a>。" .
+                        "<br /><br />如果你不愿意再接受最新评论的通知,<a href='{$unsubscribe_url}'>请点击这里取消</a>";
+                    $email = new Email;
+                    $result = $email
+                        ->to($parent['email'])
+                        ->subject($title)
+                        ->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . $content . '</div>')
+                        ->send();
+                }
+            }
+        }
+        return true;
+    }
+
+    public function sublist()
+    {
+        return $this->hasMany("Comment", "pid");
+    }
+
+}

+ 158 - 0
addons/blog/model/Post.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace addons\blog\model;
+
+use think\Cache;
+use think\Model;
+
+class Post extends Model
+{
+
+    // 表名
+    protected $name = 'blog_post';
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+        'url',
+        'create_date',
+    ];
+
+    protected static function init()
+    {
+        self::afterInsert(function ($row) {
+            $row->save(['weigh' => $row['id']]);
+        });
+    }
+
+    public function getCreateDateAttr($value, $data)
+    {
+        return human_date($data['createtime']);
+    }
+
+    public function getThumbAttr($value, $data)
+    {
+        $value = $value ? $value : '/assets/addons/blog/img/thumb.png';
+        return cdnurl($value, true);
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ? $value : '/assets/addons/blog/img/thumb.png';
+        return cdnurl($value, true);
+    }
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ? $value : $data['status'];
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $catename = isset($this->category) && $this->category ? $this->category->diyname : 'all';
+        $cateid = isset($this->category) && $this->category ? $this->category->id : 0;
+        return addon_url('blog/post/index', [':id' => $data['id'], ':diyname' => $diyname, ':catename' => $catename, ':cateid' => $cateid]);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $catename = isset($this->category) && $this->category ? $this->category->diyname : 'all';
+        $cateid = isset($this->category) && $this->category ? $this->category->id : 0;
+        return addon_url('blog/post/index', [':id' => $data['id'], ':diyname' => $diyname, ':catename' => $catename, ':cateid' => $cateid], true, true);
+    }
+
+    /**
+     * 获取日志列表
+     * @param $tag
+     * @return array|false|\PDOStatement|string|\think\Collection
+     */
+    public static function getPostList($tag)
+    {
+        $category = !isset($tag['category']) ? '' : $tag['category'];
+        $condition = empty($tag['condition']) ? '' : $tag['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $flag = empty($tag['flag']) ? '' : $tag['flag'];
+        $row = empty($tag['row']) ? 10 : (int)$tag['row'];
+        $orderby = empty($tag['orderby']) ? 'createtime' : $tag['orderby'];
+        $orderway = empty($tag['orderway']) ? 'desc' : strtolower($tag['orderway']);
+        $limit = empty($tag['limit']) ? $row : $tag['limit'];
+        $cache = !isset($tag['cache']) ? true : (int)$tag['cache'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $cache = !$cache ? false : $cache;
+        $where = ['status' => 'normal'];
+
+        if ($category !== '') {
+            $where['category_id'] = ['in', $category];
+        }
+        //如果有设置标志,则拆分标志信息并构造condition条件
+        if ($flag !== '') {
+            if (stripos($flag, '&') !== false) {
+                $arr = [];
+                foreach (explode('&', $flag) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' AND ', $arr) . ")";
+                }
+            } else {
+                $condition .= ($condition ? ' AND ' : '');
+                $arr = [];
+                foreach (explode(',', str_replace('|', ',', $flag)) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' OR ', $arr) . ")";
+                }
+            }
+        }
+        $order = $orderby == 'rand' ? 'rand()' : (in_array($orderby, ['createtime', 'updatetime', 'views', 'weigh', 'id']) ? "{$orderby} {$orderway}" : "createtime {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $postModel = self::with('category');
+        $list = $postModel
+            ->where($where)
+            ->where($condition)
+            ->field($field)
+            ->cache($cache)
+            ->order($order)
+            ->limit($limit)
+            ->select();
+        //$list = collection($list)->toArray();
+        return $list;
+    }
+
+    /**
+     * 获取SQL查询结果
+     */
+    public static function getQueryList($tag)
+    {
+        $sql = isset($tag['sql']) ? $tag['sql'] : '';
+        $bind = isset($tag['bind']) ? $tag['bind'] : [];
+        $cache = !isset($tag['cache']) ? true : (int)$tag['cache'];
+        $name = md5("sql-" . $tag['sql']);
+        $list = Cache::get($name);
+        if (!$list) {
+            $list = \think\Db::query($sql, $bind);
+            Cache::set($name, $list, $cache);
+        }
+        return $list;
+    }
+
+    public function category()
+    {
+        return $this->belongsTo('addons\blog\model\Category')->setEagerlyType(1);
+    }
+
+}

+ 58 - 0
addons/blog/public/assets/js/backend/blog/block.js

@@ -0,0 +1,58 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'blog/block/index',
+                    add_url: 'blog/block/add',
+                    edit_url: 'blog/block/edit',
+                    del_url: 'blog/block/del',
+                    multi_url: 'blog/block/multi',
+                    table: 'blog_block',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'weigh',
+                columns: [
+                    [
+                        {checkbox: true},
+                        {field: 'id', sortable: true, title: __('Id')},
+                        {field: 'type', title: __('Type'), formatter: Table.api.formatter.search, searchList: Config.typeList},
+                        {field: 'name', title: __('Name'), formatter: Table.api.formatter.search},
+                        {field: 'title', title: __('Title')},
+                        {field: 'image', title: __('Image'), events: Table.api.events.image, formatter: Table.api.formatter.image},
+                        {field: 'url', title: __('Url'), formatter: Table.api.formatter.url},
+                        {field: 'createtime', title: __('Createtime'), sortable: true, operate: 'RANGE', addclass: 'datetimerange', formatter: Table.api.formatter.datetime},
+                        {field: 'updatetime', title: __('Updatetime'), visible: false, sortable: true, operate: 'RANGE', addclass: 'datetimerange', formatter: Table.api.formatter.datetime},
+                        {field: 'status', title: __('Status'), searchList: {"normal":__('Normal'),"hidden":__('Hidden')}, formatter: Table.api.formatter.status},
+                        {field: 'weigh', title: __('Weigh'), visible: false},
+                        {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

+ 102 - 0
addons/blog/public/assets/js/backend/blog/category.js

@@ -0,0 +1,102 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'blog/category/index',
+                    add_url: 'blog/category/add',
+                    edit_url: 'blog/category/edit',
+                    del_url: 'blog/category/del',
+                    multi_url: 'blog/category/multi',
+                    table: 'blog_category',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'weigh',
+                columns: [
+                    [
+                        {checkbox: true},
+                        {field: 'id', title: __('Id')},
+                        {field: 'pid', title: __('Pid'), formatter: Table.api.formatter.search, visible: false},
+                        {field: 'name', title: __('Name')},
+                        {field: 'nickname', title: __('Nickname')},
+                        {
+                            field: 'link', title: __('Url'), formatter: function (value, row, index) {
+                                return '<div class=""><a href="' + row.url + '" class="btn btn-xs btn-default" target="_blank"><i class="fa fa-link"></i></a></div>';
+                            }
+                        },
+                        {field: 'flag', title: __('Flag'), formatter: Table.api.formatter.flag},
+                        {field: 'image', title: __('Image'), events: Table.api.events.image, formatter: Table.api.formatter.image},
+                        {field: 'keywords', title: __('Keywords')},
+                        {field: 'description', title: __('Description')},
+                        {field: 'diyname', title: __('Diyname')},
+                        {field: 'createtime', title: __('Createtime'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange'},
+                        {field: 'updatetime', title: __('Updatetime'), visible: false, formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange'},
+                        {field: 'weigh', title: __('Weigh')},
+                        {field: 'status', title: __('Status'), searchList: {"normal":__('Normal'),"hidden":__('Hidden')}, formatter: Table.api.formatter.status},
+                        {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+
+                $.validator.config({
+                    rules: {
+                        diyname: function (element) {
+                            if (element.value.toString().match(/^\d+$/)) {
+                                return __('Can not be digital');
+                            }
+                            return $.ajax({
+                                url: 'blog/category/check_element_available',
+                                type: 'POST',
+                                data: {id: $("#category-id").val(), name: element.name, value: element.value},
+                                dataType: 'json'
+                            });
+                        }
+                    }
+                });
+                //获取栏目拼音
+                var si;
+                $(document).on("keyup", "#c-name", function () {
+                    var value = $(this).val();
+                    if (value != '' && !value.match(/\n/)) {
+                        clearTimeout(si);
+                        si = setTimeout(function () {
+                            Fast.api.ajax({
+                                loading: false,
+                                url: "blog/ajax/get_title_pinyin",
+                                data: {title: value}
+                            }, function (data, ret) {
+                                $("#c-diyname").val(data.pinyin);
+                                return false;
+                            }, function (data, ret) {
+                                return false;
+                            });
+                        }, 200);
+                    }
+                });
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

Some files were not shown because too many files changed in this diff