Full Code of smallpath/blog for AI

master 8f1a4b7557a5 cached
169 files
245.6 KB
71.6k tokens
65 symbols
1 requests
Download .txt
Showing preview only (290K chars total). Download the full file or copy to clipboard to get everything.
Repository: smallpath/blog
Branch: master
Commit: 8f1a4b7557a5
Files: 169
Total size: 245.6 KB

Directory structure:
gitextract_n6nb127_/

├── Dockerfile
├── LICENSE
├── README.md
├── admin/
│   ├── .babelrc
│   ├── .eslintignore
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── README.md
│   ├── build/
│   │   ├── build.js
│   │   ├── check-versions.js
│   │   ├── dev-client.js
│   │   ├── dev-server.js
│   │   ├── utils.js
│   │   ├── webpack.base.conf.js
│   │   ├── webpack.dev.conf.js
│   │   └── webpack.prod.conf.js
│   ├── config/
│   │   ├── dev.env.js
│   │   ├── index.js
│   │   ├── prod.env.js
│   │   └── test.env.js
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/
│   │   │   ├── Main.vue
│   │   │   ├── containers/
│   │   │   │   ├── Create.vue
│   │   │   │   ├── List.vue
│   │   │   │   ├── Markdown.vue
│   │   │   │   └── Post.vue
│   │   │   ├── pages/
│   │   │   │   ├── Dashboard.vue
│   │   │   │   ├── Login.vue
│   │   │   │   ├── Logout.vue
│   │   │   │   ├── Sidebar.vue
│   │   │   │   └── Top.vue
│   │   │   ├── utils/
│   │   │   │   └── marked.js
│   │   │   └── views/
│   │   │       ├── CreateEditView.js
│   │   │       ├── CreateListView.js
│   │   │       └── CreateMarkdownView.js
│   │   ├── main.js
│   │   ├── route/
│   │   │   └── index.js
│   │   ├── store/
│   │   │   ├── api.js
│   │   │   └── index.js
│   │   └── utils/
│   │       └── error.js
│   ├── static/
│   │   └── .gitkeep
│   └── test/
│       ├── e2e/
│       │   ├── custom-assertions/
│       │   │   └── elementCount.js
│       │   ├── nightwatch.conf.js
│       │   ├── runner.js
│       │   └── specs/
│       │       └── test.js
│       └── unit/
│           ├── .eslintrc
│           ├── index.js
│           ├── karma.conf.js
│           └── specs/
│               └── Hello.spec.js
├── docs/
│   ├── .nojekyll
│   ├── README.md
│   └── index.html
├── front/
│   ├── .babelrc
│   ├── .eslintignore
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── README.md
│   ├── build/
│   │   ├── build-client.js
│   │   ├── setup-dev-server.js
│   │   ├── utils.js
│   │   ├── vue-loader.config.js
│   │   ├── webpack.base.config.js
│   │   ├── webpack.client.config.js
│   │   └── webpack.server.config.js
│   ├── config/
│   │   ├── dev.env.js
│   │   ├── index.js
│   │   ├── prod.env.js
│   │   └── test.env.js
│   ├── middleware/
│   │   ├── favicon.js
│   │   └── serverGoogleAnalytic.js
│   ├── package.json
│   ├── production.js
│   ├── server/
│   │   ├── config.js
│   │   ├── model.js
│   │   ├── mongo.tpl
│   │   ├── robots.js
│   │   ├── rss.js
│   │   ├── server-axios.js
│   │   └── sitemap.js
│   ├── server.js
│   ├── src/
│   │   ├── assets/
│   │   │   ├── css/
│   │   │   │   ├── article.css
│   │   │   │   ├── base.css
│   │   │   │   ├── footer.css
│   │   │   │   ├── header.css
│   │   │   │   ├── highlight.css
│   │   │   │   ├── icon.css
│   │   │   │   ├── pagination.css
│   │   │   │   ├── responsive.css
│   │   │   │   └── sidebar.css
│   │   │   └── js/
│   │   │       └── base.js
│   │   ├── client-entry.js
│   │   ├── components/
│   │   │   ├── App.vue
│   │   │   ├── Archive.vue
│   │   │   ├── BlogPager.vue
│   │   │   ├── BlogSummary.vue
│   │   │   ├── Disqus.vue
│   │   │   ├── Footer.vue
│   │   │   ├── Header.vue
│   │   │   ├── Loading.vue
│   │   │   ├── Pagination.vue
│   │   │   ├── Post.vue
│   │   │   ├── Sidebar.vue
│   │   │   ├── Tag.vue
│   │   │   └── TagPager.vue
│   │   ├── index.template.html
│   │   ├── main.js
│   │   ├── mixin/
│   │   │   ├── disqus.js
│   │   │   └── image.js
│   │   ├── route/
│   │   │   ├── create-route-client.js
│   │   │   ├── create-route-server.js
│   │   │   └── index.js
│   │   ├── server-entry.js
│   │   ├── store/
│   │   │   ├── api.js
│   │   │   ├── client-axios.js
│   │   │   ├── create-api-client.js
│   │   │   ├── create-api-server.js
│   │   │   ├── index.js
│   │   │   └── vuex.js
│   │   ├── utils/
│   │   │   ├── 404.js
│   │   │   └── clientGoogleAnalyse.js
│   │   └── views/
│   │       └── CreatePostView.js
│   └── test/
│       ├── e2e/
│       │   ├── custom-assertions/
│       │   │   └── elementCount.js
│       │   ├── nightwatch.conf.js
│       │   ├── runner.js
│       │   └── specs/
│       │       └── test.js
│       └── unit/
│           ├── .eslintrc
│           ├── index.js
│           ├── karma.conf.js
│           └── specs/
│               └── Hello.spec.js
├── pm2.json
└── server/
    ├── .eslintignore
    ├── .eslintrc.js
    ├── .gitignore
    ├── README.md
    ├── app.js
    ├── blogpack.js
    ├── build/
    │   ├── blogpack.base.config.js
    │   ├── blogpack.dev.config.js
    │   └── blogpack.prod.config.js
    ├── conf/
    │   ├── config.tpl
    │   └── option.js
    ├── entry.js
    ├── model/
    │   ├── mongo.js
    │   └── redis.js
    ├── mongoRest/
    │   ├── actions.js
    │   ├── index.js
    │   └── routes.js
    ├── package.json
    ├── plugins/
    │   ├── beforeRestful/
    │   │   └── checkAuth.js
    │   ├── beforeServerStart/
    │   │   ├── initOption.js
    │   │   ├── initUser.js
    │   │   └── installTheme.js
    │   ├── beforeUseRoutes/
    │   │   ├── bodyParser.js
    │   │   ├── logTime.js
    │   │   ├── ratelimit.js
    │   │   └── restc.js
    │   └── mountingRoute/
    │       ├── login.js
    │       ├── logout.js
    │       └── qiniu.js
    ├── service/
    │   └── token.js
    ├── test/
    │   ├── data/
    │   │   ├── index.js
    │   │   └── post.js
    │   ├── index.js
    │   ├── integration/
    │   │   └── postRestful.js
    │   └── unit/
    │       └── postQuery.js
    ├── theme/
    │   └── firekylin.js
    └── utils/
        └── log.js

================================================
FILE CONTENTS
================================================

================================================
FILE: Dockerfile
================================================

FROM node:7.7

MAINTAINER Jerry Bendy <jerry@icewingcc.com>, qfdk <qfdk2010#gmail.com>

# copy all files to target 
COPY . /app


# install global packages
RUN npm install -g yarn pm2 --registry=https://registry.npm.taobao.org

# install dependences
RUN cd /app/server && cp conf/config.tpl conf/config.js && yarn
RUN cd /app/front && cp server/mongo.tpl server/mongo.js && yarn && npm run build
RUN cd /app/admin && yarn && npm run build

# clean cache
RUN npm cache clean



WORKDIR /app

EXPOSE 3000
EXPOSE 8080


CMD pm2-docker start pm2.json



================================================
FILE: LICENSE
================================================
                                 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:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) 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

      (d) 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 {yyyy} {name of copyright owner}

   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.


================================================
FILE: README.md
================================================
# Blog
A blog system. Based on Vue2, Koa2, MongoDB and Redis

前后端分离 + 服务端渲染的博客系统, 前端 SPA + 后端 RESTful 服务器

# Demo
前端:[https://smallpath.me](https://smallpath.me)  
后台管理截图:[https://smallpath.me/post/blog-back-v2](https://smallpath.me/post/blog-back-v2)

Table of Contents
=================

* [TODO](#todo)
* [构建与部署](#构建与部署)
  * [前置](#前置)
  * [server](#server)
  * [front](#front)
  * [admin](#admin)
* [后端RESTful API](#后端restful-api)
  * [说明](#说明)
  * [HTTP动词](#http动词)
  * [权限验证](#权限验证)
      * [获得Token](#获得token)
      * [撤销Token](#撤销token)
      * [Token说明](#token说明)
  * [查询](#查询)
  * [新建](#新建)
  * [替换](#替换)
  * [更新](#更新)
  * [删除](#删除)

# TODO

> [TODO](https://github.com/smallpath/blog/issues/14)

# 构建与部署

## 前置

- Node v6
- pm2
- MongoDB
- Redis

## server

<details>
<summary>博客的提供RESTful API的后端</summary>

复制conf文件夹中的默认配置`config.tpl`, 并命名为`config.js`

有如下属性可以自行配置:

- `tokenSecret`
  - 改为任意字符串
- `defaultAdminPassword`
  - 默认密码, 必须修改, 否则服务器将拒绝启动

如果mongoDB或redis不在本机对应端口,可以修改对应的属性
- `mongoHost`
- `mongoDatabase`
- `mongoPort`
- `redisHost`
- `redisPort`

如果需要启用后台管理单页的七牛图片上传功能,请再修改如下属性:
- `qiniuAccessKey`
  - 七牛账号的公钥
- `qiniuSecretKey`
  - 七牛账号的私钥
- `qiniuBucketHost`
  - 七牛Bucket对应的外链域名
- `qiniuBucketName`
  - 七牛Bucket的名称
- `qiniuPipeline`
  - 七牛多媒体处理队列的名称

```
npm install
pm2 start entry.js
```

RESTful服务器在本机3000端口开启

</details>

## front

<details>
<summary>博客的前台单页, 支持服务端渲染</summary>

复制server文件夹中的默认配置`mongo.tpl`, 并命名为`mongo.js`

如果mongoDB不在本机对应端口,请自行配置`mongo.js`中的属性:

- `mongoHost`
- `mongoDatabase`
- `mongoPort`

```
npm install
npm run build
pm2 start production.js
```

请将`logo.png`与`favicon.ico`放至`static`目录中

再用nginx代理本机8080端口即可, 可以使用如下的模板

```
server{
    listen 80;                                      #如果是https, 则替换80为443
    server_name *.smallpath.me smallpath.me;        #替换域名
    root /alidata/www/Blog/front/dist;       #替换路径为构建出来的dist路径
    set $node_port 3000;
    set $ssr_port 8080;

    location ^~ / {
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$ssr_port;
        proxy_redirect off;
    }

    location ^~ /proxyPrefix/ {
        rewrite ^/proxyPrefix/(.*) /$1 break;
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$node_port;
        proxy_redirect off;
    }

    location ^~ /dist/ {
        rewrite ^/dist/(.*) /$1 break;
        etag         on;
        expires      max;
    }

    location ^~ /static/ {
        etag         on;
        expires      max;
    }
}
```

开发端口为本机8080

</details>

## admin

<details>
<summary>博客的后台管理单页</summary>

```
npm install
npm run build
```

用nginx代理构建出来的`dist`文件夹即可, 可以使用如下的模板

```
server{
    listen 80;                                     #如果是https, 则替换80为443
    server_name admin.smallpath.me;                #替换域名
    root /alidata/www/Blog/admin/dist;       #替换路径为构建出来的dist路径
    set $node_port 3000;

    index index.js index.html index.htm;

    location / {
        try_files $uri $uri/ @rewrites;
    }

    location @rewrites {
        rewrite ^(.*)$ / last;
    }

    location ^~ /proxyPrefix/ {
        rewrite ^/proxyPrefix/(.*) /$1 break;
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$node_port;
        proxy_redirect off;
    }

    location ^~ /static/ {
        etag         on;
        expires      max;
    }
}
```

开发端口为本机8082

</details>

# 后端 RESTful API

## 说明

后端服务器默认开启在 3000 端口, 如不愿意暴露 IP, 可以自行设置 nginx 代理, 或者直接使用前端两个单页的代理前缀`/proxyPrefix`

例如, demo的API根目录如下:

> https://smallpath.me/proxyPrefix/api/:modelName/:id

其中, `:modelName`为模型名, 总计如下6个模型

```
post
theme
tag
category
option
user
```

`:id`为指定的文档ID, 用以对指定文档进行 CRUD

## HTTP 动词

支持如下五种:

``` bash
GET     //查询
POST    //新建
PUT     //替换
PATCH   //更新部分属性
DELETE  //删除指定ID的文档
```

有如下两个规定:

- 对所有请求
  - header中必须将 `Content-Type` 设置为 `application/json` , 需要 `body` 的则 `body` 必须是合法 JSON格式
- 对所有回应
  - header中的`Content-Type`均为`application/json`, 且返回的数据也是 JSON格式

## 权限验证

服务器直接允许对`user`模型外的所有模型的GET请求

`user`表的所有请求, 以及其他表的非 GET 请求, 都必须将 header 中的`authorization`设置为服务器下发的 Token, 服务器验证通过后才会继续执行 CRUD 操作

### 获得 Token
> POST https://smallpath.me/proxyPrefix/admin/login

`body`格式如下:

```
{
	"name": "admin",
	"password": "testpassword"
}
```

成功, 则返回带有`token`字段的 JSON 数据
```
{
  "status": "success",
  "token": "tokenExample"
}
```

失败, 则返回如下格式的 JSON 数据:
```
{
  "status": "fail",
  "description": "Get token failed. Check name and password"
}
```

获取到`token`后, 在上述需要 token 验证的请求中, 请将 header 中的`authorization`设置为服务器下发的 Token, 否则请求将被服务器拒绝

### 撤销 Token
> POST https://smallpath.me/proxyPrefix/admin/logout

将`header`中的`authorization`设置为服务器下发的 token, 即可撤销此 token

### Token说明
Token 默认有效期为获得后的一小时, 超出时间后请重新请求 Token  
如需自定义有效期, 请修改服务端配置文件中的`tokenExpiresIn`字段, 其单位为秒

## 查询 

服务器直接允许对`user`模型外的所有模型的 GET 请求, 不需要验证 Token

为了直接通过 URI 来进行 mongoDB 查询, 后台提供六种关键字的查询:
```
conditions,
select,
count,
sort,
skip,
limit
```

### 条件(conditions)查询
类型为JSON, 被解析为对象后, 直接将其作为`mongoose.find`的查询条件

#### 查询所有文档
> GET https://smallpath.me/proxyPrefix/api/post

#### 查询title字段为'关于'的文档
> GET https://smallpath.me/proxyPrefix/api/post?conditions={"title":"关于"}

#### 查询指定id的文档的上一篇文档
> GET https://smallpath.me/proxyPrefix/api/post/?conditions={"_id":{"$lt":"580b3ff504f59b4cc27845f0"}}&sort=1&limit=1

#### select查询
类型为JSON, 用以拾取每条数据所需要的属性名, 以过滤输出来加快响应速度

#### 查询title字段为'关于'的文档的建立时间和更新时间
> GET https://smallpath.me/proxyPrefix/api/post?conditions={"title":"关于"}&select={"createdAt":1,"updatedAt":1}

### count查询
获得查询结果的数量

#### 查询文档的数量
> GET https://smallpath.me/proxyPrefix/api/post?conditions={"type":0}&count=1

### sort查询

#### 查询所有文档并按id倒序
> GET https://smallpath.me/proxyPrefix/api/post?sort={"_id":-1}

#### 查询所有文档并按更新时间倒序
> GET https://smallpath.me/proxyPrefix/api/post?sort={"updatedAt":-1}

### skip 查询和 limit 查询

#### 查询第2页的文档(每页10条)并按时间倒叙
> GET https://smallpath.me/proxyPrefix/api/post?limit=10&skip=10&sort=1

## 新建

需要验证Token

> POST https://smallpath.me/proxyPrefix/api/:modelName

Body中为用来新建文档的JSON数据

每个模型的具体字段, 可以查看该模型的[Schema定义](https://github.com/smallpath/blog/blob/master/server/model/mongo.js#L24)来获得

## 替换

需要验证Token

> PUT https://smallpath.me/proxyPrefix/api/:modelName/:id

`:id`为查询到的文档的`_id`属性, Body中为用来替换该文档的JSON数据

## 更新

需要验证Token

> PATCH https://smallpath.me/proxyPrefix/api/:modelName/:id

`:id`为查询到的文档的`_id`属性, Body中为用来更新该文档的JSON数据

更新操作请使用`PATCH`而不是`PUT`

## 删除

需要验证 Token

> DELETE https://smallpath.me/proxyPrefix/api/:modelName/:id

删除指定 ID 的文档


================================================
FILE: admin/.babelrc
================================================
{
  "presets": ["es2015", "stage-2"],
  "plugins": ["transform-runtime"],
  "comments": false
}


================================================
FILE: admin/.eslintignore
================================================
build/*.js
config/*.js


================================================
FILE: admin/.eslintrc.js
================================================
module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module'
  },
  // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
  extends: 'standard',
  // required to lint *.vue files
  plugins: [
    'html'
  ],
  // add your custom rules here
  'rules': {
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0,
    'space-before-function-paren': ['error', 'never'],
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}


================================================
FILE: admin/.gitignore
================================================
.DS_Store
node_modules/
dist/
npm-debug.log
test/unit/coverage
test/e2e/reports
selenium-debug.log
.editorconfig

================================================
FILE: admin/README.md
================================================
# admin

> 博客的后台管理单页

## 开发测试环境

``` bash
npm install
npm run dev
```
开发端口为本机8082


## 生产环境

``` bash
npm install
npm run build
```

用nginx代理构建出来的`dist`文件夹即可, 可以使用如下的模板

```
server{
    listen 80;                                     #如果是https, 则替换80为443
    server_name admin.smallpath.me;                #替换域名
    root /alidata/www/Blog/admin/dist;       #替换路径为构建出来的dist路径
    set $node_port 3000;

    index index.js index.html index.htm;

    location / {
        try_files $uri $uri/ @rewrites;
    }

    location @rewrites {
        rewrite ^(.*)$ / last;
    }

    location ^~ /proxyPrefix/ {
        rewrite ^/proxyPrefix/(.*) /$1 break;
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$node_port;
        proxy_redirect off;
    }

    location ^~ /static/ {
        etag         on;
        expires      max;
    }
}
```


================================================
FILE: admin/build/build.js
================================================
// https://github.com/shelljs/shelljs
require('./check-versions')()
require('shelljs/global')
env.NODE_ENV = 'production'

var path = require('path')
var config = require('../config')
var ora = require('ora')
var webpack = require('webpack')
var webpackConfig = require('./webpack.prod.conf')

console.log(
  '  Tip:\n' +
  '  Built files are meant to be served over an HTTP server.\n' +
  '  Opening index.html over file:// won\'t work.\n'
)

var spinner = ora('building for production...')
spinner.start()

var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
rm('-rf', assetsPath)
mkdir('-p', assetsPath)
cp('-R', 'static/*', assetsPath)

webpack(webpackConfig, function (err, stats) {
  spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }) + '\n')
})


================================================
FILE: admin/build/check-versions.js
================================================
var semver = require('semver')
var chalk = require('chalk')
var packageConfig = require('../package.json')
var exec = function (cmd) {
  return require('child_process')
    .execSync(cmd).toString().trim()
}

var versionRequirements = [
  {
    name: 'node',
    currentVersion: semver.clean(process.version),
    versionRequirement: packageConfig.engines.node
  },
  {
    name: 'npm',
    currentVersion: exec('npm --version'),
    versionRequirement: packageConfig.engines.npm
  }
]

module.exports = function () {
  var warnings = []
  for (var i = 0; i < versionRequirements.length; i++) {
    var mod = versionRequirements[i]
    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()
    for (var i = 0; i < warnings.length; i++) {
      var warning = warnings[i]
      console.log('  ' + warning)
    }
    console.log()
    process.exit(1)
  }
}


================================================
FILE: admin/build/dev-client.js
================================================
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')

hotClient.subscribe(function (event) {
  if (event.action === 'reload') {
    window.location.reload()
  }
})


================================================
FILE: admin/build/dev-server.js
================================================
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var opn = require('opn')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = process.env.NODE_ENV === 'testing'
  ? require('./webpack.prod.conf')
  : require('./webpack.dev.conf')

// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable

var app = express()
var compiler = webpack(webpackConfig)

var devMiddleware = require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
  stats: {
    colors: true,
    chunks: false
  }
})

var hotMiddleware = require('webpack-hot-middleware')(compiler)
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
  var options = proxyTable[context]
  if (typeof options === 'string') {
    options = { target: options }
  }
  app.use(proxyMiddleware(context, options))
})

// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())

// serve webpack bundle output
app.use(devMiddleware)

// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)

// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))

module.exports = app.listen(port, function (err) {
  if (err) {
    console.log(err)
    return
  }
  var uri = 'http://localhost:' + port
  console.log('Listening at ' + uri + '\n')

  // when env is testing, don't need open it
  if (process.env.NODE_ENV !== 'testing') {
    opn(uri)
  }
})


================================================
FILE: admin/build/utils.js
================================================
var path = require('path')
var config = require('../config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')

exports.assetsPath = function (_path) {
  var assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory
  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function (options) {
  options = options || {}
  // generate loader string to be used with extract text plugin
  function generateLoaders (loaders) {
    var sourceLoader = loaders.map(function (loader) {
      var extraParamChar
      if (/\?/.test(loader)) {
        loader = loader.replace(/\?/, '-loader?')
        extraParamChar = '&'
      } else {
        loader = loader + '-loader'
        extraParamChar = '?'
      }
      return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '')
    }).join('!')

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      return ExtractTextPlugin.extract('vue-style-loader', sourceLoader)
    } else {
      return ['vue-style-loader', sourceLoader].join('!')
    }
  }

  // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
  return {
    css: generateLoaders(['css']),
    postcss: generateLoaders(['css']),
    less: generateLoaders(['css', 'less']),
    sass: generateLoaders(['css', 'sass?indentedSyntax']),
    scss: generateLoaders(['css', 'sass']),
    stylus: generateLoaders(['css', 'stylus']),
    styl: generateLoaders(['css', 'stylus'])
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  var output = []
  var loaders = exports.cssLoaders(options)
  for (var extension in loaders) {
    var loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      loader: loader
    })
  }
  return output
}


================================================
FILE: admin/build/webpack.base.conf.js
================================================
var path = require('path')
var config = require('../config')
var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../')

var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
// various preprocessor loaders added to vue-loader at the end of this file
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd

module.exports = {
  entry: {
    app: './src/main.js'
  },
  output: {
    path: config.build.assetsRoot,
    publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
    filename: '[name].js'
  },
  resolve: {
    extensions: ['', '.js', '.vue'],
    fallback: [path.join(__dirname, '../node_modules')],
    alias: {
      'vue$': 'vue/dist/vue.common',
      'src': path.resolve(__dirname, '../src'),
      'assets': path.resolve(__dirname, '../src/assets'),
      'components': path.resolve(__dirname, '../src/components')
    }
  },
  resolveLoader: {
    fallback: [path.join(__dirname, '../node_modules')]
  },
  module: {
    // preLoaders: [
    //   {
    //     test: /\.vue$/,
    //     loader: 'eslint',
    //     include: projectRoot,
    //     exclude: /node_modules/
    //   },
    //   {
    //     test: /\.js$/,
    //     loader: 'eslint',
    //     include: projectRoot,
    //     exclude: /node_modules/
    //   }
    // ],
    loaders: [
      {
        test: /\.vue$/,
        loader: 'vue'
      },
      {
        test: /\.js$/,
        loader: 'babel',
        include: projectRoot,
        exclude: /node_modules/
      },
      {
        test: /\.json$/,
        loader: 'json'
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url',
        query: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url',
        query: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  eslint: {
    formatter: require('eslint-friendly-formatter')
  },
  vue: {
    loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }),
    postcss: [
      require('autoprefixer')({
        browsers: ['last 2 versions']
      })
    ]
  }
}


================================================
FILE: admin/build/webpack.dev.conf.js
================================================
var config = require('../config')
var webpack = require('webpack')
var merge = require('webpack-merge')
var utils = require('./utils')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')

// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})

module.exports = merge(baseWebpackConfig, {
  module: {
    loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  },
  // eval-source-map is faster for development
  devtool: '#eval-source-map',
  plugins: [
    new webpack.DefinePlugin({
      'process.env': config.dev.env
    }),
    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    })
  ]
})


================================================
FILE: admin/build/webpack.prod.conf.js
================================================
var path = require('path')
var config = require('../config')
var utils = require('./utils')
var webpack = require('webpack')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var env = process.env.NODE_ENV === 'testing'
  ? require('../config/test.env')
  : config.build.env

var webpackConfig = merge(baseWebpackConfig, {
  module: {
    loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  vue: {
    loaders: utils.cssLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
    })
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new webpack.optimize.OccurrenceOrderPlugin(),
    // extract css into its own file
    new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: process.env.NODE_ENV === 'testing'
        ? 'index.html'
        : config.build.index,
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    }),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module, count) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    })
  ]
})

if (config.build.productionGzip) {
  var CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

module.exports = webpackConfig


================================================
FILE: admin/config/dev.env.js
================================================
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})


================================================
FILE: admin/config/index.js
================================================
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')

module.exports = {
  build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../dist/index.html'),
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    productionSourceMap: false,
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css']
  },
  dev: {
    env: require('./dev.env'),
    port: 8082,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {
      '/proxyPrefix': {
        target: 'http://localhost:3000/',
        changeOrigin: true,
        pathRewrite: {
          '^/proxyPrefix': ''
        }
      }
    },
    // CSS Sourcemaps off by default because relative paths are "buggy"
    // with this option, according to the CSS-Loader README
    // (https://github.com/webpack/css-loader#sourcemaps)
    // In our experience, they generally work as expected,
    // just be aware of this issue when enabling this option.
    cssSourceMap: false
  }
}


================================================
FILE: admin/config/prod.env.js
================================================
module.exports = {
  NODE_ENV: '"production"'
}


================================================
FILE: admin/config/test.env.js
================================================
var merge = require('webpack-merge')
var devEnv = require('./dev.env')

module.exports = merge(devEnv, {
  NODE_ENV: '"testing"'
})


================================================
FILE: admin/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>back</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: admin/package.json
================================================
{
  "name": "admin",
  "version": "1.0.0",
  "description": "admin spa for blog",
  "author": "Smallpath <smallpath2013@gmail.com>",
  "private": true,
  "scripts": {
    "dev": "node build/dev-server.js",
    "build": "node build/build.js",
    "unit": "karma start test/unit/karma.conf.js --single-run",
    "e2e": "node test/e2e/runner.js",
    "test": "npm run unit && npm run e2e",
    "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
  },
  "dependencies": {
    "axios": "^0.15.2",
    "element-ui": "^1.0.6",
    "highlight.js": "^9.8.0",
    "marked": "^0.3.6",
    "moment": "^2.16.0",
    "vue": "^2.1.4",
    "vue-router": "^2.1.0",
    "vuex": "^2.0.0",
    "vuex-router-sync": "^3.0.0"
  },
  "devDependencies": {
    "autoprefixer": "^6.4.0",
    "babel-core": "^6.0.0",
    "babel-eslint": "^7.0.0",
    "babel-loader": "^6.0.0",
    "babel-plugin-transform-runtime": "^6.0.0",
    "babel-preset-es2015": "^6.0.0",
    "babel-preset-stage-2": "^6.0.0",
    "babel-register": "^6.0.0",
    "chai": "^3.5.0",
    "chalk": "^1.1.3",
    "connect-history-api-fallback": "^1.1.0",
    "cross-spawn": "^4.0.2",
    "css-loader": "^0.25.0",
    "eslint": "^3.7.1",
    "eslint-config-airbnb-base": "^8.0.0",
    "eslint-config-standard": "^6.2.1",
    "eslint-friendly-formatter": "^2.0.5",
    "eslint-import-resolver-webpack": "^0.6.0",
    "eslint-loader": "^1.5.0",
    "eslint-plugin-html": "^1.3.0",
    "eslint-plugin-import": "^1.16.0",
    "eslint-plugin-promise": "^3.3.2",
    "eslint-plugin-standard": "^2.0.1",
    "eventsource-polyfill": "^0.9.6",
    "express": "^4.13.3",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.9.0",
    "function-bind": "^1.0.2",
    "html-webpack-plugin": "^2.8.1",
    "http-proxy-middleware": "^0.17.2",
    "inject-loader": "^2.0.1",
    "isparta-loader": "^2.0.0",
    "json-loader": "^0.5.4",
    "lolex": "^1.4.0",
    "mocha": "^3.1.0",
    "nightwatch": "^0.9.8",
    "node-sass": "^4.5.0",
    "opn": "^4.0.2",
    "ora": "^0.3.0",
    "sass-loader": "^6.0.2",
    "semver": "^5.3.0",
    "shelljs": "^0.7.4",
    "sinon": "^1.17.3",
    "sinon-chai": "^2.8.0",
    "url-loader": "^0.5.7",
    "vue-loader": "^9.4.0",
    "vue-style-loader": "^1.0.0",
    "webpack": "^1.13.2",
    "webpack-dev-middleware": "^1.8.3",
    "webpack-hot-middleware": "^2.12.2",
    "webpack-merge": "^0.14.1"
  },
  "engines": {
    "node": ">= 4.0.0",
    "npm": ">= 3.0.0"
  }
}


================================================
FILE: admin/src/App.vue
================================================
<template>
  <div id="app">
    <keep-alive>
        <router-view></router-view>
    </keep-alive>
  </div>
</template>

<script>
import api from './store/api'
import { getChineseDesc } from './utils/error'

export default {
  name: 'app',
  data() {
    return {}
  },
  computed: {
    siteInfo() {
      return this.$store.state.siteInfo
    }
  },
  beforeMount() {
    this.$store.dispatch('FETCH_OPTIONS').then(() => {
      if (this.siteInfo['title'] && typeof document !== 'undefined') {
        document.title = this.siteInfo['title'].value
      }
    })

    const { request } = api

    request.interceptors.request.use((config) => {
      const token = window.localStorage.getItem('token')

      if (config.method === 'get' && config.url.indexOf('/proxyPrefix/api/user') === -1) {
        return config
      }

      if (token !== null && typeof token !== 'undefined') {
        config.headers['authorization'] = token
      }

      return config
    }, (error) => Promise.reject(error))

    request.interceptors.response.use((response) => {
      if (this.$store.state.route.name === 'logout') {
        return response
      }
      if (response.data && response.data.status && response.data.status === 'fail') {
        let desc = getChineseDesc(response.data.description)
        this.$notify.error(desc)
        return Promise.reject(desc)
      }
      return response
    }, (error) => Promise.reject(error))
  }
}
</script>

<style lang="scss">
html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  margin: 0px;

  #app {
    height: 100%;
  }
}

</style>


================================================
FILE: admin/src/components/Main.vue
================================================
<template>
  <div class="dashboard">
    <top></top>
    <sidebar></sidebar>
    <div class="main">
      <router-view :key="$route.params.id || -1"></router-view>
    </div>
  </div>
</template>

<script>
import Sidebar from './pages/Sidebar'
import Top from './pages/Top'

export default {
  name: 'dashboard',
  data() {
    return {
      title: ''
    }
  },
  components: {
    Sidebar,
    Top
  }
}
</script>

<style lang="scss" scoped>
.dashboard {
  height: 100%;

  .main {
    box-sizing: border-box;
    padding-bottom: 60px;
    position: fixed;
    top: 60px;
    left: 148px;
    height: 100%;
    width: -webkit-calc(100% - 148px);
    width:    -moz-calc(100% - 148px);
    width:         calc(100% - 148px);
    overflow: auto;
  }

  @media all and (max-width: 867px) {
    .main{
      left: 145px;
    }
  }
}

</style>


================================================
FILE: admin/src/components/containers/Create.vue
================================================
<template>
  <el-form ref="form" :model="form" label-width="120px">
    <el-form-item v-for="item in options.items" :label="item.label">
      <el-input v-if="typeof item.description === 'undefined'" :autosize="{ minRows: 2, maxRows: 16}" :type="item.type || 'text'" v-model="form[item.prop]"></el-input>
      <el-popover
        v-if="typeof item.description !== 'undefined'"
        placement="right-start"
        :title="item.label"
        width="50%"
        trigger="hover"
        :content="item.description">
        <el-input slot="reference" :autosize="{ minRows: 2, maxRows: 16}" :type="item.type || 'text'" v-model="form[item.prop]"></el-input>
      </el-popover>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click.native="onSubmit">提交</el-button>
    </el-form-item>
  </el-form>
</template>

<script>
const blackModelArr = ['option', 'user']

export default {
  name: 'list',
  props: ['options'],
  data() {
    let isPost = this.options.name === 'post'
    let isPage = this.options.name === 'page'
    let form = this.options.items.reduce((prev, curr) => {
      prev[curr.prop] = curr.default
      return prev
    }, {})
    return {
      isPost,
      isPage,
      form,
      isLoading: true
    }
  },
  computed: {
    list() {
      return this.$store.state.list
    }
  },
  methods: {
    parseTypeBeforeSubmit() {
      let isOk = true
      this.options.items.forEach(item => {
        try {
          if (item.sourceType === 'Object') {
            this.form[item.prop] = JSON.parse(this.form[item.prop], null, 2)
          }
        } catch (err) {
          isOk = false
        }
      })
      return isOk
    },
    parseTypeAfterFetch() {
      this.options.items.forEach(item => {
        if (item.sourceType === 'Object') {
          this.form[item.prop] = JSON.stringify(this.form[item.prop], null, 2)
        }
      })
    },
    setParams(response) {
      let id = this.$route.params.id
      if (response._id && typeof id === 'undefined') {
        this.$router.replace({ params: { id: response._id } })
        this.form = response
      }
    },
    onSubmit() {
      if (this.parseTypeBeforeSubmit() === false) {
        return this.$message.error('属性验证失败')
      }
      let id = this.$route.params.id
      if (typeof id !== 'undefined') {
        // patch
        return this.$store.dispatch('PATCH', Object.assign({}, {
          id: this.$route.params.id,
          form: this.form
        }, this.options)).then(response => {
          this.parseTypeAfterFetch()
          this.$message({
            message: '已成功提交',
            type: 'success'
          })
          this.setParams(response)
        }).catch(err => console.error(err))
      } else {
        // post
        return this.$store.dispatch('POST', Object.assign({}, {
          form: this.form
        }, this.options)).then(response => {
          this.parseTypeAfterFetch()
          this.$message({
            message: '已成功提交',
            type: 'success'
          })
          let model = this.options.model
          if (blackModelArr.indexOf(model) === -1) {
            this.setParams(response)
          }
        }).catch(err => console.error(err))
      }
    }
  },
  created() {
    // flatten user and options into obj
    if (this.options.isPlain === true) {
      return this.$store.dispatch('FETCH_CREATE', Object.assign({}, {
        id: -1
      }, this.options)).then(() => {
        this.form = Object.assign({}, this.$store.state.curr)
        this.isLoading = false
      }).catch(err => console.error(err))
    }

    // if params has value , fetch from the model
    if (typeof this.$route.params.id !== 'undefined') {
      return this.$store.dispatch('FETCH_CREATE', Object.assign({}, {
        id: this.$route.params.id
      }, this.options)).then(() => {
        this.form = Object.assign({}, this.$store.state.curr)
        this.parseTypeAfterFetch()
        this.isLoading = false
      }).catch(err => console.error(err))
    }
    // else it's a new page, nothing to do
  }
}
</script>

<style lang="scss" scoped>
  .el-form {
    width: 40%;
    margin-top: 20px;

    .el-button {
      width: 100%;
    }
  }
</style>

================================================
FILE: admin/src/components/containers/List.vue
================================================
<template>
  <el-table
    :data="list"
    v-loading.body="isLoading"
    border
    style="width: 100%">
    <el-table-column
      v-for="item in options.items"
      :prop="item.prop"
      :label="item.label"
      :width="item.width">
    </el-table-column>
    <el-table-column
      v-if="isPost"
      prop="category"
      label="分类"
      width="120"
      inline-template>
      <el-tag v-if="row.category" :type="'primary'" close-transition>{{row.category}}</el-tag>
    </el-table-column>
    <el-table-column
      v-if="isPost"
      prop="tags"
      label="标签"
      width="180"
      :filters="filters"
      :filter-method="filterTag"
      inline-template>
      <el-tag v-for="tag in row.tags" :type="0 ? 'primary' : 'success'" close-transition>{{tag}}</el-tag>
    </el-table-column>
    <el-table-column
      inline-template
      v-if="!options.isButtonFixed"
      :context="_self"
      label="操作"
      width="150">
      <span>
        <el-button @click="handleClick(row)" type="info" size="small">编辑</el-button>
        <el-button @click="handleDelete(row, $index)" type="danger" size="small">删除</el-button>
      </span>
    </el-table-column>
    <el-table-column
      inline-template
      v-if="options.isButtonFixed"
      fixed="right"
      :context="_self"
      label="操作"
      width="150">
      <span>
        <el-button @click="handleClick(row)" type="info" size="small">编辑</el-button>
        <el-button @click="handleDelete(row, $index)" type="danger" size="small">删除</el-button>
      </span>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  name: 'list',
  props: ['options'],
  data() {
    let isPost = this.options.name === 'post'
    let isPage = this.options.name === 'page'
    return {
      isPost,
      isPage,
      isLoading: true
    }
  },
  computed: {
    list() {
      return this.$store.state.list
    },
    filters() {
      if (!this.isPost && !this.isPage) return []

      let obj = this.list.reduce((prev, value) => {
        Array.isArray(value.tags) && value.tags.forEach(tag => {
          prev[tag] = {
            text: tag,
            value: tag
          }
        })
        return prev
      }, {})
      return Object.keys(obj).map(value => {
        return obj[value]
      })
    }
  },
  methods: {
    filterTag(value, row) {
      return row.tags.indexOf(value) !== -1
    },
    handleClick({ _id }) {
      this.$router.push({
        path: `/${this.options.name}/create/${_id}`
      })
    },
    handleDelete({ _id }, index) {
      this.$store.dispatch('DELETE', Object.assign({}, {
        id: _id
      }, this.options)).then(() => {
        this.$store.state.list.splice(index, 1)
      }).catch(err => console.error(err))
    }
  },
  created() {
    this.$store.dispatch('FETCH_LIST', this.options).then(() => {
      this.isLoading = false
    }).catch(err => console.error(err))
  }
}
</script>

================================================
FILE: admin/src/components/containers/Markdown.vue
================================================
<template>
  <div class="md-panel">
    <el-menu default-active="1" class="el-menu-demo" mode="horizontal" @select="handleSelect">
      <el-menu-item index="1">加粗</el-menu-item>
      <el-menu-item index="2">斜体</el-menu-item>
      <el-menu-item index="3">引用</el-menu-item>
      <el-menu-item index="4">代码段</el-menu-item>
      <el-submenu index="5">
        <template slot="title">插入图片</template>
        <el-menu-item index="5-1"><i class="el-icon-upload2"></i>上传图片</el-menu-item>
        <el-menu-item index="5-2"><i class="el-icon-upload"></i>网络图片</el-menu-item>
      </el-submenu>
      <el-menu-item index="6"><i class="el-icon-more"></i>摘要</el-menu-item>
      <el-submenu index="7">
        <template slot="title">{{labels[mode]}}</template>
        <el-menu-item index="7-1">{{labels['edit']}}</el-menu-item>
        <el-menu-item index="7-2">{{labels['split']}}</el-menu-item>
        <el-menu-item index="7-3">{{labels['preview']}}</el-menu-item>
        <el-menu-item index="7-4">{{labels['full']}}</el-menu-item>
      </el-submenu>
      <el-menu-item index="8"><i class="el-icon-edit"></i>编辑TOC</el-menu-item>
    </el-menu>

    <el-dialog title="图片上传" v-model="isUploadShow" :modal="false">
      <el-upload
        action="//up.qbox.me/"
        drag
        :on-success="handleSuccess"
        :on-error="handleError"
        :before-upload="beforeUpload"
        :show-file-list="true"
        :data="form"
        >
        <i class="el-icon-upload"></i>
        <div class="el-dragger__text">将文件拖到此处,或<em>点击上传</em></div>
        <div class="el-upload__tip" slot="tip">请确保后台已经将七牛(server/conf/config.js)相关设置配置完毕</div>
      </el-upload>
    </el-dialog>

    <div class="md-editor" :class="{ 
        'edit': mode === 'edit',
        'preview': mode === 'preview',
        'split': mode === 'split',
        'toc': mode === 'toc'
    }">
      <textarea ref="markdown" :value="value" @input="handleInput" @keydown.tab="handleTab"></textarea>
      <div v-if="mode !== 'toc'" class="md-preview markdown" v-html="compiledMarkdown"></div>
      <textarea v-else ref="toc" :value="toc" class="md-preview markdown" @input="handleTocInput"></textarea>
    </div>
  </div>
</template>

<script>
import _ from 'lodash'
import { marked } from '../utils/marked'
import moment from 'moment'

export default {
  name: 'markdown',
  props: ['value', 'toc'],
  data() {
    return {
      labels: {
        'edit': '编辑模式',
        'split': '分屏模式',
        'preview': '预览模式',
        'full': '全屏模式',
        'toc': 'TOC模式'
      },
      mode: 'edit', // ['edit', 'split', 'preview', 'toc']
      isUploadShow: false,
      supportWebp: false,
      upToken: '',
      bucketHost: '',
      key: '',
      form: {}
    }
  },
  computed: {
    compiledMarkdown() {
      return marked(this.value.replace(/<!--more-->/g, ''))
    }
  },
  methods: {
    handleSelect(key, keyPath) {
      if (keyPath.length === 1) {
        switch (key) {
          case '1':
            this._boldText()
            break
          case '2':
            this._italicText()
            break
          case '3':
            this._blockquoteText()
            break
          case '4':
            this._codeText()
            break
          case '6':
            this._insertMore()
            break
          case '8':
            this.mode = 'toc'
            break
        }
      } else if (keyPath.length === 2) {
        switch (key) {
          case '5-1':
            this._uploadImage()
            break
          case '5-2':
            this._importImage()
            break
          case '7-1':
            this.mode = 'edit'
            break
          case '7-2':
            this.mode = 'split'
            break
          case '7-3':
            this.mode = 'preview'
            break
          case '7-4':
            this.mode = 'edit'
            break
        }
      }
    },
    handleTab: function(e) {
      this._preInputText('\t')
      e.preventDefault()
    },
    handleInput: _.debounce(function(e) {
      let value = e.target.value
      this.$emit('input', value)
    }, 300),
    handleTocInput: _.debounce(function(e) {
      let value = e.target.value
      this.$emit('change', value)
    }, 300),
    _preInputText(text, preStart, preEnd) {
      let textControl = this.$refs.markdown
      const start = textControl.selectionStart
      const end = textControl.selectionEnd
      const origin = this.value

      if (start !== end) {
        const exist = origin.slice(start, end)
        text = text.slice(0, preStart) + exist + text.slice(preEnd)
        preEnd = preStart + exist.length
      }
      let input = origin.slice(0, start) + text + origin.slice(end)

      this.$emit('input', input)
    },
    handlePreview(file) {},
    handleSuccess(response, file, fileList) {
      let key = response.key
      let prefix = this.supportWebp ? 'webp/' : ''
      const preUrl = `${this.bucketHost}/${encodeURI(key)}`
      const url = `${this.bucketHost}/${prefix}${encodeURI(key)}`
      this.$store.dispatch('GET_IMAGE_HEIGHT', {
        url: preUrl
      }).then(height => {
        const target = `<img height="${height}" src="${url}">`
        this.$confirm(target, '上传成功,是否插入图片链接?', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          closeOnClickModal: false
        }).then(() => {
          this.isUploadShow = false
          this._preInputText(target, 12, 12)
          this.$message({
            type: 'success',
            message: '已插入图片链接'
          })
        }).catch(() => {
          this.isUploadShow = false
          this.$message({
            type: 'info',
            message: '已取消插入图片链接'
          })
        })
      })
    },
    handleError(err, response, file) {
      if (err.status === 401) {
        this.$message.error('图片上传失败,请求中未附带Token')
      } else {
        this.$message.error(JSON.stringify(err))
      }
    },
    beforeUpload(file) {
      let curr = moment().format('YYYYMMDD').toString()
      let prefix = moment(file.lastModified).format('HHmmss').toString()
      let suffix = file.name
      let key = encodeURI(`${curr}/${prefix}_${suffix}`)
      return this.$store.dispatch('GET_IMAGE_TOKEN', {
        key
      }).then(response => {
        this.upToken = response.upToken
        this.key = response.key
        this.bucketHost = response.bucketHost
        this.supportWebp = response.supportWebp
        if (this.bucketHost === '') {
          this.$notify.error('获取七牛Token失败,请确认您已修改后台服务器的七牛配置文件(server/conf/config.js)')
          return Promise.reject()
        }
        this.form = {
          key,
          token: this.upToken
        }
      })
    },
    _uploadImage() {
      this.isUploadShow = true
    },
    _importImage() {
      this.$prompt('请输入图片的链接', '导入图片链接', {
        confirmButtonText: '确定',
        cancelButtonText: '取消'
      }).then(({ value }) => {
        this._preInputText(`![](${value})`, 12, 12)
        this.$message({
          type: 'success',
          message: '已插入图片链接'
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消插入图片链接'
        })
      })
    },
    _insertMore() {
      this._preInputText('<!--more-->', 12, 12)
    },
    _boldText() {
      this._preInputText('**加粗文字**', 2, 6)
    },
    _italicText() {
      this._preInputText('_斜体文字_', 1, 5)
    },
    _blockquoteText() {
      this._preInputText('> 引用', 3, 5)
    },
    _codeText() {
      this._preInputText('```\ncode block\n```', 5, 15)
    }
  }
}
</script>

<style lang="scss" scoped>
.md-editor textarea,
.md-preview {
    line-height: 1.5;
}

.el-dialog__wrapper {
  .el-dialog {
      width: 31%;
  }
}

.md-panel {
    display: block;
    position: relative;
    border: 1px solid #ccc;
    border-radius: 3px;
    font-size: 14px;
    overflow: hidden;

    .md-editor {
      width: 100%;
      height: auto;
      transition: width .3s;
      background-color: #fff;
      position: relative;

      textarea {
        box-sizing: border-box;
        display: block;
        border-style: none;
        resize: none;
        outline: 0;
        height: 100%;
        min-height: 500px;
        width: 100%;
        padding: 15px 15px 0
      }

      .md-preview {
        box-sizing: border-box;
        position: absolute;
        word-wrap: break-word;
        word-break: normal;
        width: 50%;
        height: 100%;
        left: 100%;
        top: 0;
        background-color: #F9FAFC;
        border-left: 1px solid #ccc;
        overflow: auto;
        transition: left .3s, width .3s;
        padding: 15px 15px 0;
      }
    }  

    .md-editor.edit {
      textarea {
        width: 100%;
      }
    }    
    
    .md-editor.split {
      textarea {
        width: 50%;
      }

      .md-preview {
        left: 50%;
        width: 50%;
      }
    }

    .md-editor.toc {
      textarea {
        width: 50%;
      }

      .md-preview {
        left: 50%;
        width: 50%;
      }
    }

    .md-editor.preview {
      textarea {
        width: 50%;
      }

      .md-preview {
        left: 0;
        width: 100%;
        border-left-style: none;
      }
    }
}

</style>


================================================
FILE: admin/src/components/containers/Post.vue
================================================
<template>
  <el-form ref="form" v-loading.body="isLoading" :model="form" label-width="80px">
    <el-row :gutter="0">
      <el-col :span="18">
        <el-form-item v-for="(item, index) in prevItems" :label="item.label">
          <el-input v-if="item.type === 'input'" :placeholder="item.description || ''" v-model="form[item.prop]"></el-input>
          <markdown v-if="item.type === 'markdown'" v-model="form[item.prop]" :toc="form[item.subProp]" @change="form[item.subProp] = arguments[0]"></markdown>
          <el-radio v-if="item.type === 'radio'" v-model="form[item.prop]" :label="item.label"></el-radio>
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label-width="20px">
          <el-button :class="{ 'margin-left': true }" type="info" @click.native="jump('prev')"><i class="el-icon-d-arrow-left"></i></el-button>
          <el-button type="info" @click.native="jump('next')"><i class="el-icon-d-arrow-right"></i></el-button>
        </el-form-item>
        <el-form-item label-width="20px">
          <el-button :class="{ 'margin-left': true }" type="info" @click.native="onSaveToc">生成目录 </el-button>
          <el-button type="success" @click.native="onSubmit">提交文章 </el-button>
        </el-form-item>
        <el-form-item v-for="(item, index) in nextItems" :label="item.label">

          <markdown v-if="item.type === 'markdown'" v-model="form[item.prop]"></markdown>

          <el-date-picker 
              v-if="item.type === 'date-picker'"
              type="datetime" v-model="form[item.prop]"
              placeholder="选择日期时间">
          </el-date-picker>

          <el-select v-if="item.type === 'select'" v-model="form[item.prop]" multiple>
            <el-option
              v-for="tag in tags"
              :label="tag"
              :value="tag">
            </el-option>            
          </el-select>

          <el-select v-if="item.type === 'radio'" v-model="form[item.prop]">
            <el-option
              v-for="cate in cates"
              :label="cate"
              :value="cate">
            </el-option>            
          </el-select>

          <el-switch v-if="item.type === 'switch'" v-model="form[item.prop]"></el-switch>

        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>

<script>
import Markdown from './Markdown'
import { marked, toc } from '../utils/marked'
import moment from 'moment'

export default {
  name: 'post',
  props: ['options'],
  components: {
    Markdown
  },
  data() {
    let isPost = this.options.name === 'post'
    let isPage = this.options.name === 'page'
    let id = typeof this.$route.params.id === 'undefined' ? -1 : this.$route.params.id
    let form = this.options.items.reduce((prev, curr) => {
      prev[curr.prop] = Array.isArray(curr.default)
                         ? curr.default.map(value => value)
                           : curr.default
      return prev
    }, {})
    form.type = this.options.name
    return {
      isPost,
      isPage,
      cates: [],
      tags: [],
      form,
      id,
      test: '',
      isLoading: id !== -1,
      markdownChecked: false
    }
  },
  computed: {
    splitIndex() {
      return this.options.items.reduce((prev, curr, index) => {
        if (curr.type === 'split') {
          return index
        }
        return prev
      }, -1)
    },
    prevItems() {
      return this.options.items.slice(0, this.splitIndex)
    },
    nextItems() {
      return this.options.items.slice(this.splitIndex)
    }
  },
  watch: {
    'form.markdownContent': {
      immediate: true,
      handler: function(val, oldVal) {
        if (!val || !this.form.updatedAt) return
        const url = this.$store.state.siteInfo.siteUrl.value
        const path = this.form.pathName
        const key = `${url}|${path}`
        const temp = this.getLS(key) || ''

        const realVal = temp.replace(/\|\d+$/gm, '')
        const matched = temp.match(/\d+$/gm)
        const hitoryTimestamp = parseInt(matched ? matched.slice(-1) : Date.now())
        const currentTimestamp = new Date(this.form.updatedAt).valueOf()
        if (temp !== '' && val !== realVal &&
              this.markdownChecked === false &&
              hitoryTimestamp >= currentTimestamp) {
          this.restore(key, realVal)
        } else if (this.markdownChecked === true) {
          const targetVal = val + `|${Date.now()}`
          this.saveLS(key, targetVal)
        }
        this.markdownChecked = true
      }
    }
  },
  methods: {
    jump(type) {
      if (this.id === -1) return

      let key = type === 'prev' ? '$lt' : '$gt'
      let query = Object.assign({}, this.options.query)
      query.conditions['_id'] = { [key]: this.form._id }
      query.limit = 1
      if (type === 'prev') {
        query.sort = 1
      }
      this.$store.dispatch('FETCH', {
        model: this.options.model,
        query
      }).then(item => {
        if (item.length !== 0) {
          let id = item[0]._id
          this.$router.push({ params: { id } })
          this.id = id
        }
      })
    },
    restore(key, val) {
      this.$confirm('', '发现草稿,是否恢复?', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        closeOnClickModal: false
      }).then(() => {
        this.form.markdownContent = val
        this.$message({
          type: 'success',
          message: '已恢复草稿'
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消恢复,提交文章后将清理草稿'
        })
      })
    },
    saveLS(key, value) {
      window.localStorage.setItem(key, value)
    },
    getLS(key) {
      return window.localStorage.getItem(key)
    },
    onSaveToc() {
      toc.length = 0
      marked(this.form.markdownContent)
      let tocMarkdown = this.tocToTree(toc)
      this.form.markdownToc = '**文章目录**\n' + tocMarkdown
    },
    tocToTree(toc) {
      return toc.map(item => {
        let times = (item.level - 1) * 2
        return `${' '.repeat(times)} - [${item.title}](#${item.slug})`
      }).join('\n')
    },
    validate() {
      this.form.summary = marked(this.form.markdownContent.split('<!--more-->')[0])
      this.form.content = marked(this.form.markdownContent.replace(/<!--more-->/g, ''))
      this.form.markdownToc = this.form.markdownToc || ''
      this.form.toc = marked(this.form.markdownToc)
      if (this.form.createdAt === '') {
        this.form.createdAt = moment().format('YYYY-MM-DD HH:mm:ss')
      } else {
        this.form.createdAt = moment(this.form.createdAt).format('YYYY-MM-DD HH:mm:ss')
      }
      this.form.updatedAt = moment().format('YYYY-MM-DD HH:mm:ss')
    },
    onSubmit() {
      this.validate()
      this.$store.dispatch('POST', {
        model: this.options.model,
        form: this.form
      }).then(response => {
        const url = this.$store.state.siteInfo.siteUrl.value
        const path = this.form.pathName
        const timestamp = new Date(this.form.updatedAt).valueOf()
        const key = `${url}|${path}`
        this.saveLS(key, this.form.markdownContent + `|${timestamp}`)
        this.$message({
          message: '文章已成功提交',
          type: 'success'
        })
        if (response._id && this.id === -1) {
          this.$router.replace({ params: { id: response._id } })
          this.form = response
          this.id = response._id
        }
      }).catch(err => console.error(err))
    },
    handleAddTag(tag) {
      this.form.tags.indexOf(tag) === -1 && this.form.tags.push(tag)
    },
    handleDelete(index) {
      this.form.tags.splice(index, 1)
    }
  },
  created() {
    if (this.id !== -1) {
      // fetch from post model
      this.$store.dispatch('FETCH_BY_ID', Object.assign({}, {
        id: this.id
      }, this.options)).then(post => {
        this.isLoading = false
        post.type = this.options.name
        this.form = post
      })
    }

    if (this.isPage) return

    let fetchCate = this.$store.dispatch('FETCH', {
      model: 'category',
      query: {}
    })

    let fetchTag = this.$store.dispatch('FETCH', {
      model: 'tag',
      query: {}
    })

    Promise.all([fetchCate, fetchTag]).then(([cates, tags]) => {
      this.cates = cates.map(value => value.name)
      this.tags = tags.map(value => value.name)
    }).catch(err => console.error(err))
  }
}
</script>

<style lang="scss" scoped>
  .margin-left {
    margin-left: 10px;
  }

  .el-select {
    margin-right: 5px;
  }

  .el-form {
    margin-top: 20px;
  }
</style>

================================================
FILE: admin/src/components/pages/Dashboard.vue
================================================
<template>
  <div class="dashboard">
    <el-row>
      <el-col :span="24">
        <div class="info">
          <h1>网站概要</h1>
          <div>
            <router-link :to="{ name: 'postCreate' }">
              <el-button type="success">撰写新文章</el-button>
            </router-link>
            <router-link :to="{ name: 'optionGeneral' }">
              <el-button style="margin-left: 10px" type="info">系统设置</el-button>
            </router-link>
          </div>
        </div>
        <el-slider style="margin-top: 20px" v-model="sliderValue"></el-slider>
      </el-col>
    </el-row>
    <el-row :gutter="10" style="margin-top:20px;">
      <el-col :span="10">
        <div class="recent">
          <el-tree 
              :default-expand-all='true'
              style="border-width: 0px" 
              :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
        </div>
      </el-col>
      <el-col :offset="4" :span="10">
        <div class="blog">
          <el-alert
            title="博客源码:"
            description="https://github.com/smallpath/blog"
            type="success"
            show-icon>
          </el-alert>
          <el-alert
            style="margin-top: 20px;"
            title="问题反馈:"
            description="https://github.com/smallpath/blog/issues"
            type="info"
            show-icon>
          </el-alert>
        </div>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: 'info',
  data() {
    return {
      sliderValue: 100,
      data: [],
      defaultProps: {
        children: 'children',
        label: 'title'
      },
      list: []
    }
  },
  methods: {
    handleNodeClick(data, node, tree) {
      if (data.title !== '最近发布的文章') {
        this.$router.push({
          name: data.type === 'post' ? 'postCreate' : 'pageCreate',
          params: { id: data._id }
        })
      }
    }
  },
  mounted() {
    this.$store.dispatch('FETCH', {
      model: 'post',
      query: {
        select: {
          title: 1,
          type: 1
        },
        sort: {
          createdAt: -1
        },
        limit: 10
      }
    }).then(list => {
      this.data = [{
        title: '最近发布的文章',
        children: list
      }]
    })
  }
}
</script>

<style lang="scss" scoped>
  .dashboard {
    padding: 20px; 

    .info {
      height: 100px;
    }
  }
</style>

================================================
FILE: admin/src/components/pages/Login.vue
================================================
<template>
  <div class="login">
    <el-row type="flex" class="row-bg" justify="center" align="middle">
      <el-col :span="6">
        <div class="grid-content bg-purple-light">
          <el-form label-position="right" ref="form" :model="form" label-width="40px">
            <p class="align-center" label-width="0">{{title}}</p>
            <el-form-item label="账号">
              <el-input auto-complete="on" v-model="form.name"></el-input>
            </el-form-item>
            <el-form-item label="密码">
              <el-input type="password" auto-complete="on" v-model="form.password"></el-input>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="onSubmit">登陆</el-button>
            </el-form-item>
          </el-form>
        </div>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import Api from '../../store/api'

export default {
  name: 'login',
  data() {
    return {
      title: '',
      form: {
        name: '',
        password: ''
      }
    }
  },
  methods: {
    onSubmit() {
      Api.login(this.form).then(response => {
        if (response.data.status === 'fail') {
          this.$message({
            message: '登陆失败,请检查帐号与密码',
            duration: 2000,
            type: 'error'
          })
        } else if (response.data.status === 'success') {
          window.localStorage.setItem('token', response.data.token)
          window.localStorage.setItem('username', this.form.name)
          this.$message({
            message: '登陆成功',
            type: 'success',
            duration: 2000
          })
          this.$store.dispatch('FETCH_USER', {
            model: 'user',
            query: {},
            username: this.form.name
          }).then(() => {
            this.$router.push({ path: '/dashboard' })
          })
        }
      }).catch(err => console.error(err))
    }
  },
  mounted() {
    this.$store.dispatch('FETCH_OPTIONS').then(() => {
      this.title = this.$store.state.siteInfo['title'].value || ''
    })
  }
}
</script>

<style lang="scss" scoped>
.login {
  height: 100%;

  .align-center {
    font-size: 30px;
    text-align: center;
  }

  .el-button--primary {
    width: 100%;
  }

  .row-bg {
    height: 100%;
  }
}
</style>


================================================
FILE: admin/src/components/pages/Logout.vue
================================================
<template>
</template>

<script>
import Api from '../../store/api'

export default {
  methods: {
    logout() {
      Api.logout().then(response => {
        window.localStorage.removeItem('token')
        window.localStorage.removeItem('username')
        this.$message({
          message: '登出成功',
          type: 'success',
          duration: 2000
        })
        this.$router.push({ path: '/admin/login' })
      }).catch(() => {})
    }
  },
  watch: {
    '$route': function(route) {
      if (route.name === 'logout') {
        this.logout()
      }
    }
  },
  created() {
    this.logout()
  }
}
</script>

================================================
FILE: admin/src/components/pages/Sidebar.vue
================================================
<template>
  <el-row class="sidebar">
    <el-col :span="4" class="full-height" style="overflow-y:auto;">
      <el-menu :router="true" :default-active="fullPath" 
                class="full-height" :unique-opened="true"
                @select="handleSelect">

        <el-menu-item index="/dashboard"><i class="el-icon-date"></i>概述</el-menu-item>

        <el-submenu index="2">
          <template slot="title"><i class="el-icon-document"></i>文章管理</template>
          <el-menu-item index="/post/list">文章列表</el-menu-item>
          <el-menu-item index="/post/create">添加文章</el-menu-item>
        </el-submenu>

        <el-submenu index="3">
          <template slot="title"><i class="el-icon-message"></i>页面管理</template>
          <el-menu-item index="/page/list">页面列表</el-menu-item>
          <el-menu-item index="/page/create">添加页面</el-menu-item>
        </el-submenu>

        <el-submenu index="4">
          <template slot="title"><i class="el-icon-menu"></i>主题管理</template>
          <el-menu-item index="/theme/list">主题列表</el-menu-item>
          <el-menu-item index="/theme/create">编辑主题</el-menu-item>
        </el-submenu>

        <el-submenu index="5">
          <template slot="title"><i class="el-icon-star-on"></i>分类管理</template>
          <el-menu-item index="/cate/list">分类列表</el-menu-item>
          <el-menu-item index="/cate/create">添加分类</el-menu-item>
        </el-submenu>
        
        <el-submenu index="6">
          <template slot="title"><i class="el-icon-star-off"></i>标签管理</template>
          <el-menu-item index="/tag/list">标签列表</el-menu-item>
          <el-menu-item index="/tag/create">添加标签</el-menu-item>
        </el-submenu>
        
        <el-menu-item index="/user/edit"><i class="el-icon-star-off"></i>用户设置</el-menu-item>
        
        <el-submenu index="8">
          <template slot="title"><i class="el-icon-setting"></i>系统设置</template>
          <el-menu-item index="/option/general">基本设置</el-menu-item>
          <el-menu-item index="/option/comment">评论设置</el-menu-item>
          <el-menu-item index="/option/analytic">统计代码</el-menu-item>
        </el-submenu>
        

      </el-menu>
    </el-col>
  </el-row>
</template>

<script>
export default {
  name: 'sidebar',
  data() {
    return {
      title: '',
      fullPath: ''
    }
  },
  watch: {
    '$route': function(route) {
      this.fullPath = route.path.split('/').slice(0, 3).join('/')
    }
  },
  methods: {
    push(name) {
      this.$router.push({ name })
    },
    handleSelect(index, indexPath) {

    }
  },
  beforeMount() {
    this.fullPath = this.$route.path.split('/').slice(0, 3).join('/')
  }
}
</script>

<style lang="scss" scoped>
.sidebar {
  height: 100%;
  box-sizing: border-box;
  padding-bottom: 60px;

  .row-bg {
    height: 100%;
  }
}

.full-height {
  height: 100%;
  width: 148px;
}
</style>


================================================
FILE: admin/src/components/pages/Top.vue
================================================
<template>
  <div class="top">
    <el-row class="tac">
      <el-col :span="24">
          
        <el-menu default-active="1" mode="horizontal" class="el-menu-vertical-demo" :unique-opened="true"
                 theme="dark" @select="handleSelect">
          <el-submenu index="1">
            <template slot="title">{{displayName === -1 ? '未登录' : displayName}}</template>
            <el-menu-item index="1-1">修改密码</el-menu-item>
            <el-menu-item index="1-2">退出</el-menu-item>
          </el-submenu>
          
        </el-menu>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: 'top',
  data() {
    return {
      title: ''
    }
  },
  computed: {
    displayName() {
      return this.$store.state.user.displayName || -1
    }
  },
  methods: {
    handleSelect(index, indexPath) {
      if (index === '1-1') {
        this.$router.push({
          name: 'userEdit'
        })
      } else if (index === '1-2') {
        this.$router.push({
          name: 'logout'
        })
      }
    }
  },
  created() {
    if (this.displayName === -1) {
      let username = window.localStorage.getItem('username')
      this.$store.dispatch('FETCH_USER', {
        model: 'user',
        query: {},
        username
      }).catch((err) => console.error(err))
    }
  }
}
</script>

<style lang="scss" scoped>
  .el-breadcrumb {
    position: relative;
    left: 20px;
    float: left;
    z-index: 1;
    line-height: 60px;
    font-size: 14px;
  }

  li.el-submenu {
    float: right;
  }

</style>


================================================
FILE: admin/src/components/utils/marked.js
================================================
import Marked from 'marked'
import hljs from 'highlight.js'

const renderer = new Marked.Renderer()
export const toc = []

renderer.heading = function(text, level) {
  var slug = text.toLowerCase().replace(/\s+/g, '-')
  toc.push({
    level: level,
    slug: slug,
    title: text
  })
  return `<h${level}><a href='#${slug}' id='${slug}' class='anchor'></a><a href='#${slug}'>${text}</a></h${level}>`
}

Marked.setOptions({
  highlight: function(code, lang) {
    if (hljs.getLanguage(lang)) {
      return hljs.highlight(lang, code).value
    } else {
      return hljs.highlightAuto(code).value
    }
  },
  renderer
})

export const marked = text => {
  var tok = Marked.lexer(text)
  text = Marked.parser(tok).replace(/<pre>/ig, '<pre class="hljs">')
  return text
}


================================================
FILE: admin/src/components/views/CreateEditView.js
================================================
import Item from '../containers/Create.vue'

export default function(options) {
  return {
    name: `${options.name}-create-view`,
    preFetch(store) {
      return store.dispatch('FETCH_LIST', options)
    },
    render(h) {
      return h(Item, { props: { options: options } })
    }
  }
}


================================================
FILE: admin/src/components/views/CreateListView.js
================================================
import Item from '../containers/List.vue'

export default function(options) {
  return {
    name: `${options.name}-list-view`,
    preFetch(store) {
      return store.dispatch('FETCH_LIST', options)
    },
    render(h) {
      return h(Item, { props: { options: options } })
    }
  }
}


================================================
FILE: admin/src/components/views/CreateMarkdownView.js
================================================
import Item from '../containers/Post.vue'

export default function(options) {
  return {
    name: `${options.name}-markdown-view`,
    preFetch(store) {
      return store.dispatch('FETCH_LIST', options)
    },
    render(h) {
      return h(Item, { props: { options: options } })
    }
  }
}


================================================
FILE: admin/src/main.js
================================================
/* eslint-disable */
import Vue from 'vue'

import router from './route/index'
import { sync } from 'vuex-router-sync'
import store from './store/index'

sync(store, router)

import 'element-ui/lib/theme-default/index.css'
import Element from 'element-ui'
Vue.use(Element)

import App from './App'

const app = new Vue({
  router,
  store,
  ...App
})

app.$mount('#app')

export { app, router, store }


================================================
FILE: admin/src/route/index.js
================================================
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)

import createListView from '../components/views/CreateListView'
import createEditView from '../components/views/CreateEditView'
import createMarkdownView from '../components/views/CreateMarkdownView'

import Main from '../components/Main'
import Dashboard from '../components/pages/Dashboard'
import Login from '../components/pages/Login'
import Logout from '../components/pages/Logout'

export default new VueRouter({
  mode: 'history',
  scrollBehavior: function(to, from, savedPosition) {
    return savedPosition || { x: 0, y: 0 }
  },
  routes: [
    {
      path: '/admin/login',
      name: 'login',
      components: {
        default: Login
      }
    },
    {
      path: '/admin/logout',
      name: 'logout',
      components: {
        default: Logout
      }
    },
    {
      path: '/dashboard',
      component: Main,
      children: [
        {
          path: '/',
          name: 'dashboard',
          component: Dashboard
        }
      ]
    },
    {
      path: '/post',
      name: 'post',
      component: Main,
      children: [
        {
          path: 'list',
          name: 'postList',
          component: createListView({
            name: 'post',
            model: 'post',
            isButtonFixed: true,
            items: [
              {
                prop: 'title',
                label: '标题',
                width: 300
              },
              {
                prop: 'pathName',
                label: '路径',
                width: 300
              }
            ],
            query: {
              conditions: {
                type: 'post'
              },
              select: {
                title: 1,
                pathName: 1,
                tags: 1,
                category: 1
              },
              sort: {
                createdAt: -1
              }
            }
          })
        },
        {
          path: 'create/:id?',
          name: 'postCreate',
          component: createMarkdownView({
            name: 'post',
            model: 'post',
            items: [
              {
                prop: 'title',
                label: '标题',
                type: 'input',
                default: '',
                width: 250
              },
              {
                prop: 'pathName',
                label: '路径',
                type: 'input',
                default: '',
                width: 250,
                description: '作为文章的唯一标志在前端路径中显示,例如test-article'
              },
              {
                prop: 'markdownContent',
                label: '内容',
                type: 'markdown',
                default: '',
                width: 170,
                subProp: 'markdownToc'
              },
              {
                type: 'split'
              },
              {
                prop: 'createdAt',
                label: '创建日期',
                type: 'date-picker',
                default: '',
                width: 170
              },
              {
                prop: 'updatedAt',
                label: '修改日期',
                type: 'date-picker',
                default: '',
                width: 170
              },
              {
                prop: 'tags',
                label: '标签',
                type: 'select',
                default: [],
                width: 170
              },
              {
                prop: 'category',
                label: '分类',
                type: 'radio',
                default: '',
                width: 170
              },
              {
                prop: 'isPublic',
                label: '是否公开',
                type: 'switch',
                default: true,
                width: 170
              },
              {
                prop: 'allowComment',
                label: '允许评论',
                type: 'switch',
                default: true,
                width: 170
              }
            ],
            query: {
              conditions: {
                type: 'post'
              },
              select: {
                title: 1,
                pathName: 1,
                tags: 1,
                category: 1,
                isPublic: 1,
                allowComment: 1,
                updatedAt: 1,
                createdAt: 1,
                markdownContent: 1,
                type: 1,
                markdownToc: 1
              }
            }
          })
        }
      ]
    },
    {
      path: '/page',
      name: 'page',
      component: Main,
      children: [
        {
          path: 'list',
          name: 'pageList',
          component: createListView({
            name: 'page',
            model: 'post',
            items: [
              {
                prop: 'title',
                label: '标题',
                width: 250
              },
              {
                prop: 'pathName',
                label: '路径',
                width: 170
              },
              {
                prop: 'createdAt',
                label: '创建日期',
                width: 170
              },
              {
                prop: 'updatedAt',
                label: '修改日期',
                width: 170
              }
            ],
            query: {
              conditions: {
                type: 'page'
              },
              select: {
                title: 1,
                pathName: 1,
                createdAt: 1,
                updatedAt: 1
              },
              sort: {
                createdAt: -1
              }
            }
          })
        },
        {
          path: 'create/:id?',
          name: 'pageCreate',
          component: createMarkdownView({
            name: 'page',
            model: 'post',
            items: [
              {
                prop: 'title',
                label: '标题',
                type: 'input',
                default: '',
                width: 250
              },
              {
                prop: 'pathName',
                label: '路径',
                type: 'input',
                default: '',
                width: 250,
                description: '作为文章的唯一标志在前端路径中显示,例如test-article'
              },
              {
                prop: 'markdownContent',
                label: '内容',
                type: 'markdown',
                default: '',
                width: 170,
                subProp: 'markdownToc'
              },
              {
                type: 'split'
              },
              {
                prop: 'createdAt',
                label: '创建日期',
                type: 'date-picker',
                default: '',
                width: 170
              },
              {
                prop: 'updatedAt',
                label: '修改日期',
                type: 'date-picker',
                default: '',
                width: 170
              },
              {
                prop: 'isPublic',
                label: '是否公开',
                type: 'switch',
                default: true,
                width: 170
              },
              {
                prop: 'allowComment',
                label: '允许评论',
                type: 'switch',
                default: true,
                width: 170
              }
            ],
            query: {
              conditions: {
                type: 'page'
              },
              select: {
                title: 1,
                pathName: 1,
                isPublic: 1,
                allowComment: 1,
                updatedAt: 1,
                createdAt: 1,
                markdownContent: 1,
                type: 1,
                markdownToc: 1
              }
            }
          })
        }
      ]
    },
    {
      path: '/theme',
      name: 'theme',
      component: Main,
      children: [
        {
          path: 'list',
          name: 'themeList',
          component: createListView({
            name: 'theme',
            model: 'theme',
            items: [
              {
                prop: 'name',
                label: '主题',
                width: 250
              },
              {
                prop: 'author',
                label: '作者',
                width: 170
              }
            ],
            query: {}
          })
        },
        {
          path: 'create/:id?',
          name: 'themeEdit',
          component: createEditView({
            name: 'theme',
            model: 'theme',
            items: [
              {
                prop: 'name',
                label: '主题名称',
                width: 250
              },
              {
                prop: 'author',
                label: '作者',
                width: 170
              },
              {
                prop: 'option',
                label: '配置',
                width: 170,
                type: 'textarea',
                sourceType: 'Object',
                description: '配置内容应为一个JSON对象,不符合JSON格式时提交将被忽略'
              }
            ],
            query: {}
          })
        }
      ]
    },
    {
      path: '/cate',
      name: 'cate',
      component: Main,
      children: [
        {
          path: 'list',
          name: 'cateList',
          component: createListView({
            name: 'cate',
            model: 'category',
            items: [
              {
                prop: 'name',
                label: '分类',
                width: 250
              }
            ],
            query: {}
          })
        },
        {
          path: 'create/:id?',
          name: 'cateCreate',
          component: createEditView({
            name: 'cate',
            model: 'category',
            items: [
              {
                prop: 'name',
                label: '分类名称',
                width: 250
              }
            ],
            query: {}
          })
        }
      ]
    },
    {
      path: '/tag',
      name: 'tag',
      component: Main,
      children: [
        {
          path: 'list',
          name: 'tagList',
          component: createListView({
            name: 'tag',
            model: 'tag',
            items: [
              {
                prop: 'name',
                label: '标签',
                width: 250
              }
            ],
            query: {}
          })
        },
        {
          path: 'create/:id?',
          name: 'tagCreate',
          component: createEditView({
            name: 'tag',
            model: 'tag',
            items: [
              {
                prop: 'name',
                label: '标签名称',
                width: 250
              }
            ],
            query: {}
          })
        }
      ]
    },
    {
      path: '/user',
      name: 'user',
      component: Main,
      children: [
        {
          path: 'edit',
          name: 'userEdit',
          component: createEditView({
            name: 'user',
            model: 'user',
            isPlain: true,
            items: [
              {
                prop: 'name',
                label: '账号',
                default: '',
                width: 250
              },
              {
                prop: 'password',
                label: '密码',
                default: '',
                width: 170
              },
              {
                prop: 'displayName',
                label: '昵称',
                default: '',
                width: 170,
                description: '在后台管理的顶部导航栏中显示'
              },
              {
                prop: 'email',
                label: '邮箱',
                default: '',
                width: 170,
                description: '在文章被回复时博客需要通知的目标邮箱,空则不通知'
              }
            ],
            query: {}
          })
        }
      ]
    },
    {
      path: '/option',
      name: 'option',
      component: Main,
      children: [
        {
          path: 'general',
          name: 'optionGeneral',
          component: createEditView({
            name: 'general',
            model: 'option',
            isPlain: true,
            items: [
              {
                prop: 'title',
                label: '网站名称',
                default: '',
                width: 250,
                description: '网站的名称,作为前后台的标题'
              },
              {
                prop: 'logoUrl',
                label: 'logo地址',
                default: '',
                width: 170,
                description: '前台单页的正方形图标,80x80'
              },
              {
                prop: 'description',
                label: '站点描述',
                default: '',
                width: 170,
                description: '作为前台的侧边栏描述'
              },
              {
                prop: 'siteUrl',
                label: '网站地址',
                default: '',
                width: 170,
                description: '博客前台的域名,建议加上http/https前缀'
              },
              {
                prop: 'faviconUrl',
                label: 'favicon地址',
                default: '',
                width: 170,
                description: '博客前台的favicon地址,请填写相对前台域名的根路径'
              },
              {
                prop: 'keywords',
                label: '关键词',
                default: '',
                width: 170,
                description: '作为前台单页的meta中的keywords,以供搜索引擎收录'
              },
              {
                prop: 'githubUrl',
                label: 'github地址',
                default: '',
                width: 170,
                description: 'github地址,填写用户昵称即可'
              },
              {
                prop: 'weiboUrl',
                label: '微博地址',
                default: '',
                width: 170,
                description: '微博地址,请填写全部链接,包括http/https前缀'
              },
              {
                prop: 'miitbeian',
                label: '网站备案号',
                default: '',
                width: 170,
                description: '网站的备案号,在前台单页的底部显示'
              }
            ],
            query: {}
          })
        },
        {
          path: 'comment',
          name: 'optionComment',
          component: createEditView({
            name: 'comment',
            model: 'option',
            isPlain: true,
            items: [
              {
                prop: 'commentType',
                label: '评论类型',
                default: '',
                width: 250,
                description: '目前仅支持disqus,计划支持duoshuo'
              },
              {
                prop: 'commentName',
                label: '评论名称',
                default: '',
                width: 250,
                description: 'disqus或duoshuo中分配给博客的id'
              }
            ],
            query: {}
          })
        },
        {
          path: 'analytic',
          name: 'optionAnalytic',
          component: createEditView({
            name: 'analytic',
            model: 'option',
            isPlain: true,
            items: [
              {
                prop: 'analyzeCode',
                label: '统计代码',
                default: '',
                width: 250,
                description: '目前仅支持谷歌统计,填入谷歌统计分配给博客的ID即可'
              }
            ],
            query: {}
          })
        }
      ]
    },
    {
      path: '/',
      redirect: '/admin/login'
    }
  ]
})


================================================
FILE: admin/src/store/api.js
================================================
import request from 'axios'

const root = `/proxyPrefix/api`

const store = {}

export default store

store.request = request

store.login = (conditions) => {
  return request.post(`/proxyPrefix/admin/login`, conditions)
}

store.logout = (conditions) => {
  return request.post(`/proxyPrefix/admin/logout`, conditions)
}

store.getImageHeight = url => {
  return request.get(`${url}?imageInfo`).then(response => response.data)
}

store.getImageToken = (body) => {
  return request.post(`/proxyPrefix/admin/qiniu`, body).then(response => response.data)
}

store.fetchList = (model, query) => {
  let target = `${root}/${model}`
  return request.get(target, { params: query }).then((response) => {
    return response.data
  })
}

store.fetchByID = (model, id, query) => {
  let target = `${root}/${model}/${id}`
  return request.get(target, { params: query }).then((response) => {
    return response.data
  })
}

store.post = (model, form) => {
  let target = `${root}/${model}`
  return request.post(target, form).then((response) => {
    return response.data
  })
}

store.patchByID = (model, id, form) => {
  let target = `${root}/${model}/${id}`
  return request.patch(target, form).then((response) => {
    return response.data
  })
}

store.deleteByID = (model, id) => {
  let target = `${root}/${model}/${id}`
  return request.delete(target).then((response) => {
    return response.data
  })
}


================================================
FILE: admin/src/store/index.js
================================================
import Vue from 'vue'
import Vuex from 'vuex'
import api from './api'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    siteInfo: {},
    list: [],
    curr: {},
    user: {}
  },

  actions: {

    GET_IMAGE_HEIGHT: ({ commit, state }, { url }) => {
      return api.getImageHeight(url).then(data => data.height || 100)
    },

    FETCH: ({ commit, state }, { model, query }) => {
      return api.fetchList(model, query)
    },

    FETCH_BY_ID: ({ commit, state }, { model, id, query }) => {
      return api.fetchByID(model, id, query)
    },

    FETCH_LIST: ({ commit, state }, { model, query }) => {
      return api.fetchList(model, query).then(obj => {
        commit('SET_LIST', { obj })
      })
    },

    FETCH_CREATE: ({ commit, state }, { model, id, query }) => {
      if (id === -1) {
        return api.fetchList(model, query).then(curr => {
          let obj = curr.reduce((prev, value) => {
            if (model === 'option') {
              prev[value.key] = value.value
            } else if (model === 'user') {
              Object.keys(value).forEach(item => {
                prev[item] = value[item]
              })
            }
            return prev
          }, {})
          commit('SET_CURR', { obj })
        })
      } else {
        return api.fetchByID(model, id, query).then(obj => {
          commit('SET_CURR', { obj })
        })
      }
    },

    POST: ({ commit, state }, { model, form }) => {
      if (model !== 'post' && model !== 'option' && model !== 'user') {
        return api.post(model, form)
      } else if (model === 'user' || model === 'post') {
        let { _id: id } = form
        if (typeof id !== 'undefined') {
          return api.patchByID(model, id, form).then((result) => {
            if (model === 'user') {
              commit('SET_USER', { user: result })
            }
            return result
          })
        } else {
          return api.post(model, form)
        }
      } else if (model === 'option') {
        let promiseArr = Object.keys(form).reduce((prev, curr) => {
          if (form[curr] !== state.curr[curr]) {
            const { _id: id } = state.siteInfo[curr]
            let promise = api.patchByID(model, id, {
              value: form[curr]
            })
            prev.push(promise)
          }
          return prev
        }, [])
        return Promise.all(promiseArr)
      }
    },

    GET_IMAGE_TOKEN: ({ commit, state }, body) => {
      return api.getImageToken(body)
    },

    PATCH: ({ commit, state }, { model, id, form }) => {
      return api.patchByID(model, id, form)
    },

    DELETE: ({ commit, state }, { model, id }) => {
      return api.deleteByID(model, id)
    },

    FETCH_USER: ({ commit, state }, { model, query, username }) => {
      return api.fetchList(model, query).then(result => {
        for (let i = 0, len = result.length; i < len; i++) {
          let user = result[i]
          if (user.name === username) {
            commit('SET_USER', { user })
            break
          }
        }
      })
    },

    FETCH_OPTIONS: ({ commit, state }) => {
      return api.fetchList('option', {}).then(optionArr => {
        let obj = optionArr.reduce((prev, curr) => {
          prev[curr.key] = curr
          return prev
        }, {})
        commit('SET_OPTIONS', { obj })
      })
    }

  },

  mutations: {
    SET_LIST: (state, { obj }) => {
      Vue.set(state, 'list', obj)
    },

    SET_OPTIONS: (state, { obj }) => {
      Vue.set(state, 'siteInfo', obj)
    },

    SET_CURR: (state, { obj }) => {
      Vue.set(state, 'curr', obj)
    },

    SET_USER: (state, { user }) => {
      Vue.set(state, 'user', user)
    }
  },

  getters: {

  }
})

export default store


================================================
FILE: admin/src/utils/error.js
================================================
export const getChineseDesc = (desc) => {
  switch (desc) {
    case 'Token not found':
      return '请求失败,请确认您已登陆'
    case 'Get token failed. Check the password':
      return '登陆失败,请检查您的密码'
    case 'Get token failed. Check the name':
      return '登陆失败,请检查您的账号'
    case 'Token verify failed':
    case 'Token invalid':
      return 'Token验证失败,请重新登录'
    default:
      return desc
  }
}


================================================
FILE: admin/static/.gitkeep
================================================


================================================
FILE: admin/test/e2e/custom-assertions/elementCount.js
================================================
// A custom Nightwatch assertion.
// the name of the method is the filename.
// can be used in tests like this:
//
//   browser.assert.elementCount(selector, count)
//
// for how to write custom assertions see
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
  this.message = 'Testing if element <' + selector + '> has count: ' + count;
  this.expected = count;
  this.pass = function (val) {
    return val === this.expected;
  }
  this.value = function (res) {
    return res.value;
  }
  this.command = function (cb) {
    var self = this;
    return this.api.execute(function (selector) {
      return document.querySelectorAll(selector).length;
    }, [selector], function (res) {
      cb.call(self, res);
    });
  }
}


================================================
FILE: admin/test/e2e/nightwatch.conf.js
================================================
require('babel-register')
var config = require('../../config')

// http://nightwatchjs.org/guide#settings-file
module.exports = {
  "src_folders": ["test/e2e/specs"],
  "output_folder": "test/e2e/reports",
  "custom_assertions_path": ["test/e2e/custom-assertions"],

  "selenium": {
    "start_process": true,
    "server_path": "node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar",
    "host": "127.0.0.1",
    "port": 4444,
    "cli_args": {
      "webdriver.chrome.driver": require('chromedriver').path
    }
  },

  "test_settings": {
    "default": {
      "selenium_port": 4444,
      "selenium_host": "localhost",
      "silent": true,
      "globals": {
        "devServerURL": "http://localhost:" + (process.env.PORT || config.dev.port)
      }
    },

    "chrome": {
      "desiredCapabilities": {
        "browserName": "chrome",
        "javascriptEnabled": true,
        "acceptSslCerts": true
      }
    },

    "firefox": {
      "desiredCapabilities": {
        "browserName": "firefox",
        "javascriptEnabled": true,
        "acceptSslCerts": true
      }
    }
  }
}


================================================
FILE: admin/test/e2e/runner.js
================================================
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing';
var server = require('../../build/dev-server.js');

// 2. run the nightwatch test suite against it
// to run in additional browsers:
//    1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
//    2. add it to the --env flag below
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2);
if (opts.indexOf('--config') === -1) {
  opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']);
}
if (opts.indexOf('--env') === -1) {
  opts = opts.concat(['--env', 'chrome']);
}

var spawn = require('cross-spawn');
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });

runner.on('exit', function (code) {
  server.close();
  process.exit(code);
});

runner.on('error', function (err) {
  server.close();
  throw err;
});


================================================
FILE: admin/test/e2e/specs/test.js
================================================
// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage

module.exports = {
  'default e2e tests': function test(browser) {
    // automatically uses dev Server port from /config.index.js
    // default: http://localhost:8080
    // see nightwatch.conf.js
    const devServer = browser.globals.devServerURL

    browser
      .url(devServer)
      .waitForElementVisible('#app', 5000)
      .assert.elementPresent('.hello')
      .assert.containsText('h1', 'Welcome to Your Vue.js App')
      .assert.elementCount('img', 1)
      .end()
  }
}


================================================
FILE: admin/test/unit/.eslintrc
================================================
{
  "env": {
    "mocha": true
  },
  "globals": {
    "expect": true,
    "sinon": true
  }
}


================================================
FILE: admin/test/unit/index.js
================================================
// Polyfill fn.bind() for PhantomJS
/* eslint-disable no-extend-native */
Function.prototype.bind = require('function-bind');

// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/);
testsContext.keys().forEach(testsContext);

// require all src files except main.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/);
srcContext.keys().forEach(srcContext);


================================================
FILE: admin/test/unit/karma.conf.js
================================================
// This is a karma config file. For more details see
//   http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
//   https://github.com/webpack/karma-webpack

var path = require('path');
var merge = require('webpack-merge');
var baseConfig = require('../../build/webpack.base.conf');
var utils = require('../../build/utils');
var webpack = require('webpack');
var projectRoot = path.resolve(__dirname, '../../');

var webpackConfig = merge(baseConfig, {
  // use inline sourcemap for karma-sourcemap-loader
  module: {
    loaders: utils.styleLoaders()
  },
  devtool: '#inline-source-map',
  vue: {
    loaders: {
      js: 'isparta'
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../../config/test.env')
    })
  ]
});

// no need for app entry during tests
delete webpackConfig.entry;

// make sure isparta loader is applied before eslint
webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || [];
webpackConfig.module.preLoaders.unshift({
  test: /\.js$/,
  loader: 'isparta',
  include: path.resolve(projectRoot, 'src'),
});

// only apply babel for test files when using isparta
webpackConfig.module.loaders.some(function (loader, i) {
  if (loader.loader === 'babel') {
    loader.include = path.resolve(projectRoot, 'test/unit');
    return true;
  }
});

module.exports = function (config) {
  config.set({
    // to run in additional browsers:
    // 1. install corresponding karma launcher
    //    http://karma-runner.github.io/0.13/config/browsers.html
    // 2. add it to the `browsers` array below.
    browsers: ['PhantomJS'],
    frameworks: ['mocha', 'sinon-chai'],
    reporters: ['spec', 'coverage'],
    files: ['./index.js'],
    preprocessors: {
      './index.js': ['webpack', 'sourcemap']
    },
    webpack: webpackConfig,
    webpackMiddleware: {
      noInfo: true,
    },
    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' },
      ]
    },
  });
};


================================================
FILE: admin/test/unit/specs/Hello.spec.js
================================================
import Vue from 'vue'
import Hello from 'src/components/Hello'

describe('Hello.vue', () => {
  it('should render correct contents', () => {
    const vm = new Vue({
      el: document.createElement('div'),
      render: (h) => h(Hello)
    })
    expect(vm.$el.querySelector('.hello h1').textContent)
      .to.equal('Welcome to Your Vue.js App')
  })
})


================================================
FILE: docs/.nojekyll
================================================


================================================
FILE: docs/README.md
================================================
# Blog
A blog system. Based on Vue2, Koa2, MongoDB and Redis

前后端分离 + 服务端渲染的博客系统, 前端 SPA + 后端 RESTful 服务器

# Demo
前端:[https://smallpath.me](https://smallpath.me)  
后台管理截图:[https://smallpath.me/post/blog-back-v2](https://smallpath.me/post/blog-back-v2)

Table of Contents
=================

* [TODO](#todo)
* [构建与部署](#构建与部署)
  * [前置](#前置)
  * [server](#server)
  * [front](#front)
  * [admin](#admin)
* [后端RESTful API](#后端restful-api)
  * [说明](#说明)
  * [HTTP动词](#http动词)
  * [权限验证](#权限验证)
      * [获得Token](#获得token)
      * [撤销Token](#撤销token)
      * [Token说明](#token说明)
  * [查询](#查询)
  * [新建](#新建)
  * [替换](#替换)
  * [更新](#更新)
  * [删除](#删除)

# TODO

> [TODO](https://github.com/smallpath/blog/issues/14)

# 构建与部署

## 前置

- Node v6
- pm2
- MongoDB
- Redis

## server

<details>
<summary>博客的提供RESTful API的后端</summary>

复制conf文件夹中的默认配置`config.tpl`, 并命名为`config.js`

有如下属性可以自行配置:

- `tokenSecret`
  - 改为任意字符串
- `defaultAdminPassword`
  - 默认密码, 必须修改, 否则服务器将拒绝启动

如果mongoDB或redis不在本机对应端口,可以修改对应的属性
- `mongoHost`
- `mongoDatabase`
- `mongoPort`
- `redisHost`
- `redisPort`

如果需要启用后台管理单页的七牛图片上传功能,请再修改如下属性:
- `qiniuAccessKey`
  - 七牛账号的公钥
- `qiniuSecretKey`
  - 七牛账号的私钥
- `qiniuBucketHost`
  - 七牛Bucket对应的外链域名
- `qiniuBucketName`
  - 七牛Bucket的名称
- `qiniuPipeline`
  - 七牛多媒体处理队列的名称

```
npm install
pm2 start entry.js
```

RESTful服务器在本机3000端口开启

</details>

## front

<details>
<summary>博客的前台单页, 支持服务端渲染</summary>

复制server文件夹中的默认配置`mongo.tpl`, 并命名为`mongo.js`

如果mongoDB不在本机对应端口,请自行配置`mongo.js`中的属性:

- `mongoHost`
- `mongoDatabase`
- `mongoPort`

```
npm install
npm run build
pm2 start production.js
```

请将`logo.png`与`favicon.ico`放至`static`目录中

再用nginx代理本机8080端口即可, 可以使用如下的模板

```
server{
    listen 80;                                      #如果是https, 则替换80为443
    server_name *.smallpath.me smallpath.me;        #替换域名
    root /alidata/www/Blog/front/dist;       #替换路径为构建出来的dist路径
    set $node_port 3000;
    set $ssr_port 8080;

    location ^~ / {
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$ssr_port;
        proxy_redirect off;
    }

    location ^~ /proxyPrefix/ {
        rewrite ^/proxyPrefix/(.*) /$1 break;
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$node_port;
        proxy_redirect off;
    }

    location ^~ /dist/ {
        rewrite ^/dist/(.*) /$1 break;
        etag         on;
        expires      max;
    }

    location ^~ /static/ {
        etag         on;
        expires      max;
    }
}
```

开发端口为本机8080

</details>

## admin

<details>
<summary>博客的后台管理单页</summary>

```
npm install
npm run build
```

用nginx代理构建出来的`dist`文件夹即可, 可以使用如下的模板

```
server{
    listen 80;                                     #如果是https, 则替换80为443
    server_name admin.smallpath.me;                #替换域名
    root /alidata/www/Blog/admin/dist;       #替换路径为构建出来的dist路径
    set $node_port 3000;

    index index.js index.html index.htm;

    location / {
        try_files $uri $uri/ @rewrites;
    }

    location @rewrites {
        rewrite ^(.*)$ / last;
    }

    location ^~ /proxyPrefix/ {
        rewrite ^/proxyPrefix/(.*) /$1 break;
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$node_port;
        proxy_redirect off;
    }

    location ^~ /static/ {
        etag         on;
        expires      max;
    }
}
```

开发端口为本机8082

</details>

# 后端 RESTful API

## 说明

后端服务器默认开启在 3000 端口, 如不愿意暴露 IP, 可以自行设置 nginx 代理, 或者直接使用前端两个单页的代理前缀`/proxyPrefix`

例如, demo的API根目录如下:

> https://smallpath.me/proxyPrefix/api/:modelName/:id

其中, `:modelName`为模型名, 总计如下6个模型

```
post
theme
tag
category
option
user
```

`:id`为指定的文档ID, 用以对指定文档进行 CRUD

## HTTP 动词

支持如下五种:

``` bash
GET     //查询
POST    //新建
PUT     //替换
PATCH   //更新部分属性
DELETE  //删除指定ID的文档
```

有如下两个规定:

- 对所有请求
  - header中必须将 `Content-Type` 设置为 `application/json` , 需要 `body` 的则 `body` 必须是合法 JSON格式
- 对所有回应
  - header中的`Content-Type`均为`application/json`, 且返回的数据也是 JSON格式

## 权限验证

服务器直接允许对`user`模型外的所有模型的GET请求

`user`表的所有请求, 以及其他表的非 GET 请求, 都必须将 header 中的`authorization`设置为服务器下发的 Token, 服务器验证通过后才会继续执行 CRUD 操作

### 获得 Token
> POST https://smallpath.me/proxyPrefix/admin/login

`body`格式如下:

```
{
	"name": "admin",
	"password": "testpassword"
}
```

成功, 则返回带有`token`字段的 JSON 数据
```
{
  "status": "success",
  "token": "tokenExample"
}
```

失败, 则返回如下格式的 JSON 数据:
```
{
  "status": "fail",
  "description": "Get token failed. Check name and password"
}
```

获取到`token`后, 在上述需要 token 验证的请求中, 请将 header 中的`authorization`设置为服务器下发的 Token, 否则请求将被服务器拒绝

### 撤销 Token
> POST https://smallpath.me/proxyPrefix/admin/logout

将`header`中的`authorization`设置为服务器下发的 token, 即可撤销此 token

### Token说明
Token 默认有效期为获得后的一小时, 超出时间后请重新请求 Token  
如需自定义有效期, 请修改服务端配置文件中的`tokenExpiresIn`字段, 其单位为秒

## 查询 

服务器直接允许对`user`模型外的所有模型的 GET 请求, 不需要验证 Token

为了直接通过 URI 来进行 mongoDB 查询, 后台提供六种关键字的查询:
```
conditions,
select,
count,
sort,
skip,
limit
```

### 条件(conditions)查询
类型为JSON, 被解析为对象后, 直接将其作为`mongoose.find`的查询条件

#### 查询所有文档
> GET https://smallpath.me/proxyPrefix/api/post

#### 查询title字段为'关于'的文档
> GET https://smallpath.me/proxyPrefix/api/post?conditions={"title":"关于"}

#### 查询指定id的文档的上一篇文档
> GET https://smallpath.me/proxyPrefix/api/post/?conditions={"_id":{"$lt":"580b3ff504f59b4cc27845f0"}}&sort=1&limit=1

#### select查询
类型为JSON, 用以拾取每条数据所需要的属性名, 以过滤输出来加快响应速度

#### 查询title字段为'关于'的文档的建立时间和更新时间
> GET https://smallpath.me/proxyPrefix/api/post?conditions={"title":"关于"}&select={"createdAt":1,"updatedAt":1}

### count查询
获得查询结果的数量

#### 查询文档的数量
> GET https://smallpath.me/proxyPrefix/api/post?conditions={"type":0}&count=1

### sort查询

#### 查询所有文档并按id倒序
> GET https://smallpath.me/proxyPrefix/api/post?sort={"_id":-1}

#### 查询所有文档并按更新时间倒序
> GET https://smallpath.me/proxyPrefix/api/post?sort={"updatedAt":-1}

### skip 查询和 limit 查询

#### 查询第2页的文档(每页10条)并按时间倒叙
> GET https://smallpath.me/proxyPrefix/api/post?limit=10&skip=10&sort=1

## 新建

需要验证Token

> POST https://smallpath.me/proxyPrefix/api/:modelName

Body中为用来新建文档的JSON数据

每个模型的具体字段, 可以查看该模型的[Schema定义](https://github.com/smallpath/blog/blob/master/server/model/mongo.js#L24)来获得

## 替换

需要验证Token

> PUT https://smallpath.me/proxyPrefix/api/:modelName/:id

`:id`为查询到的文档的`_id`属性, Body中为用来替换该文档的JSON数据

## 更新

需要验证Token

> PATCH https://smallpath.me/proxyPrefix/api/:modelName/:id

`:id`为查询到的文档的`_id`属性, Body中为用来更新该文档的JSON数据

更新操作请使用`PATCH`而不是`PUT`

## 删除

需要验证 Token

> DELETE https://smallpath.me/proxyPrefix/api/:modelName/:id

删除指定 ID 的文档


================================================
FILE: docs/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <meta name="description" content="Description">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
</head>
<body>
  <div id="app"></div>
</body>
<script>
  window.$docsify = {
    name: '',
    repo: ''
  }
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
</html>


================================================
FILE: front/.babelrc
================================================
{
  "presets": [
    ["latest", {
      "es2015": {
        "loose": true,
        "modules": false
      }
    }]
  ],
  "plugins": [
    "transform-object-rest-spread",
    "syntax-dynamic-import"
  ],
  "comments": true
}


================================================
FILE: front/.eslintignore
================================================
build/*.js
config/*.js


================================================
FILE: front/.eslintrc.js
================================================
module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module'
  },
  // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
  extends: 'standard',
  // required to lint *.vue files
  plugins: [
    'html'
  ],
  // add your custom rules here
  'rules': {
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0,
    'space-before-function-paren': ['error', 'never'],
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}


================================================
FILE: front/.gitignore
================================================
.DS_Store
node_modules/
dist/
npm-debug.log
selenium-debug.log
test/unit/coverage
test/e2e/reports
static/
server/mongo.js
.editorconfig


================================================
FILE: front/README.md
================================================
## front

> 博客的前台单页, 支持服务端渲染

复制server文件夹中的默认配置`mongo.tpl`, 并命名为`mongo.js`

``` bash
npm install
# 测试环境
npm run dev
# 默认打包文件在 dist 文件夹内
npm run build
```

命令顺利执行会得到类似以下回显:

```
[2017-04-07 22:09:55.971] [DEBUG] ssr axios - MongoDB is ready
[HPM] Proxy created: /proxyPrefix  ->  http://localhost:3000/
[HPM] Proxy rewrite rule created: "^/proxyPrefix" ~> ""
[2017-04-07 22:09:56.640] [DEBUG] ssr server - server started at localhost:8080
```
不出意外的话访问 http://localhost:8080/ 就可以访问博客页面了

## TIPS

- 如果mongoDB不在本机对应端口,请自行配置`mongo.js`中的属性:

- 请将`logo.png`与`favicon.ico`放至`static`目录中


- 生产环境 nginx 代理本机8080端口模板

```
server{
    listen 80;                                      #如果是https, 则替换80为443
    server_name *.smallpath.me smallpath.me;        #替换域名
    root /alidata/www/Blog/front/dist;       #替换路径为构建出来的dist路径
    set $node_port 3000;
    set $ssr_port 8080;

    location ^~ / {
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$ssr_port;
        proxy_redirect off;
    }

    location ^~ /proxyPrefix/ {
        rewrite ^/proxyPrefix/(.*) /$1 break;
        proxy_http_version 1.1;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$node_port;
        proxy_redirect off;
    }

    location ^~ /dist/ {
        rewrite ^/dist/(.*) /$1 break;
        etag         on;
        expires      max;
    }

    location ^~ /static/ {
        etag         on;
        expires      max;
    }
}
```

================================================
FILE: front/build/build-client.js
================================================
// https://github.com/shelljs/shelljs
require('shelljs/global')
env.NODE_ENV = 'production'

var path = require('path')
var config = require('../config')
var ora = require('ora')
var webpack = require('webpack')
var webpackConfig = require('./webpack.client.config')

var spinner = ora('building for production...')
spinner.start()

var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
var dist = path.join(__dirname, '../dist')

rm('-rf', dist)
mkdir('-p', dist)
rm('-rf', assetsPath)
mkdir('-p', assetsPath)
cp('-R', 'static/*', assetsPath)

webpack(webpackConfig, function (err, stats) {
  spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }) + '\n')
})


================================================
FILE: front/build/setup-dev-server.js
================================================
const path = require('path')
const webpack = require('webpack')
const MFS = require('memory-fs')
const proxyTable = require('../config/index').dev.proxyTable;
const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')
const proxyMiddleware = require('http-proxy-middleware')

module.exports = function setupDevServer (app, opts) {
  // modify client config to work with hot middleware
  clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
  clientConfig.output.filename = '[name].js'
  clientConfig.plugins.push(
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  )

  // dev middleware
  const clientCompiler = webpack(clientConfig)
  const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
    publicPath: clientConfig.output.publicPath,
    stats: {
      colors: true,
      chunks: false
    }
  })
  app.use(devMiddleware)
  clientCompiler.plugin('done', () => {
    const fs = devMiddleware.fileSystem
    const filePath = path.join(clientConfig.output.path, 'index.html')
    if (fs.existsSync(filePath)) {
      const index = fs.readFileSync(filePath, 'utf-8')
      opts.indexUpdated(index)
    }
  })

  // hot middleware
  app.use(require('webpack-hot-middleware')(clientCompiler))

  // proxy api requests
  Object.keys(proxyTable).forEach(function (context) {
    var options = proxyTable[context]
    if (typeof options === 'string') {
      options = { target: options }
    }
    app.use(proxyMiddleware(context, options))
  })

  // watch and update server renderer
  const serverCompiler = webpack(serverConfig)
  const mfs = new MFS()
  const outputPath = path.join(serverConfig.output.path, serverConfig.output.filename)
  serverCompiler.outputFileSystem = mfs
  serverCompiler.watch({}, (err, stats) => {
    if (err) throw err
    stats = stats.toJson()
    stats.errors.forEach(err => console.error(err))
    stats.warnings.forEach(err => console.warn(err))
    opts.bundleUpdated(mfs.readFileSync(outputPath, 'utf-8'))
  })
}


================================================
FILE: front/build/utils.js
================================================
var path = require('path')
var config = require('../config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')

exports.assetsPath = function (_path) {
  return path.posix.join(config.build.assetsSubDirectory, _path)
}

exports.cssLoaders = function (options) {
  options = options || {}
  // generate loader string to be used with extract text plugin
  function generateLoaders (loaders) {
    var sourceLoader = loaders.map(function (loader) {
      var extraParamChar
      if (/\?/.test(loader)) {
        loader = loader.replace(/\?/, '-loader?')
        extraParamChar = '&'
      } else {
        loader = loader + '-loader'
        extraParamChar = '?'
      }
      return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '')
    }).join('!')

    if (options.extract) {
      return ExtractTextPlugin.extract({ 
        fallback: 'vue-style-loader', 
        use: sourceLoader
      });
    } else {
      return ['vue-style-loader', sourceLoader].join('!')
    }
  }

  // http://vuejs.github.io/vue-loader/configurations/extract-css.html
  return {
    css: generateLoaders(['css']),
    postcss: generateLoaders(['css']),
    less: generateLoaders(['css', 'less']),
    sass: generateLoaders(['css', 'sass?indentedSyntax']),
    scss: generateLoaders(['css', 'sass']),
    stylus: generateLoaders(['css', 'stylus']),
    styl: generateLoaders(['css', 'stylus'])
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  var output = []
  var loaders = exports.cssLoaders(options)
  for (var extension in loaders) {
    var loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      loader: loader
    })
  }
  return output
}


================================================
FILE: front/build/vue-loader.config.js
================================================
module.exports = {
  preserveWhitespace: false,
  postcss: [
    require('autoprefixer')({
      browsers: ['last 3 versions']
    })
  ]
}


================================================
FILE: front/build/webpack.base.config.js
================================================
var config = require('../config')
var vueConfig = require('./vue-loader.config')

module.exports = {
  entry: {
    app: './src/client-entry.js'
  },
  output: {
    path: config.build.assetsRoot,
    publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name].[chunkhash].js'
  },
  resolve: {
    extensions: ['.js', '.vue']
  },
  module: {
    noParse: /es6-promise\.js$/, // avoid webpack shimming process
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueConfig
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: 'img/[name].[hash].[ext]'
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: 'fonts/[name].[hash].[ext]'
        }
      }
    ]
  },
  performance: {
    hints: process.env.NODE_ENV === 'production' ? 'warning' : false
  }
}


================================================
FILE: front/build/webpack.client.config.js
================================================
const webpack = require('webpack')
const base = require('./webpack.base.config')
const vueConfig = require('./vue-loader.config')
const utils = require('./utils')
const path = require('path')
const HTMLPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const SWPrecachePlugin = require('sw-precache-webpack-plugin')

const config = Object.assign({}, base, {
  resolve: {
    alias: Object.assign({}, base.resolve.alias, {
      'create-api': './create-api-client.js',
      'create-route': './create-route-client.js'
    }),
    extensions: base.resolve.extensions
  },
  plugins: (base.plugins || []).concat([
    // strip comments in Vue code
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"client"',
      'process.BROWSER': true
    }),
    // generate output HTML
    new HTMLPlugin({
      template: 'src/index.template.html'
    })
  ])
})

if (process.env.NODE_ENV === 'production') {
  // Use ExtractTextPlugin to extract CSS into a single file
  // so it's applied on initial render.
  vueConfig.loaders = utils.cssLoaders({
    extract: true
  })

  config.plugins.push(
    new ExtractTextPlugin('styles.css'),
    // this is needed in webpack 2 for minifying CSS
    new webpack.LoaderOptionsPlugin({
      minimize: true
    }),
    // minify JS
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      output: {
        comments: false
      }
    }),
    new SWPrecachePlugin({
      cacheId: 'blog',
      filename: 'service-worker.js',
      minify: true,
      mergeStaticsConfig: true,
      staticFileGlobs: [
        path.join(__dirname, '../dist/static/*.*')
      ],
      stripPrefixMulti: {
        [path.join(__dirname, '../dist/static')]: '/static'
      },
      // add hash to path to force updated
      dontCacheBustUrlsMatching: false,
      staticFileGlobsIgnorePatterns: [
        /index\.html$/,
        /\.map$/,
        /\.css$/,
        /\.svg$/,
        /\.eot$/
      ],
      // runtime caching for offline pwa
      runtimeCaching: [
        {
          // never cache service worker
          urlPattern: /service-worker.js/,
          handler: 'networkOnly'
        },
        {
          // note that this pattern will cache ajax request
          urlPattern: /(.+\/[^\.]*$)/,
          handler: 'networkFirst',
          options: {
            cache: {
              maxEntries: 30,
              name: 'blog-runtime-cache'
            }
          }
        },
        {
          urlPattern: /\.(png|jpg|webp|gif)/,
          handler: 'cacheFirst',
          options: {
            cache: {
              maxEntries: 20,
              name: 'blog-picture-cache'
            }
          }
        }
      ]
    })
  )
}

module.exports = config


================================================
FILE: front/build/webpack.server.config.js
================================================
const webpack = require('webpack')
const base = require('./webpack.base.config')
const VueSSRPlugin = require('vue-ssr-webpack-plugin')

const plugins = [
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
    'process.env.VUE_ENV': '"server"',
    'process.BROWSER': false
  })
]

if (process.env.NODE_ENV === 'production') {
  plugins.push(new VueSSRPlugin())
}

module.exports = Object.assign({}, base, {
  target: 'node',
  devtool: '#source-map',
  entry: './src/server-entry.js',
  output: Object.assign({}, base.output, {
    filename: 'server-bundle.js',
    libraryTarget: 'commonjs2'
  }),
  resolve: {
    alias: Object.assign({}, base.resolve.alias, {
      'create-api': './create-api-server.js',
      'create-route': './create-route-server.js'
    }),
    extensions: base.resolve.extensions
  },
  externals: Object.keys(require('../package.json').dependencies),
  plugins: plugins
})


================================================
FILE: front/config/dev.env.js
================================================
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})


================================================
FILE: front/config/index.js
================================================
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')

module.exports = {
  build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../dist/index.html'),
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/dist/',
    productionSourceMap: true,
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css']
  },
  dev: {
    env: require('./dev.env'),
    port: 8080,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/dist/',
    proxyTable: {
      '/proxyPrefix':{
        target:'http://localhost:3000/',
        changeOrigin: true,
        pathRewrite:{
          '^/proxyPrefix':'',
        }
      }
    },
    // CSS Sourcemaps off by default because relative paths are "buggy"
    // with this option, according to the CSS-Loader README
    // (https://github.com/webpack/css-loader#sourcemaps)
    // In our experience, they generally work as expected,
    // just be aware of this issue when enabling this option.
    cssSourceMap: false,
  }
}


================================================
FILE: front/config/prod.env.js
================================================
module.exports = {
  NODE_ENV: '"production"'
}


================================================
FILE: front/config/test.env.js
================================================
var merge = require('webpack-merge')
var devEnv = require('./dev.env')

module.exports = merge(devEnv, {
  NODE_ENV: '"testing"'
})


================================================
FILE: front/middleware/favicon.js
================================================
const fs = require('fs')
const serveFavicon = require('serve-favicon')
const log = require('log4js').getLogger('favicon')
const path = require('path')

module.exports = function(faviconPath, options) {
  let _middleware
  return function(req, res, next) {
    if (_middleware) return _middleware(req, res, next)
    const target = path.join(__dirname, `../${faviconPath}`)
    fs.readFile(target, function(err, buf) {
      if (err) {
        log.error(err)
        return res.status(404).end()
      }
      _middleware = serveFavicon(buf, options)
      return _middleware(req, res, next)
    })
  }
}


================================================
FILE: front/middleware/serverGoogleAnalytic.js
================================================
const log = require('log4js').getLogger('google analytic')

const config = require('../server/config')
const request = require('axios')
const EMPTY_GIF = new Buffer('R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAICTAEAOw==', 'base64')
const uuid = require('uuid')
const expires = 3600 * 1000 * 24 * 365 * 2
const ipReg = /\d+\.\d+\.\d+\.\d+/
const lowerReg = /\s+/g

const shouldBanSpider = ua => {
  if (!ua) {
    return true
  }

  ua = ua.toLowerCase().replace(lowerReg, '')
  return config.ga.spider.some(item => ua.indexOf(item) > -1)
}

const getClientIp = (req) => {
  let matched = req.ip.match(ipReg)
  return matched ? matched[0] : req.ip
}

module.exports = (req, res, next, query) => {
  let realQuery
  let clientId
  if (!query) {
    clientId = req.cookies.id
    if (!clientId) {
      clientId = uuid.v4()
      res.cookie('id', clientId, {
        expires: new Date(Date.now() + expires)
      })
    }
    res.setHeader('Content-Type', 'image/gif')
    res.setHeader('Content-Length', 43)
    res.end(EMPTY_GIF)
    realQuery = req.query
  } else {
    realQuery = query
    clientId = realQuery.cid
  }

  let passParams = config.ga.required.reduce((prev, curr) => {
    if (!realQuery[curr]) {
      prev = false
    }
    return prev
  }, true)

  if (passParams === false) return
  if (config.googleTrackID === '') return
  let userAgent = req.header('user-agent')
  if (shouldBanSpider(userAgent) === true) return

  let { z: timeStamp = Date.now() } = realQuery
  let form = Object.assign({}, realQuery, {
    v: config.ga.version,
    tid: config.googleTrackID,
    ds: 'web',
    z: timeStamp + clientId,
    cid: clientId,
    uip: getClientIp(req),
    ua: userAgent,
    t: 'pageview',
    an: config.title,
    dh: config.siteUrl
  })
  request.get(config.ga.api, {
    params: form
  }).then(response => {
    if (response.status !== 200) {
      log.error(response, form)
      return
    }
    log.info('Google Analytic sended', form.cid, form.ua)
  }).catch(err => log.error(err, form))
}


================================================
FILE: front/package.json
================================================
{
  "name": "vue_client_side",
  "version": "1.0.0",
  "description": "A Vue.js project",
  "author": "Smallpath <smallpath2013@gmail.com>",
  "private": true,
  "scripts": {
    "dev": "node server",
    "lint": "eslint --ext .js,.vue middleware server server.js src test/unit/specs test/e2e/specs",
    "start": "cross-env NODE_ENV=production node server",
    "build": "npm run build:client && npm run build:server",
    "build:client": "node build/build-client.js",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules"
  },
  "dependencies": {
    "axios": "^0.15.3",
    "es6-promise": "^4.0.5",
    "express": "^4.14.0",
    "lru-cache": "^4.0.1",
    "node-schedule": "^1.2.0",
    "vue": "2.3.0",
    "vue-meta": "0.5.3",
    "vue-router": "^2.0.3",
    "vue-server-renderer": "2.3.0",
    "vuex": "^2.0.0",
    "vuex-router-sync": "^3.0.0"
  },
  "devDependencies": {
    "autoprefixer": "^6.4.0",
    "babel-core": "^6.0.0",
    "babel-eslint": "^7.2.2",
    "babel-loader": "^6.0.0",
    "babel-plugin-dynamic-import-node": "^1.0.1",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "babel-plugin-transform-object-rest-spread": "^6.16.0",
    "babel-plugin-transform-runtime": "^6.0.0",
    "babel-preset-es2015": "^6.0.0",
    "babel-preset-latest": "^6.22.0",
    "babel-preset-stage-2": "^6.0.0",
    "bluebird": "^3.4.7",
    "chai": "^3.5.0",
    "connect-history-api-fallback": "^1.1.0",
    "cookie-parser": "^1.4.3",
    "cross-env": "^3.1.3",
    "cross-spawn": "^2.1.5",
    "css-loader": "^0.25.0",
    "eslint": "^2.10.2",
    "eslint-config-standard": "^5.1.0",
    "eslint-friendly-formatter": "^2.0.5",
    "eslint-loader": "^1.3.0",
    "eslint-plugin-html": "^1.3.0",
    "eslint-plugin-promise": "^1.0.8",
    "eslint-plugin-standard": "^1.3.2",
    "eventsource-polyfill": "^0.9.6",
    "express": "^4.13.3",
    "extract-text-webpack-plugin": "^2.0.0-beta.3",
    "file-loader": "^0.10.0",
    "function-bind": "^1.0.2",
    "html-webpack-plugin": "^2.8.1",
    "http-proxy-middleware": "^0.12.0",
    "inject-loader": "^2.0.1",
    "isparta-loader": "^2.0.0",
    "json-loader": "^0.5.4",
    "log4js": "^1.1.0",
    "lolex": "^1.4.0",
    "mongoose": "^4.8.5",
    "ora": "^0.3.0",
    "request": "^2.81.0",
    "rimraf": "^2.5.4",
    "serve-favicon": "^2.3.2",
    "shelljs": "^0.7.5",
    "sinon": "^1.17.3",
    "sinon-chai": "^2.8.0",
    "stylus": "^0.54.5",
    "stylus-loader": "^2.3.1",
    "sw-precache-webpack-plugin": "^0.9.0",
    "url-loader": "^0.5.7",
    "uuid": "^3.0.1",
    "vue-hot-reload-api": "^1.2.0",
    "vue-html-loader": "^1.0.0",
    "vue-loader": "^12.0.0",
    "vue-ssr-webpack-plugin": "^1.0.2",
    "vue-style-loader": "^3.0.0",
    "vue-template-compiler": "2.3.0",
    "webpack": "^2.4.0",
    "webpack-dev-middleware": "^1.6.1",
    "webpack-hot-middleware": "^2.12.2",
    "webpack-merge": "^0.8.3"
  },
  "engines": {
    "node": ">= 4.0.0",
    "npm": ">= 3.0.0"
  }
}


================================================
FILE: front/production.js
================================================
process.env.NODE_ENV = 'production'
require('./server.js')


================================================
FILE: front/server/config.js
================================================
const isProd = process.env.NODE_ENV === 'production'
const request = require('./server-axios')
const { ssrPort, serverPort, enableServerSideGA } = require('./mongo')

let siteUrl = 'http://localhost:8080'
let title = 'Blog'
let description = ''
let googleTrackID = ''
let favicon = isProd ? './dist' : '.'
let ga = {
  version: 1,
  api: 'https://www.google-analytics.com/collect',
  required: ['dt', 'dr', 'dp', 'z'],
  spider: [
    'spider',
    'bot',
    'appid', // for google appengine
    'go-http-client',
    'loadtest', // for load test
    'feed'
  ].map(item => item.toLowerCase().replace(/\s+/g, ''))
}

function flushOption() {
  return request.get('http://localhost:3000/api/option').then(res => {
    let options = res.data.reduce((prev, curr) => {
      prev[curr.key] = curr.value
      return prev
    }, {})
    siteUrl = options['siteUrl']
    title = options['title']
    description = options['description']
    googleTrackID = options['analyzeCode']
    favicon += options['faviconUrl']
  })
}

exports.ssrPort = ssrPort
exports.serverPort = serverPort
exports.enableServerSideGA = enableServerSideGA

Object.defineProperty(exports, 'title', {
  enumerable: true,
  get: function() {
    return title
  },
  set: function(value) {
    title = value
  }
})
Object.defineProperty(exports, 'siteUrl', {
  enumerable: true,
  get: function() {
    return siteUrl
  },
  set: function(value) {
    siteUrl = value
  }
})
Object.defineProperty(exports, 'description', {
  enumerable: true,
  get: function() {
    return description
  },
  set: function(value) {
    description = value
  }
})
Object.defineProperty(exports, 'googleTrackID', {
  enumerable: true,
  get: function() {
    return googleTrackID
  },
  set: function(value) {
    googleTrackID = value
  }
})
Object.defineProperty(exports, 'favicon', {
  enumerable: true,
  get: function() {
    return favicon
  },
  set: function(value) {
    favicon = value
  }
})
Object.defineProperty(exports, 'flushOption', {
  enumerable: true,
  get: function() {
    return flushOption
  }
})
Object.defineProperty(exports, 'ga', {
  enumerable: true,
  get: function() {
    return ga
  }
})


================================================
FILE: front/server/model.js
================================================
const config = require('./mongo.js')

const mongoose = require('mongoose')
const log = require('log4js').getLogger('ssr axios')

mongoose.Promise = global.Promise = require('bluebird')

const mongoUrl = `${config.mongoHost}:${config.mongoPort}/${config.mongoDatabase}`

mongoose.connect(mongoUrl)

const db = mongoose.connection

db.on('error', (err) => {
  log.error('connect error:', err)
})

db.once('open', () => {
  log.debug('MongoDB is ready')
})

const Schema = mongoose.Schema

let post = new Schema({
  type: { type: String, default: '' },
  status: { type: Number, default: 0 },
  title: String,
  pathName: { type: String, default: '' },
  summary: { type: String },
  markdownContent: { type: String },
  content: { type: String },
  markdownToc: { type: String, default: '' },
  toc: { type: String, default: '' },
  allowComment: { type: Boolean, default: true },
  createdAt: { type: String, default: '' },
  updatedAt: { type: String, default: '' },
  isPublic: { type: Boolean, default: true },
  commentNum: Number,
  options: Object,
  category: String,
  tags: Array
})

let category = new Schema({
  name: String,
  pathName: String
})

let tag = new Schema({
  name: String,
  pathName: String
})

let option = new Schema({
  key: String,
  value: Schema.Types.Mixed,
  desc: String
})

let theme = new Schema({
  name: String,
  author: String,
  option: Schema.Types.Mixed
})

post = mongoose.model('post', post)
category = mongoose.model('category', category)
option = mongoose.model('option', option)
theme = mongoose.model('theme', theme)
tag = mongoose.model('tag', tag)

module.exports = {
  post,
  category,
  option,
  tag,
  theme
}


================================================
FILE: front/server/mongo.tpl
================================================
const env = process.env

module.exports = {
  ssrPort: env.ssrPort || 8080,
  
  mongoHost: env.mongoHost || '127.0.0.1',
  mongoDatabase: env.mongoDatabase || 'blog',
  mongoPort: env.mongoPort || 27017,

  serverPort: env.serverPort || 3000,
  enableServerSideGA: env.enableServerSideGA || false,
  redisHost: env.redisHost || '127.0.0.1',
  redisPort: env.redisPort || 6379,
  redisPassword: env.redisPassword || ''
}


================================================
FILE: front/server/robots.js
================================================
module.exports = (config) =>
`User-agent: *
Allow: /
Sitemap: ${config.siteUrl}/sitemap.xml

User-agent: YisouSpider
Disallow: /
User-agent: EasouSpider
Disallow: /
User-agent: EtaoSpider
Disallow: /
User-agent: MJ12bot
Disallow: /`


================================================
FILE: front/server/rss.js
================================================
let getUpdatedDate = date => `    <lastBuildDate>${date}</lastBuildDate>\r\n`
let tail = `  </channel>
</rss>`

let api = 'http://localhost:3000/api/post'
let params = {
  conditions: {
    type: 'post',
    isPublic: true
  },
  select: {
    pathName: 1,
    updatedAt: 1,
    content: 1,
    title: 1
  },
  sort: {
    updatedAt: -1
  },
  limit: 10
}

let getRssBodyFromBody = (result, config) => {
  let head = `<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>${config.title}</title>
    <link>${config.siteUrl}</link>
    <description>${config.description}</description>
    <atom:link href="${config.siteUrl}/rss.xml" rel="self"/>
    <language>zh-cn</language>\r\n`
  let body = result.data.reduce((prev, curr) => {
    let date = new Date(curr.updatedAt).toUTCString()
    let content = curr.content.replace(/&/g, '&amp;')
                                      .replace(/</g, '&lt;')
                                      .replace(/>/g, '&gt;')
                                      .replace(/"/g, '&quot;')
                                      .replace(/'/g, '&apos;')
    prev += `    <item>\r\n`
    prev += `      <title>${curr.title}</title>\r\n`
    prev += `      <link>${config.siteUrl}/post/${curr.pathName}</link>\r\n`
    prev += `      <description>${content}</description>\r\n`
    prev += `      <pubDate>${date}</pubDate>\r\n`
    prev += `      <guid>${config.siteUrl}/post/${curr.pathName}</guid>\r\n`
    prev += `    </item>\r\n`
    return prev
  }, '')
  return head + getUpdatedDate(new Date().toUTCString()) + body + tail
}

module.exports = {
  api,
  params,
  getRssBodyFromBody
}


================================================
FILE: front/server/server-axios.js
================================================
const models = require('./model')
module.exports = process.__API__ = {
  get: function(target, options = {}) {
    const modelName = target.split('/').slice(-1)
    const query = options.params || {}
    const model = models[modelName]
    return new Promise((resolve, reject) => {
      queryModel(model, query).then(data => {
        resolve({ data })
      }) // mongoose uses mpromise who doesn't have a reject method here
    })
  }
}

function queryModel(model, query) {
  let conditions = {}
  let select = {}
  if (query.conditions) {
    conditions = query.conditions
  }
  let builder = model.find(conditions)
  if (query.select) {
    select = query.select
    builder = builder.select(select)
  }

  ['limit', 'skip', 'sort', 'count'].forEach(key => {
    if (query[key]) {
      let arg = query[key]
      if (key === 'limit' || key === 'skip') {
        arg = parseInt(arg)
      }
      if (key === 'sort' && typeof arg === 'string') {
        arg = JSON.parse(arg)
      }
      if (key !== 'count') builder[key](arg)
      else builder[key]()
    }
  })
  return builder.exec()
}


================================================
FILE: front/server/sitemap.js
================================================
let head = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\r\n`

let tail = '</urlset>'

let api = 'http://localhost:3000/api/post'
let params = {
  conditions: {
    type: 'post',
    isPublic: true
  },
  select: {
    pathName: 1,
    updatedAt: 1
  },
  sort: {
    updatedAt: -1
  }
}

let getSitemapFromBody = (result, config) => {
  let res = result.data
  let body = res.reduce((prev, curr) => {
    prev += `  <url>\r\n`
    prev += `    <loc>${config.siteUrl}/post/${curr.pathName}</loc>\r\n`
    prev += `    <lastmod>${curr.updatedAt.slice(0, 10)}</lastmod>\r\n`
    prev += `    <priority>0.6</priority>\r\n`
    prev += `  </url>\r\n`
    return prev
  }, '')
  return head + body + tail
}

module.exports = {
  api,
  params,
  getSitemapFromBody
}


================================================
FILE: front/server.js
================================================
const isProd = process.env.NODE_ENV === 'production'

const log = require('log4js').getLogger('ssr server')
const fs = require('fs')
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const express = require('express')
const schedule = require('node-schedule')
const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

const sendGoogleAnalytic = require('./middleware/serverGoogleAnalytic')
const favicon = require('./middleware/favicon')
const getRobotsFromConfig = require('./server/robots.js')
const { api: sitemapApi, params: sitemapParams, getSitemapFromBody } = require('./server/sitemap.js')
const { api: rssApi, params: rssParams, getRssBodyFromBody } = require('./server/rss.js')
const inline = isProd ? fs.readFileSync(resolve('./dist/styles.css'), 'utf-8') : ''
const config = require('./server/config')
const request = require('./server/server-axios')
const proxyRequest = require('request')

const uuid = require('uuid')
const titleReg = /<.*?>(.+?)<.*?>/
const expires = 3600 * 1000 * 24 * 365 * 2

const chunkObj = {}
if (isProd) {
  const fileArr = fs.readdirSync(resolve('./dist'))
  for (let i = 0, len = fileArr.length; i < len; i++) {
    const fileName = fileArr[i]
    const arr = fileName.split('.')
    if (arr.length === 3 && arr[0] !== 'app') {
      const input = fs.readFileSync(resolve(`./dist/${fileName}`), 'utf-8')
      chunkObj[fileName] = input
    }
  }
}

let sitemap = ''
let rss = ''
let robots = ''

config.flushOption().then(() => {
  robots = getRobotsFromConfig(config)

  const flushSitemap = () => request.get(sitemapApi, sitemapParams).then(result => {
    sitemap = getSitemapFromBody(result, config)
  })

  const flushRss = () => request.get(rssApi, rssParams).then(result => {
    rss = getRssBodyFromBody(result, config)
  })

  flushSitemap()
  flushRss()
  schedule.scheduleJob('30 3 * * * ', function() {
    flushRss()
    flushSitemap()
  })

  let app = express()
  app.enable('trust proxy')
  let renderer
  let html // generated by html-webpack-plugin
  if (isProd) {
    renderer = createRenderer(require('./dist/vue-ssr-bundle.json'))
    html = flushHtml(fs.readFileSync(resolve('./dist/index.html'), 'utf-8'))
  } else {
    // in development: setup the dev server with watch and hot-reload,
    // and update renderer / index HTML on file change.
    require('./build/setup-dev-server')(app, {
      bundleUpdated: bundle => {
        renderer = createRenderer(bundle)
      },
      indexUpdated: index => {
        html = flushHtml(index)
      }
    })
  }

  function flushHtml(template) {
    const style = isProd ? `<style type="text/css">${inline}</style>` : ''
    const i = template.indexOf('<div id=app></div>')
    return {
      head: template.slice(0, i).replace('<link href="/dist/styles.css" rel="stylesheet">', style),
      tail: template.slice(i + '<div id=app></div>'.length),
      origin: template
    }
  }

  function createRenderer(bundle) {
    return createBundleRenderer(bundle, {
      cache: require('lru-cache')({
        max: 1000,
        maxAge: 1000 * 60 * 15
      }),
      runInNewContext: false
    })
  }

  app.use(require('cookie-parser')())
  app.get('/favicon.ico', favicon(config.favicon))

  const prefix = '/proxyPrefix/'
  app.use((req, res, next) => {
    const url = decodeURIComponent(req.url)
    log.debug(`${req.method} ${url}`)
    if (!isProd) return next()
    // proxy with node in production
    if (url.startsWith(prefix)) {
      const rewriteUrl = `http://localhost:${config.serverPort}/${url.replace(prefix, '')}`
      proxyRequest.get(rewriteUrl).on('error', (err) => {
        res.end(err)
        log.error(err)
      }).pipe(res)
    } else {
      return next()
    }
  })

  const serve = (path, cache) => express.static(resolve(path), {
    maxAge: cache && isProd ? 60 * 60 * 24 * 30 : 0,
    fallthrough: false
  })
  app.use('/service-worker.js', serve('./dist/service-worker.js'))
  app.use('/dist', serve('./dist'))
  app.use('/static', serve('./static'))
  app.get('/_.gif', (req, res, next) => sendGoogleAnalytic(req, res, next))
  app.get('/robots.txt', (req, res, next) => res.end(robots))
  app.get('/rss.xml', (req, res, next) => {
    res.header('Content-Type', 'application/xml')
    return res.end(rss)
  })
  app.get('/sitemap.xml', (req, res, next) => {
    res.header('Content-Type', 'application/xml')
    return res.end(sitemap)
  })

  app.get('*', (req, res, next) => {
    if (!renderer) {
      return res.end('waiting for compilation... refresh in a moment.')
    }

    const supportWebp = req.header('accept').includes('image/webp')

    let s = Date.now()
    const context = {
      url: req.url,
      supportWebp
    }
    const renderStream = renderer.renderToStream(context)

    res.header('Content-Type', 'text/html; charset=utf-8')

    renderStream.once('data', () => {
      const { title, link, meta } = context.meta.inject()
      const titleText = title.text()
      const metaData = `${titleText}${meta.text()}${link.text()}`
      const matched = titleText.match(titleReg)
      let clientId = req.cookies.id
      if (!clientId) {
        clientId = uuid.v4()
        res.cookie('id', clientId, {
          expires: new Date(Date.now() + expires)
        })
      }
      const chunk = html.head.replace('<title></title>', metaData)
      res.write(chunk)
      sendGoogleAnalytic(req, res, next, {
        dt: matched ? matched[1] : config.title,
        dr: req.url,
        dp: req.url,
        z: +Date.now(),
        cid: clientId
      })
    })

    renderStream.on('data', chunk => {
      res.write(chunk)
    })

    renderStream.on('end', () => {
      if (context.initialState) {
        context.initialState.supportWebp = supportWebp
        context.initialState.route = Object.assign({}, context.initialState.route, {
          matched: {}
        })
        res.write(
          `<script>window.__INITIAL_STATE__=${
          JSON.stringify(context.initialState)
          }</script>`
        )
      }
      let tail = html.tail
      if (isProd && typeof context.chunkName === 'string') {
        for (let key in chunkObj) {
          if (key.split('.')[0] === context.chunkName) {
            const chunk = `<script type="text/javascript" charset="utf-8">${chunkObj[key]}</script></body>`
            tail = tail.replace('</body>', chunk)
            break
          }
        }
      }
      res.end(tail)
      log.debug(`whole request: ${Date.now() - s}ms`)
    })

    renderStream.on('error', err => {
      res.end(html.origin)
      log.error(err)
    })
  })

  const port = config.ssrPort
  app.listen(port, () => {
    log.debug(`server started at localhost:${port}`)
  })
}).catch(err => log.error(err))



================================================
FILE: front/src/assets/css/article.css
================================================
#toc {
  float: right;
  border: 1px solid #e2e2e2;
  font-size: 14px;
  margin: 0 0 15px 20px;
  max-width: 260px;
  min-width: 120px;
  padding: 6px;
  background-color: #fff;
}

#toc p {
  margin: 1px;
}

#toc ul {
  margin: 0 .5em 0 1.5em;
}

#toc strong {
  border-bottom: 1px solid #e2e2e2;
  display: block;
}

a.anchor {
  display: block;
  position: relative;
  visibility: hidden;
}

article img {
  height: auto;
  max-width: 910px;
}

article {
  border-bottom: 1px solid #ddd;
  border-top: 1px solid #fff;
  padding: 30px 0;
  position: relative;
  word-wrap: break-word
}

article .meta {
  color: #555;
  float: right;
  font-size: .9em;
  line-height: 2;
  position: relative;
  text-align: right;
  width: auto
}

article .meta a {
  color: #999
}

article h1.title {
  font-size: 2em;
  font-weight: 300;
  line-height: 35px;
  margin-bottom: 25px
}

article h1.title,
article h1.title a {
  color: #333
}

article .meta .date,
article .meta .comment,
article .meta .tags {
  position: relative
}

article h1.title a:hover {
  color: #2479CC;
  transition: color .3s
}

article button,
article input.runcode {
  -webkit-appearance: button;
  background: #12b0e6;
  border: none;
  box-shadow: inset 0 -5px 20px rgba(0, 0, 0, .1);
  color: #fff;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  margin-top: 10px;
  padding: 0.625em .5em
}

article strong {
  font-weight: 700
}

article em {
  font-style: italic
}

article blockquote {
  background-color: #f8f8f8;
  border-left: 5px solid #2479CC;
  margin-top: 10px;
  margin-left: 0;
  padding: 15px 20px
}

article code {
  background-color: #f2f2f2;
  border-radius: 5px;
  font-family: "Consolas", "Courier New", Courier, mono, serif;
  font-size: 80%;
  margin: 0 2px;
  padding: 4px 5px;
  vertical-align: middle
}

article pre {
  color: #5d6a6a;
  font-family: "Consolas", "Liberation Mono", Courier, monospace;
  font-size: 14px;
  line-height: 1.6;
  overflow: hidden;
  padding: 0.6em;
  position: relative;
  white-space: pre-wrap;
  word-break: break-all;
  word-wrap: break-word
}

article img {
  border: 1px solid #ccc;
  display: block;
  margin: 10px 0 5px;
  max-width: 100%;
  padding: 0
}

article table {
  border: 0;
  border-collapse: collapse;
  border-spacing: 0
}

article blockquote p {
  margin-bottom: 0
}

article pre code {
  background-color: transparent;
  border-radius: 0 0 0 0;
  border: 0;
  display: block;
  font-size: 100%;
  margin: 0;
  padding: 0;
  position: relative
}

article table th,
article table td {
  border: 0
}

article table th {
  border-bottom: 2px solid #848484;
  padding: 6px 20px;
  text-align: left
}

article table td {
  border-bottom: 1px solid #d0d0d0;
  padding: 6px 20px
}

article .expire-tips {
  background-color: #ffffc0;
  border: 1px solid #e2e2e2;
  border-left: 5px solid #fff000;
  color: #333;
  font-size: 15px;
  padding: 5px 10px
}

article .aliyun-tips {
  background-color: #f0f8f4;
  border: 1px solid #e2e2e2;
  border-left: 5px solid #7cc4a0;
  font-size: 15px;
  padding: 5px 10px
}

article .post-info {
  font-size: 14px
}

article .entry-content {
  font-size: 16px;
  line-height: 1.8;
  word-wrap: break-word
}

article .entry-content p,
article .entry-content blockquote,
article .entry-content ul,
article .entry-content ol,
article .entry-content dl,
article .entry-content table,
article .entry-content iframe,
article .entry-content h1,
article .entry-content h2,
article .entry-content h3,
article .entry-content h4,
article .entry-content h5,
article .entry-content h6,
article .entry-content p,
article .entry-content pre {
  margin-top: 15px
}

article pre b.name {
  color: #eee;
  font-size: 60px;
  line-height: 1;
  pointer-events: none;
  position: absolute;
  right: 10px;
  top: 10px
}

article .entry-content .date {
  color: #999;
  font-size: 14px;
  font-style: italic
}

article input.runcode:hover,
article input.runcode:focus,
article input.runcode:active {
    background: #f6ad08
}

article .entry-content ul ul,
article .entry-content ul ol,
article .entry-content ul dl,
article .entry-content ol ul,
article .entry-content ol ol,
article .entry-content ol dl,
article .entry-content dl ul,
article .entry-content dl ol,
article .entry-content dl dl,
article .entry-content blockquote > p:first-of-type {
  margin-top: 0
}

article .entry-content ul,
article .entry-content ol,
article .entry-content dl {
  margin-left: 25px
}

article.tags section a {
  border: 1px solid rgba(36, 121, 204, .8);
  border-radius: 4px;
  color: rgba(36, 121, 204, .8);
  display: inline-block;
  font-size: 14px;
  height: 40px;
  line-height: 40px;
  margin: 0 15px 10px 0;
  padding: 0 15px;
  text-decoration: none;
  -webkit-transition: color .2s cubic-bezier(.4, .01, .165, .99), border .2s cubic-bezier(.4, .01, .165, .99);
  transition: color .2s cubic-bezier(.4, .01, .165, .99), border .2s cubic-bezier(.4, .01, .165, .99)
}

article.tags section a:hover {
  border-color: #2479CC;
  color: #2479CC
}

================================================
FILE: front/src/assets/css/base.css
================================================
body {
  color: #666;
  font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", STHeiti, "Microsoft YaHei";
  -webkit-font-smoothing: antialiased;
  margin: 0;
  -webkit-overflow-scrolling: touch;
  padding: 0;
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: none;
  -webkit-transition: -webkit-transform .2s cubic-bezier(.4, .01, .165, .99);
  transition: transform .2s cubic-bezier(.4, .01, .165, .99), -webkit-transform .2s cubic-bezier(.4, .01, .165, .99)
}

body,
html {
  height: 100%;
  width: 100%
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-weight: 400;
  margin: 0;
  padding: 0
}

a,
a:hover {
  color: #2479CC;
  text-decoration: none
}

* {
  -webkit-box-sizing: border-box;
  box-sizing: border-box
}

ul,
ol {
  padding: 0
}

body.side {
  position: fixed;
  -webkit-transform: translate3D(250px, 0, 0);
  -ms-transform: translate3D(250px, 0, 0);
  transform: translate3D(250px, 0, 0)
}

h1.intro {
  background-color: #f6f9fa;
  color: #999;
  padding: 20px 30px;
  text-align: center
}

#main {
  background-color: #fff;
  max-width: 1390px;
  -webkit-overflow-scrolling: touch;
  padding-left: 290px;
  padding-right: 40px
}

================================================
FILE: front/src/assets/css/footer.css
================================================
#footer {
  border-top: 1px solid #fff;
  font-size: .9em;
  line-height: 1.8;
  padding: 15px;
  text-align: center
}

#footer .beian {
  color: #666
}

================================================
FILE: front/src/assets/css/header.css
================================================
#header {
  display: none;
  background-color: #323436;
  height: 50px;
  left: 0;
  line-height: 50px;
  overflow: hidden;
  position: fixed;
  top: 0;
  width: 100%;
  z-index: 9
}

#header h1 {
  font-size: 16px;
  text-align: center
}

#header h1 a {
  color: #999
}

#header .btn-bar {
  height: 50px;
  left: 0;
  position: absolute;
  top: 0;
  width: 50px
}

body.side #header .btn-bar i {
  opacity: 0
}

body.side #header .btn-bar:before {
  top: 25px;
  -webkit-transform: rotate(-45deg);
  -ms-transform: rotate(-45deg);
  transform: rotate(-45deg);
  width: 24px
}

body.side #header .btn-bar:after {
  bottom: 24px;
  -webkit-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  transform: rotate(45deg);
  width: 24px
}

#header .btn-bar i,
#header .btn-bar:after,
#header .btn-bar:before {
  background-color: #fff;
  height: 1px;
  left: 14px;
  position: absolute;
  -webkit-transition: all .2s cubic-bezier(.4, .01, .165, .99) .3s;
  transition: all .2s cubic-bezier(.4, .01, .165, .99) .3s;
  width: 22px
}

#header .btn-bar i {
  opacity: 1;
  top: 25px
}

#header .btn-bar:before {
  content: '';
  top: 17px
}

#header .btn-bar:after {
  bottom: 16px;
  content: ''
}

#header a.me,
#header a.me img {
  border-radius: 30px;
  height: 30px;
  overflow: hidden;
  width: 30px
}

#header a.me {
  position: absolute;
  right: 10px;
  top: 10px
}

================================================
FILE: front/src/assets/css/highlight.css
================================================
/*

Atom One Dark by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax

base:    #282c34
mono-1:  #abb2bf
mono-2:  #818896
mono-3:  #5c6370
hue-1:   #56b6c2
hue-2:   #61aeee
hue-3:   #c678dd
hue-4:   #98c379
hue-5:   #e06c75
hue-5-2: #be5046
hue-6:   #d19a66
hue-6-2: #e6c07b

*/

.hljs {
  display: block;
  overflow-x: auto;
  padding: 0.5em;
  color: #abb2bf;
  background: #282c34;
  /* modified by smallpath */
  border-left: 5px solid #61aeee;
}

.hljs-comment {
  color: #61aeee;
}

.hljs-quote {
  color: #5c6370;
  font-style: italic;
}

.hljs-doctag,
.hljs-keyword,
.hljs-formula {
  color: #c678dd;
}

.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
  color: #e06c75;
}

.hljs-literal {
  color: #56b6c2;
}

.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
  color: #98c379;
}

.hljs-built_in,
.hljs-class .hljs-title {
  color: #e6c07b;
}

.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
  color: #d19a66;
}

.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
  color: #61aeee;
}

.hljs-emphasis {
  font-style: italic;
}

.hljs-strong {
  font-weight: bold;
}

.hljs-link {
  text-decoration: underline;
}

================================================
FILE: front/src/assets/css/icon.css
================================================
@font-face {
  font-family: iconfont;
  src: url(../font/iconfont.eot);
  src: url(../font/iconfont.eot?#iefix) format("embedded-opentype"), url(../font/iconfont.ttf) format("truetype"), url(../font/iconfont.svg#iconfont) format("svg");
}

.iconfont {
  font-family: iconfont!important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-stroke-width: .2px
}

.icon-weibo:before {
  content: "\e600"
}

.icon-archive:before {
  content: "\e601"
}

.icon-user:before {
  content: "\e602"
}

.icon-rss-v:before {
  content: "\e603"
}

.icon-tags:before {
  content: "\e604"
}

.icon-home:before {
  content: "\e605"
}

.icon-search:before {
  content: "\e606"
}

.icon-googleplus:before {
  content: "\e607"
}

.icon-weixin:before {
  content: "\e608"
}

.icon-mail:before {
  content: "\e609"
}

.icon-twitter-v:before {
  content: "\e60a"
}

.icon-linkedin:before {
  content: "\e60b"
}

.icon-stackoverflow:before {
  content: "\e60c"
}

.icon-github-v:before {
  content: "\e60d"
}

.icon-facebook:before {
  content: "\e60e"
}

.icon-right:before {
  content: "\e60f"
}

.icon-left:before {
  content: "\e610"
}

.icon-link:before {
  content: "\e611"
}

.icon-https:before {
  content: "\e612"
}

================================================
FILE: front/src/assets/css/pagination.css
================================================
.pagination {
  border-top: 1px solid #fff;
  border-bottom: 1px solid #ddd;
  line-height: 20px;
  overflow: hidden;
  padding: 20px 0;
  position: relative;
  width: 100%
}

.pagination .prev {
  float: left
}

.pagination .next {
  float: right
}

.pagination .center {
  margin: auto;
  text-align: center;
  width: 80px
}

================================================
FILE: front/src/assets/css/responsive.css
================================================
@media screen and (max-width:768px) {
  a.anchor {
    top: -50px;
  }
  #toc {
    margin: 0;
    max-width: 100%;
    float: none;
  }
  #header {
    display: block;
    -webkit-transform: translate3D(0, 0, 0);
    -ms-transform: translate3D(0, 0, 0);
    transform: translate3D(0, 0, 0);
    -webkit-transition: all .2s cubic-bezier(.4, .01, .165, .99);
    transition: all .2s cubic-bezier(.4, .01, .165, .99)
  }
  #sidebar {
    -webkit-transition: -webkit-transform .2s cubic-bezier(.4, .01, .165, .99);
    transition: transform .2s cubic-bezier(.4, .01, .165, .99), -webkit-transform .2s cubic-bezier(.4, .01, .165, .99)
  }
  #sidebar.behavior_1 {
    -webkit-transform: translate3D(-250px, 0, 0);
    -ms-transform: translate3D(-250px, 0, 0);
    transform: translate3D(-250px, 0, 0)
  }
  #sidebar.behavior_2 {
    -webkit-transform: translate3D(0, 0, 0);
    -ms-transform: translate3D(0, 0, 0);
    transform: translate3D(0, 0, 0)
  }
  #sidebar .profile {
    padding-top: 20px;
    padding-bottom: 20px
  }
  #sidebar .profile a,
  #sidebar .profile img {
    border-radius: 100px;
    height: 100px;
    width: 100px
  }
  #sidebar .profile span {
    display: none
  }
}

@media screen and (min-width:769px) and (max-width:1024px) {
  #sidebar {
    width: 75px
  }
  #sidebar .profile {
    padding-top: 20px
  }
  #sidebar .profile a,
  #sidebar .profile img {
    border-radius: 40px;
    height: 40px;
    width: 40px
  }
  #sidebar .profile span {
    display: none
  }
  #sidebar .buttons li a {
    padding: 0
  }
  #sidebar .buttons li a i {
    display: block;
    font-size: 18px;
    margin: 0 auto
  }
  #sidebar .buttons li a span {
    display: none
  }
  #sidebar .buttons li a.inline {
    width: 100%
  }
}

@media screen and (min-width:768px) and (max-width:1024px) {
  #main {
    padding-left: 115px
  }
}

@media screen and (max-width:769px) {
  #main {
    min-height: 100%;
    padding-top: 50px;
    padding-left: 10px;
    padding-right: 10px;
    width: 100%
  }
}

@media screen and (max-width:769px) {
  article {
    background-color: #fff;
    padding: 10px
  }
  article h1 {
    font-size: 22px;
    margin: 0;
    padding: 5px 0 10px
  }
  article .meta {
    display: none
  }
  article .desc {
    color: #999;
    font-size: 14px
  }
  article p.more {
    font-size: 14px;
    margin: 5px 0
  }
}

================================================
FILE: front/src/assets/css/sidebar.css
================================================
#sidebar {
  background-color: #202020;
  height: 100%;
  left: 0;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  position: fixed;
  top: 0;
  width: 250px;
  z-index: 1
}

#sidebar-mask {
  background-color: rgba(255, 255, 255, 0);
  bottom: 0;
  display: none;
  left: 0;
  overflow: hidden;
  position: absolute;
  right: 0;
  top: 0;
  z-index: 999
}

#sidebar li,
#sidebar ul {
  list-style: none;
  margin: 0;
  padding: 0
}

#sidebar .profile {
  padding-top: 40px;
  padding-bottom: 10px
}

#sidebar .buttons {
  margin: 0 0 20px
}

#sidebar .profile a,
#sidebar .profile img {
  border-radius: 70px;
  height: 140px;
  overflow: hidden;
  width: 140px
}

#sidebar .profile a {
  display: block;
  margin: 0 auto
}

#sidebar .profile span {
  color: #999;
  display: block;
  font-size: 18px;
  padding: 10px 0;
  text-align: center
}

#sidebar .buttons li {
  display: block;
  font-size: 16px;
  height: 45px;
  line-height: 45px;
  width: 100%
}

#sidebar .buttons li a {
  color: #999;
  display: block;
  padding-left: 25px;
  text-decoration: none;
  -webkit-transition: color .2s cubic-bezier(.4, .01, .165, .99);
  transition: color .2s cubic-bezier(.4, .01, .165, .99)
}

#sidebar .buttons li a i,
#sidebar .buttons li a span {
  display: inline-block;
  vertical-align: middle
}

#sidebar .buttons li a i {
  font-size: 20px;
  height: 45px;
  line-height: 45px;
  margin-right: 20px;
  text-align: center;
  width: 25px
}

#sidebar .buttons li a:hover {
  color: rgba(153, 153, 153, .8)
}

#sidebar .buttons li a.inline {
  display: inline-block;
  width: 40px
}

================================================
FILE: front/src/assets/js/base.js
================================================
/* eslint-disable */
export default function () {
  if (typeof window !== 'undefined') {

    (function (win, doc) {
      var getById = function (el) {
        return document.getElementById(el);
      };

      //from qwrap
      var getDocRect = function (doc) {
        doc = doc || document;
        var win = doc.defaultView || doc.parentWindow,
          mode = doc.compatMode,
          root = doc.documentElement,
          h = win.innerHeight || 0,
          w = win.innerWidth || 0,
          scrollX = win.pageXOffset || 0,
          scrollY = win.pageYOffset || 0,
          scrollW = root.scrollWidth,
          scrollH = root.scrollHeight;
        if (mode != 'CSS1Compat') { // Quirks
          root = doc.body;
          scrollW = root.scrollWidth;
          scrollH = root.scrollHeight;
        }
        if (mode) { // IE, Gecko
          w = root.clientWidth;
          h = root.clientHeight;
        }
        scrollW = Math.max(scrollW, w);
        scrollH = Math.max(scrollH, h);
        scrollX = Math.max(scrollX, doc.documentElement.scrollLeft, doc.body.scrollLeft);
        scrollY = Math.max(scrollY, doc.documentElement.scrollTop, doc.body.scrollTop);
        return {
          width: w,
          height: h,
          scrollWidth: scrollW,
          scrollHeight: scrollH,
          scrollX: scrollX,
          scrollY: scrollY
        };
      };

      var getXY = function (node) {
        var doc = node.ownerDocument,
          docRect = getDocRect(doc),
          scrollLeft = docRect.scrollX,
          scrollTop = docRect.scrollY,
          box = node.getBoundingClientRect(),
          xy = [box.left, box.top],
          mode,
          off1,
          off2;
        if (scrollTop || scrollLeft) {
          xy[0] += scrollLeft;
          xy[1] += scrollTop;
        }
        return xy;
      };

      var getRect = function (el) {
        var p = getXY(el);
        var x = p[0];
        var y = p[1];
        var w = el.offsetWidth;
        var h = el.offsetHeight;
        return {
          'width': w,
          'height': h,
          'left': x,
          'top': y,
          'bottom': y + h,
          'right': x + w
        };
      };

      var utils = {
        isMob: (function () {
          var ua = navigator.userAgent.toLowerCase();
          var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
          var result = false;
          for (var i = 0; i < agents.length; i++) {
            if (ua.indexOf(agents[i].toLowerCase()) > -1) {
              result = true;
            }
          }
          return result;
        })()
      }


      if (utils.isMob) {
        document.documentElement.className += ' mob';
      } else {
        document.documentElement.className += ' pc';
      }


      var Dom = {
        $sidebar: document.querySelector('#sidebar'),
        $main: document.querySelector('#main'),
        $sidebar_mask: document.querySelector('#sidebar-mask'),
        $body: document.body,
        $btn_side: document.querySelector('#header .btn-bar'),
        $article: document.querySelectorAll('.mob #page-index article')
      };

      Dom.bindEvent = function () {

        var _this = this,
          body_class_name = 'side',
          eventFirst = 'click',
          eventSecond = 'click';

        if (utils.isMob) {
          eventFirst = 'touchstart';
          eventSecond = 'touchend';
        }

        try {

          this.$btn_side.addEventListener(eventSecond, function () {

            if (_this.$body.className.indexOf(body_class_name) > -1) {
              _this.$body.className = _this.$body.className.replace(body_class_name, '');
              _this.$sidebar_mask.style.display = 'none';
            } else {
              _this.$body.className += (' ' + body_class_name);
              _this.$sidebar_mask.style.display = 'block';
            }

          }, false);

        } catch (err) {}

        try {

          this.$sidebar_mask.addEventListener(eventFirst, function (e) {
            _this.$body.className = _this.$body.className.replace(body_class_name, '');
            _this.$sidebar_mask.style.display = 'none';
            e.preventDefault();
          }, false);

        } catch (err) {}


        window.addEventListener('resize', function () {
          try {
            _this.$body.className = _this.$body.className.replace(body_class_name, '');
            _this.$sidebar_mask.style.display = 'none';
          } catch (err) {}
        }, false);
      }

      Dom.bindEvent();

    })(window, document);
  }
}

================================================
FILE: front/src/client-entry.js
================================================
import Vue from 'vue'
import createApp from './main'
const { app, appOption, router, store, isProd, preFetchComponent } = createApp()
import clientGoogleAnalyse from './utils/clientGoogleAnalyse'
import makeResponsive from './assets/js/base'

const callback = isProd ? setTimeout : router.onReady.bind(router)
if (isProd) {
  store.state.isLoadingAsyncComponent = true
}

// setTimeout to make the following chunks loaded to webpack modules,
// therefore webpackJsonp won't create script to head to send a request
callback(() => {
  if (isProd) store.state.isLoadingAsyncComponent = false
  let realApp = isProd ? new Vue(appOption) : app

  // SSR can not render hash since browsers even don't send it
  // therefore we must hydrate the hash for the client side vue-router,
  // which is important for hash anchor jump especially for Table Of Contents(toc)
  if (window.__INITIAL_STATE__) {
    makeResponsive()
    window.__INITIAL_STATE__.route.hash = window.location.hash
    store.replaceState(window.__INITIAL_STATE__)
    realApp.$mount('#app')
  }

  // service worker
  if (isProd && 'serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js')
  }

  const beforeEachHook = (to, from, next) => {
    // required by a new hash, just navigate to it
    if (to.path === from.path && to.hash !== from.hash) {
      return next()
    }
    let loadingPromise = store.dispatch('START_LOADING')
    let endLoadingCallback = (path) => {
      return loadingPromise.then(interval => {
        clearInterval(interval)
        store.dispatch('SET_PROGRESS', 100)
        next(path)
      })
    }

    // there must be a matched component according
    // to routes definition
    let component = router.getMatchedComponents(to.fullPath)[0]

    // if it's an async component, resolve it and check the preFetch
    // which can avoid clock when routes change
    if (typeof component === 'function' && !component.options) {
      return new Promise((resolve, reject) => {
        const _resolve = realComponent => {
          resolve(realComponent)
        }
        // for general component
        let res = component(_resolve)
        // for factory component
        if (res && res.then) {
          res.then(_resolve)
        }
      }).then(component => letsGo(component, store, to, endLoadingCallback))
    }
    // component is there, check the preFetch
    letsGo(component, store, to, endLoadingCallback)
  }

  router.beforeEach(beforeEachHook)

  function letsGo(component, store, to, endLoadingCallback) {
    if (component && component.preFetch) {
      // component need fetching some data before navigating to it
      return component.preFetch(store, to, endLoadingCallback).catch(err => {
        console.error(new Date().toLocaleString(), err)
        endLoadingCallback(false)
      })
    } else {
      // component's a static page and just navigate to it
      endLoadingCallback()
    }
  }

  if (typeof window.__INITIAL_STATE__ === 'undefined') {
    realApp.$mount('#app')
    beforeEachHook(router.currentRoute, {}, () => {})
    Promise.all(
      preFetchComponent.map(component => component.preFetch(store, store.state.route))
    ).then(() => makeResponsive())
  }

  // send user info if google analytics code is provided.
  if (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.siteInfo) {
    let analyzeCode = window.__INITIAL_STATE__.siteInfo.analyzeCode
    if (analyzeCode && analyzeCode.value !== '') {
      router.afterEach((to, from) => {
        // should delay it to get the correct title generated by vue-meta
        from.name && to.path !== from.path && setTimeout(() => {
          clientGoogleAnalyse(to.path || '/')
        })
      })
    }
  }
})


================================================
FILE: front/src/components/App.vue
================================================
<template>
  <div id="app">
    <loading-bar :progress="progress"></loading-bar>
    <sidebar></sidebar>
    <my-header></my-header>
    <router-view></router-view>
  </div>
</template>

<script>
import { mapGetters } from '../store/vuex'
import Sidebar from './Sidebar'
import LoadingBar from '../components/Loading'
import MyHeader from '../components/Header'

export default {
  components: {
    LoadingBar,
    Sidebar,
    MyHeader
  },
  computed: {
    ...mapGetters([
      'progress'
    ])
  }
}
</script>

================================================
FILE: front/src/components/Archive.vue
================================================
<template>
	<div id='main'>
		<article class="post archive">
			<h1 class=title>{{title}}</h1>
			<div class="entry-content" v-for="(item, key, index) in achieves">
				<h3>{{ key }} ({{item.length}})</h3>
				<ul>
					<li v-for="subItem in item">
						<router-link :to="{name: 'post', params: { pathName:subItem.pathName  }}" :title="subItem.title">{{subItem.title}}</router-link>&nbsp
						<span class=date>{{ subItem.createdAt.split(' ')[0] }}</span>
					</li>
				</ul>
			</div>
		</article>
		<my-footer></my-footer>
	</div>
</template>

<script>
import { mapGetters } from '../store/vuex'

function fetchAchieves(store, to, callback) {
  return store.dispatch('FETCH_ACHIEVE', {
    model: 'post',
    query: {
      conditions: {
        type: 'post',
        isPublic: true
      },
      select: {
        _id: 0,
        title: 1,
        createdAt: 1,
        pathName: 1
      },
      sort: {
        createdAt: -1
      }
    },
    callback
  })
}

export default {
  metaInfo() {
    return {
      title: this.title
    }
  },
  data() {
    return {
      title: '归档'
    }
  },
  computed: {
    ...mapGetters([
      'achieves',
      'isLoadingAsyncComponent'
    ])
  },
  preFetch: fetchAchieves,
  beforeMount() {
    this.isLoadingAsyncComponent && this.$root._isMounted && fetchAchieves(this.$store, this.$route)
  }
}
</script>

================================================
FILE: front/src/components/BlogPager.vue
================================================
<template>
  <div id='main'>
    <section id="page-index">
      <blog-summary v-for="item in items" :key="item.pathName" :support-webp="supportWebp" :article="item"></blog-summary>
      <pagination :page="page" :total-page="totalPage"></pagination>
    </section>
    <my-footer></my-footer>
  </div>
</template>

<script>
import { mapGetters } from '../store/vuex'
import BlogSummary from './BlogSummary'

function fetchItems(store, { path, query, params }, callback) {
  if (path !== '/') {
    return Promise.resolve()
  }

  let page = query ? (typeof query.page !== 'undefined') ? parseInt(query.page) : 1 : 1
  if (page < 0) {
    page = 1
  }

  return store.dispatch('FETCH_ITEMS', {
    model: 'post',
    query: {
      conditions: {
        type: 'post',
        isPublic: true
      },
      select: {
        _id: 0,
        title: 1,
        summary: 1,
        createdAt: 1,
        updatedAt: 1,
        pathName: 1
      },
      limit: 10,
      skip: (page - 1) * 10,
      sort: {
        createdAt: -1
      }
    },
    callback
  })
}

export default {
  name: 'blog-pager',
  metaInfo() {
    return {
      title: '首页'
    }
  },
  components: {
    BlogSummary
  },
  computed: {
    ...mapGetters([
      'items',
      'page',
      'totalPage',
      'siteInfo',
      'isLoadingAsyncComponent',
      'supportWebp'
    ])
  },
  preFetch: fetchItems,
  beforeMount() {
    this.isLoadingAsyncComponent && this.$root._isMounted && fetchItems(this.$store, this.$route)
  }
}
</script>

================================================
FILE: front/src/components/BlogSummary.vue
================================================
<template>
  <article class="post">
    <div class="meta">
      <div class="date">{{ article.createdAt }}</div>
    </div>
    <h1 class="title"> <router-link :to="{ name:'post', params:{ pathName: article.pathName } }" >{{ article.title }}</router-link></h1>
    <div class="entry-content">
      <div v-html="filterWebp(article.summary)"/>
      <router-link :to="{ name:'post', params:{ pathName: article.pathName } }" >阅读更多 »</router-link>
    </div>
  </article>
</template>

<script>
export default {
  name: 'blog-summary',
  props: {
    article: {
      type: Object,
      required: true
    },
    supportWebp: Boolean
  },
  serverCacheKey: props => {
    return `${props.article.pathName}::${props.article.updatedAt}::webp::${props.supportWebp}`
  },
  methods: {
    filterWebp(content) {
      if (!this.supportWebp) return content.replace(/\/webp/gm, '')
      return content
    }
  }
}
</script>


================================================
FILE: front/src/components/Disqus.vue
================================================
<template>
	<div id="disqus_thread"></div>
</template>

<script>
  export default {
    props: {
      shortname: {
        type: String,
        required: true
      }
    },
    mounted() {
      if (window.DISQUS) {
        this.reset(window.DISQUS)
        return
      }
      this.init()
    },
    methods: {
      reset(dsq) {
        const self = this
        dsq.reset({
          reload: true,
          config: function() {
            this.page.identifier = (self.$route.path || window.location.pathname)
            this.page.url = self.$el.baseURI
          }
        })
      },
      init() {
        const self = this
        window.disqus_config = function() {
          this.page.identifier = (self.$route.path || window.location.pathname)
          this.page.url = self.$el.baseURI
        }
        setTimeout(() => {
          let d = document
          let s = d.createElement('script')
          s.type = 'text/javascript'
          s.async = true
          s.setAttribute('id', 'embed-disqus')
          s.setAttribute('data-timestamp', +new Date())
          s.src = `//${this.shortname}.disqus.com/embed.js`
          ;(d.head || d.body).appendChild(s)
        }, 0)
      }
    }
  }
</script>

================================================
FILE: front/src/components/Footer.vue
================================================
<template>
  <footer id="footer" class="inner">
    &copy; 2016&nbsp;-&nbsp; {{ siteInfo.title.value }} &nbsp;-&nbsp;
    <a v-if="!(siteInfo.miitbeian.value)" target=_blank href="https://github.com/smallpath/blog">博客源码</a>
    <a v-else target="_blank" rel="nofollow noopener" 
        href="http://www.miitbeian.gov.cn/"
        style="color: #666"
    >{{ siteInfo.miitbeian.value }}</a>
    <br> Powered by&nbsp;
    <a target=_blank href="https://github.com/vuejs/vue">Vue2</a>
    &nbsp;&amp;&nbsp;
    <a target=_blank href="https://github.com/koajs/koa">Koa2</a>        
  </footer>
</template>

<script>
import { mapGetters } from '../store/vuex'

export default {
  computed: {
    ...mapGetters([
      'siteInfo'
    ])
  }
}
</script>

================================================
FILE: front/src/components/Header.vue
================================================
<template>
  <div>
    <div id="header" :style="{ 
        'background-image': sidebarUrl 
        ? 'url(' + sidebarUrl + ')' : ''
      }">
      <div class="btn-bar"><i></i></div>
      <h1><a :style="{'color': option ? option.sidebarFontColor || '' : ''}" href="/">{{ siteInfo.title.value }}</a></h1>
      <a class=me href="/about">
        <img :src="logoUrl" :alt="siteInfo.title.value">
      </a>
    </div>
    <div id="sidebar-mask"></div>
  </div>
</template>

<script>
import mixin from '../mixin/image'

export default {
  mixins: [mixin]
}
</script>

================================================
FILE: front/src/components/Loading.vue
================================================
<template>
  <div>
      <div v-if="show" class="loading-bar loading-bar--to_right"
            :class="{ 'loading-bar--full': full }"
            :style="styling()">
        <div class="loading-bar-glow"></div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    progress: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      show: true,
      full: '',
      width: 0,
      wait: false
    }
  },
  watch: {
    progress(val, old) {
      if (old !== val) {
        this.width = val
        this.$nextTick(() => {
          this.isFull()
        })
      }
    }
  },
  methods: {
    isFull() {
      let isFull = this.width === 100
      if (isFull) {
        this.wait = true
        setTimeout(() => {
          this.full = true
          setTimeout(() => {
            this.show = false
            this.width = 0
            this.wait = false
            this.$nextTick(() => {
              this.full = ''
              this.show = true
            })
          }, 400)
        }, 400)
      }
    },
    styling() {
      if (!this.wait) {
        return { width: `${this.width}%` }
      } else {
        return { width: '100%' }
      }
    }
  }
}
</script>

<style scoped>
  .loading-bar{
    transition: all .4s ease;
    -moz-transition: all .4s ease;
    -webkit-transition: all .4s ease;
    -o-transition: all .4s ease;
    position: fixed;
    top: 0;
    background: #77b6ff;
    height: 2px;
    opacity: 1
  }

  .loading-bar-glow{
    top: 0;
    position: absolute;
    width: 100%;
    height: 100%;
    box-shadow: -2px 0 15px 1px rgba(119,182,255,0.7)
  }

  .loading-bar--to_right{
    left: 0;
    z-index: 1000;
  }

  .loading-bar--to_right .loading-bar-glow{
    right: 0;
    z-index: 1000;
  }

  .loading-bar--full{
    transition: all .3s ease;
    -moz-transition: all .3s ease;
    -webkit-transition: all .3s ease;
    -o-transition: all .3s ease;
    height: 0;
    opacity: 0;
  }
</style>

================================================
FILE: front/src/components/Pagination.vue
================================================
<template>
  <nav class="pagination">
    <router-link v-if="page > 1" :to="{ query: { page: parseInt(page) - 1} }" class="prev">&laquo; 上一页</router-link>
    <router-link v-if="page < totalPage" :to="{ query: { page: parseInt(page) + 1}  }" class="next">下一页 &raquo;</router-link>
    <div class=center>
      <router-link :to="{name:'archive'}">博客归档</router-link>
    </div>
  </nav>
</template>

<script>
export default {
  props: {
    totalPage: Number,
    page: Number
  }
}
</script>

================================================
FILE: front/src/components/Post.vue
================================================
<template>
  <div id='main'>
    <div id="page-post">
      <article class="post detail">
        <div class="meta">
          <div class="date">{{ post.createdAt }}</div>
        </div>
        <h1 class="title">{{ post.title }}</h1>

        <div class="entry-content" v-html="content"></div>

        <template v-if="shouldShow">
          <p>本文链接:<a :href="siteURL+ '/post/'+ post.pathName">{{siteURL}}/post/{{post.pathName}}</a></p>
          <p>-- <acronym title="End of File">EOF</acronym> --</p>
          <div class="post-info">
            <p> 发表于 <i>{{post.createdAt}}</i> ,
              添加在分类「
              <a :data-cate="post.category">
                  <code class="notebook">{{post.category}}</code>
              </a> 」下 ,并被添加「
              <router-link v-for="tag in post.tags" 
                  :to="{name:'tagPager', params: { tagName: tag }}"
                  :key="tag"
                  :data-tag="tag"> 
                  <code class="notebook">{{tag}}</code>
              </router-link> 」标签 ,最后修改于 <i>{{post.updatedAt}}</i>
            </p>
          </div>
        </template>
      </article>
      <nav v-if="shouldShow" class=pagination> 
        <router-link v-if="typeof prev.pathName !== 'undefined'" 
          :to="{name:'post', params: {pathName: prev.pathName }}" class="prev">&laquo {{prev.title }}</router-link> 
        <router-link v-if="typeof next.pathName !== 'undefined'" 
          :to="{name:'post', params: {pathName: next.pathName }}" class="next">{{next.title }} &raquo</router-link> 
      </nav>
      <div class="comments" v-if="post.allowComment === true && commentName !== ''">
        <disqus :shortname="commentName" ></disqus>
      </div>
    </div>
    <my-footer></my-footer>
  </div>
</template>

<script>
import mixin from '../mixin/disqus'
import Disqus from './Disqus'

export default {
  name: 'post',
  components: {
    Disqus
  },
  props: ['type', 'post', 'prev', 'next', 'siteInfo', 'supportWebp'],
  mixins: [mixin],
  serverCacheKey: props => {
    return `${props.post.pathName}::${props.post.updatedAt}::webp::${props.supportWebp}`
  },
  computed: {
    content() {
      const post = this.post
      const result = post.toc ? `<div id="toc" class="toc">${post.toc}</div>${post.content}` : post.content
      return this.filterWebp(result)
    },
    shouldShow() {
      return this.post.pathName !== 404 && this.type === 'post'
    },
    commentName() {
      return this.siteInfo.commentName.value || ''
    },
    siteURL() {
      return this.siteInfo.siteUrl.value || 'localhost'
    }
  },
  methods: {
    filterWebp(content) {
      if (!this.supportWebp) return content.replace(/\/webp/gm, '')
      return content
    }
  }
}
</script>


================================================
FILE: front/src/components/Sidebar.vue
================================================
<template>
  <nav id=sidebar class=behavior_1 
          :class="{'sidebar-image': sidebarUrl !== ''}"
          :style="imageStyle">
    <div class=wrap>
      <div class=profile>
        <a href="/"> 
          <img :src="logoUrl" 
            :alt="siteInfo.title.value">
        </a> 
        <span :style="{ 'color': sidebarUrl ? option.sidebarFontColor : '' }" >{{siteInfo.title.value}}</span>
      </div>
      <ul class="buttons" v-if="option && option.menu">
        <li v-for="menu in option.menu">
          <router-link 
            :style="buttonColor"  
            :to="{ path: menu.url }" :title="menu.label"> <i class="iconfont" :class="'icon-' + menu.option"></i> <span>{{menu.label}}</span></router-link>
        </li>
      </ul>
      <ul class="buttons" v-if="siteInfo && siteInfo.weiboUrl">
        <li>
          <a class="inline" :style="buttonColor" v-if="siteInfo.githubUrl.value"
            rel="nofollow" target="_blank" :href="'https://github.com/'+siteInfo.githubUrl.value"><i class="iconfont icon-github-v" title="GitHub"></i></a>
          <a class="inline" :style="buttonColor" v-if="siteInfo.weiboUrl.value" 
            rel="nofollow" target="_blank" :href="siteInfo.weiboUrl.value"><i class="iconfont icon-twitter-v" title="Twitter"></i></a>
          <a class="inline" :style="buttonColor" href="/rss.xml"><i class="iconfont icon-rss-v" title="RSS"></i></a>
          <a class="inline" :style="buttonColor" 
            v-if="siteInfo.siteUrl.value"
            target=_blank :href="'https://www.google.com/webhp#newwindow=1&safe=strict&q=site:' + siteInfo.siteUrl.value"><i class="iconfont icon-search" title="Search"></i></a>
        </li>
      </ul>
    </div>
  </nav>
</template>

<script>
import mixin from '../mixin/image'

function fetchInfo(store, { path, params, query }) {
  return Promise.all([store.dispatch('FETCH_OPTIONS'), store.dispatch('FETCH_FIREKYLIN')])
}

export default {
  metaInfo() {
    const {
      title: { value: title },
      description: { value: description },
      keywords: { value: keywords },
      logoUrl: { value: logoUrl },
      faviconUrl: { value: favicon }
    } = this.siteInfo
    return {
      title,
      titleTemplate: `%s - ${title}`,
      meta: [
        { name: 'charset', content: 'UTF-8' },
        { name: 'description', content: description },
        { name: 'keywords', content: keywords },
        { name: 'viewport', content: 'width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' }
      ],
      link: [
        { rel: 'icon', href: favicon },
        { rel: 'apple-touch-icon', href: logoUrl },
        { rel: 'alternate', type: 'application/rss+xml', title: 'RSS 2.0', href: '/rss.xml' }
      ]
    }
  },
  mixins: [mixin],
  preFetch: fetchInfo,
  computed: {
    buttonColor() {
      return { 'color': this.sidebarUrl ? this.option.sidebarFontColor : '' }
    },
    imageStyle() {
      const sidebarUrl = this.sidebarUrl
      const sidebarMoveCss = sidebarUrl ? this.option.sidebarMoveCss : ''
      const result = {
        'background-image': sidebarUrl ? 'url(' + sidebarUrl + ')' : '',
        'transition': sidebarMoveCss
      }
      return result
    }
  }
}
</script>
 
<style>
@import '../assets/css/icon.css';
@import '../assets/css/article.css';
@import '../assets/css/base.css';
@import '../assets/css/footer.css';
@import '../assets/css/header.css';
@import '../assets/css/highlight.css';
@import '../assets/css/pagination.css';
@import '../assets/css/sidebar.css';
@import '../assets/css/responsive.css';

.sidebar-image {
  background-position: left center;
  background-repeat: no-repeat;
  background-size: cover;
  overflow: auto;
}

.sidebar-image:hover {
  background-position: right center;
}
</style>

================================================
FILE: front/src/components/Tag.vue
================================================
<template>
  <div id='main'>
    <article class="post tags">
      <h1 class=title>{{title}}</h1>
      <div class="entry-content">
        <section> 
          <router-link v-for="(key, index) in sortedKeys" 
              :to="{ name: 'tagPager', params:{ tagName: key } }"
              :key="index"
              :data-tag="key">{{key}}({{tags[key]}})</router-link> 
        </section>
      </div>
    </article>
    <my-footer></my-footer>
  </div>
</template>

<script>
import { mapGetters } from '../store/vuex'

function fetchTags(store, { path: pathName, params, query }, callback) {
  return store.dispatch('FETCH_TAGS', {
    model: 'post',
    query: {
      conditions: {
        type: 'post',
        isPublic: true
      },
      select: {
        _id: 0,
        tags: 1
      }
    },
    callback
  })
}

export default {
  metaInfo() {
    return {
      title: this.title
    }
  },
  data() {
    return {
      title: '标签'
    }
  },
  computed: {
    ...mapGetters([
      'tags',
      'isLoadingAsyncComponent'
    ]),
    sortedKeys() {
      let ref = this.tags
      return Object.keys(ref).sort((a, b) => ref[b] - ref[a])
    }
  },
  preFetch: fetchTags,
  beforeMount() {
    this.isLoadingAsyncComponent && this.$root._isMounted && fetchTags(this.$store, this.$route)
  }
Download .txt
gitextract_n6nb127_/

├── Dockerfile
├── LICENSE
├── README.md
├── admin/
│   ├── .babelrc
│   ├── .eslintignore
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── README.md
│   ├── build/
│   │   ├── build.js
│   │   ├── check-versions.js
│   │   ├── dev-client.js
│   │   ├── dev-server.js
│   │   ├── utils.js
│   │   ├── webpack.base.conf.js
│   │   ├── webpack.dev.conf.js
│   │   └── webpack.prod.conf.js
│   ├── config/
│   │   ├── dev.env.js
│   │   ├── index.js
│   │   ├── prod.env.js
│   │   └── test.env.js
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/
│   │   │   ├── Main.vue
│   │   │   ├── containers/
│   │   │   │   ├── Create.vue
│   │   │   │   ├── List.vue
│   │   │   │   ├── Markdown.vue
│   │   │   │   └── Post.vue
│   │   │   ├── pages/
│   │   │   │   ├── Dashboard.vue
│   │   │   │   ├── Login.vue
│   │   │   │   ├── Logout.vue
│   │   │   │   ├── Sidebar.vue
│   │   │   │   └── Top.vue
│   │   │   ├── utils/
│   │   │   │   └── marked.js
│   │   │   └── views/
│   │   │       ├── CreateEditView.js
│   │   │       ├── CreateListView.js
│   │   │       └── CreateMarkdownView.js
│   │   ├── main.js
│   │   ├── route/
│   │   │   └── index.js
│   │   ├── store/
│   │   │   ├── api.js
│   │   │   └── index.js
│   │   └── utils/
│   │       └── error.js
│   ├── static/
│   │   └── .gitkeep
│   └── test/
│       ├── e2e/
│       │   ├── custom-assertions/
│       │   │   └── elementCount.js
│       │   ├── nightwatch.conf.js
│       │   ├── runner.js
│       │   └── specs/
│       │       └── test.js
│       └── unit/
│           ├── .eslintrc
│           ├── index.js
│           ├── karma.conf.js
│           └── specs/
│               └── Hello.spec.js
├── docs/
│   ├── .nojekyll
│   ├── README.md
│   └── index.html
├── front/
│   ├── .babelrc
│   ├── .eslintignore
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── README.md
│   ├── build/
│   │   ├── build-client.js
│   │   ├── setup-dev-server.js
│   │   ├── utils.js
│   │   ├── vue-loader.config.js
│   │   ├── webpack.base.config.js
│   │   ├── webpack.client.config.js
│   │   └── webpack.server.config.js
│   ├── config/
│   │   ├── dev.env.js
│   │   ├── index.js
│   │   ├── prod.env.js
│   │   └── test.env.js
│   ├── middleware/
│   │   ├── favicon.js
│   │   └── serverGoogleAnalytic.js
│   ├── package.json
│   ├── production.js
│   ├── server/
│   │   ├── config.js
│   │   ├── model.js
│   │   ├── mongo.tpl
│   │   ├── robots.js
│   │   ├── rss.js
│   │   ├── server-axios.js
│   │   └── sitemap.js
│   ├── server.js
│   ├── src/
│   │   ├── assets/
│   │   │   ├── css/
│   │   │   │   ├── article.css
│   │   │   │   ├── base.css
│   │   │   │   ├── footer.css
│   │   │   │   ├── header.css
│   │   │   │   ├── highlight.css
│   │   │   │   ├── icon.css
│   │   │   │   ├── pagination.css
│   │   │   │   ├── responsive.css
│   │   │   │   └── sidebar.css
│   │   │   └── js/
│   │   │       └── base.js
│   │   ├── client-entry.js
│   │   ├── components/
│   │   │   ├── App.vue
│   │   │   ├── Archive.vue
│   │   │   ├── BlogPager.vue
│   │   │   ├── BlogSummary.vue
│   │   │   ├── Disqus.vue
│   │   │   ├── Footer.vue
│   │   │   ├── Header.vue
│   │   │   ├── Loading.vue
│   │   │   ├── Pagination.vue
│   │   │   ├── Post.vue
│   │   │   ├── Sidebar.vue
│   │   │   ├── Tag.vue
│   │   │   └── TagPager.vue
│   │   ├── index.template.html
│   │   ├── main.js
│   │   ├── mixin/
│   │   │   ├── disqus.js
│   │   │   └── image.js
│   │   ├── route/
│   │   │   ├── create-route-client.js
│   │   │   ├── create-route-server.js
│   │   │   └── index.js
│   │   ├── server-entry.js
│   │   ├── store/
│   │   │   ├── api.js
│   │   │   ├── client-axios.js
│   │   │   ├── create-api-client.js
│   │   │   ├── create-api-server.js
│   │   │   ├── index.js
│   │   │   └── vuex.js
│   │   ├── utils/
│   │   │   ├── 404.js
│   │   │   └── clientGoogleAnalyse.js
│   │   └── views/
│   │       └── CreatePostView.js
│   └── test/
│       ├── e2e/
│       │   ├── custom-assertions/
│       │   │   └── elementCount.js
│       │   ├── nightwatch.conf.js
│       │   ├── runner.js
│       │   └── specs/
│       │       └── test.js
│       └── unit/
│           ├── .eslintrc
│           ├── index.js
│           ├── karma.conf.js
│           └── specs/
│               └── Hello.spec.js
├── pm2.json
└── server/
    ├── .eslintignore
    ├── .eslintrc.js
    ├── .gitignore
    ├── README.md
    ├── app.js
    ├── blogpack.js
    ├── build/
    │   ├── blogpack.base.config.js
    │   ├── blogpack.dev.config.js
    │   └── blogpack.prod.config.js
    ├── conf/
    │   ├── config.tpl
    │   └── option.js
    ├── entry.js
    ├── model/
    │   ├── mongo.js
    │   └── redis.js
    ├── mongoRest/
    │   ├── actions.js
    │   ├── index.js
    │   └── routes.js
    ├── package.json
    ├── plugins/
    │   ├── beforeRestful/
    │   │   └── checkAuth.js
    │   ├── beforeServerStart/
    │   │   ├── initOption.js
    │   │   ├── initUser.js
    │   │   └── installTheme.js
    │   ├── beforeUseRoutes/
    │   │   ├── bodyParser.js
    │   │   ├── logTime.js
    │   │   ├── ratelimit.js
    │   │   └── restc.js
    │   └── mountingRoute/
    │       ├── login.js
    │       ├── logout.js
    │       └── qiniu.js
    ├── service/
    │   └── token.js
    ├── test/
    │   ├── data/
    │   │   ├── index.js
    │   │   └── post.js
    │   ├── index.js
    │   ├── integration/
    │   │   └── postRestful.js
    │   └── unit/
    │       └── postQuery.js
    ├── theme/
    │   └── firekylin.js
    └── utils/
        └── log.js
Download .txt
SYMBOL INDEX (65 symbols across 32 files)

FILE: admin/build/utils.js
  function generateLoaders (line 15) | function generateLoaders (loaders) {

FILE: admin/src/components/views/CreateEditView.js
  method preFetch (line 6) | preFetch(store) {
  method render (line 9) | render(h) {

FILE: admin/src/components/views/CreateListView.js
  method preFetch (line 6) | preFetch(store) {
  method render (line 9) | render(h) {

FILE: admin/src/components/views/CreateMarkdownView.js
  method preFetch (line 6) | preFetch(store) {
  method render (line 9) | render(h) {

FILE: front/build/setup-dev-server.js
  constant MFS (line 3) | const MFS = require('memory-fs')

FILE: front/build/utils.js
  function generateLoaders (line 12) | function generateLoaders (loaders) {

FILE: front/middleware/serverGoogleAnalytic.js
  constant EMPTY_GIF (line 5) | const EMPTY_GIF = new Buffer('R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAA...

FILE: front/server.js
  function flushHtml (line 80) | function flushHtml(template) {
  function createRenderer (line 90) | function createRenderer(bundle) {

FILE: front/server/config.js
  function flushOption (line 24) | function flushOption() {

FILE: front/server/server-axios.js
  function queryModel (line 15) | function queryModel(model, query) {

FILE: front/src/client-entry.js
  function letsGo (line 72) | function letsGo(component, store, to, endLoadingCallback) {

FILE: front/src/main.js
  function createApp (line 10) | function createApp(context) {

FILE: front/src/mixin/disqus.js
  constant TYPES (line 1) | const TYPES = ['post', 'page']
  method reset (line 8) | reset(dsq) {
  method resetDisqus (line 18) | resetDisqus(val, oldVal) {

FILE: front/src/mixin/image.js
  method logoUrl (line 10) | logoUrl() {
  method sidebarUrl (line 13) | sidebarUrl() {
  method getValidImageUrl (line 18) | getValidImageUrl(url) {

FILE: front/src/route/index.js
  function createRouter (line 23) | function createRouter() {

FILE: front/src/store/client-axios.js
  function get (line 1) | function get(url, cb) {

FILE: front/src/store/index.js
  function createStore (line 7) | function createStore() {

FILE: front/src/store/vuex.js
  class Store (line 3) | class Store {
    method constructor (line 4) | constructor({
    method state (line 37) | get state() {
    method state (line 41) | set state(state) {
    method commit (line 45) | commit(mutation, options) {
    method dispatch (line 52) | dispatch(action, options) {
    method watch (line 65) | watch(getter, cb, options) {
    method replaceState (line 69) | replaceState(state) {
    method registerModule (line 73) | registerModule(path, rawModule) {
  function install (line 79) | function install(_Vue) {
  function vuexInit (line 97) | function vuexInit() {
  function forEachValue (line 116) | function forEachValue(obj, fn) {

FILE: front/src/views/CreatePostView.js
  method metaInfo (line 35) | metaInfo() {
  method post (line 49) | post() {
  method beforeMount (line 55) | beforeMount() {
  method render (line 58) | render(h) {

FILE: server/blogpack.js
  class blogpack (line 2) | class blogpack {
    method constructor (line 3) | constructor(options) {
    method beforeUseRoutes (line 10) | async beforeUseRoutes(...args) {
    method getMiddlewareRoutes (line 16) | async getMiddlewareRoutes(...args) {
    method getBeforeRestfulRoutes (line 29) | getBeforeRestfulRoutes() {
    method getAfterRestfulRoutes (line 35) | getAfterRestfulRoutes() {
    method getBeforeServerStartFuncs (line 41) | getBeforeServerStartFuncs() {

FILE: server/plugins/beforeRestful/checkAuth.js
  method beforeRestful (line 5) | async beforeRestful(ctx, next) {

FILE: server/plugins/beforeServerStart/initOption.js
  method beforeServerStart (line 6) | async beforeServerStart() {

FILE: server/plugins/beforeServerStart/initUser.js
  method beforeServerStart (line 6) | async beforeServerStart() {

FILE: server/plugins/beforeServerStart/installTheme.js
  method beforeServerStart (line 8) | async beforeServerStart() {

FILE: server/plugins/beforeUseRoutes/bodyParser.js
  method beforeUseRoutes (line 4) | async beforeUseRoutes({ app }) {

FILE: server/plugins/beforeUseRoutes/logTime.js
  method beforeUseRoutes (line 4) | async beforeUseRoutes({ app, redis }) {

FILE: server/plugins/beforeUseRoutes/ratelimit.js
  method constructor (line 4) | constructor(options) {
  method beforeUseRoutes (line 8) | async beforeUseRoutes({ app, redis }) {

FILE: server/plugins/beforeUseRoutes/restc.js
  method beforeUseRoutes (line 4) | async beforeUseRoutes({ app }) {

FILE: server/plugins/mountingRoute/login.js
  method mountingRoute (line 6) | async mountingRoute() {
  function middleware (line 15) | async function middleware(ctx, next) {

FILE: server/plugins/mountingRoute/logout.js
  method mountingRoute (line 5) | async mountingRoute() {
  function middleware (line 14) | async function middleware(ctx, next) {

FILE: server/plugins/mountingRoute/qiniu.js
  method constructor (line 40) | constructor(options) {
  method mountingRoute (line 46) | async mountingRoute() {

FILE: server/service/token.js
  method createToken (line 10) | createToken(userinfo) {
  method verifyToken (line 17) | verifyToken(token) {
Condensed preview — 169 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (281K chars).
[
  {
    "path": "Dockerfile",
    "chars": 549,
    "preview": "\nFROM node:7.7\n\nMAINTAINER Jerry Bendy <jerry@icewingcc.com>, qfdk <qfdk2010#gmail.com>\n\n# copy all files to target \nCOP"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 6539,
    "preview": "# Blog\nA blog system. Based on Vue2, Koa2, MongoDB and Redis\n\n前后端分离 + 服务端渲染的博客系统, 前端 SPA + 后端 RESTful 服务器\n\n# Demo\n前端:[ht"
  },
  {
    "path": "admin/.babelrc",
    "chars": 96,
    "preview": "{\n  \"presets\": [\"es2015\", \"stage-2\"],\n  \"plugins\": [\"transform-runtime\"],\n  \"comments\": false\n}\n"
  },
  {
    "path": "admin/.eslintignore",
    "chars": 23,
    "preview": "build/*.js\nconfig/*.js\n"
  },
  {
    "path": "admin/.eslintrc.js",
    "chars": 614,
    "preview": "module.exports = {\n  root: true,\n  parser: 'babel-eslint',\n  parserOptions: {\n    sourceType: 'module'\n  },\n  // https:/"
  },
  {
    "path": "admin/.gitignore",
    "chars": 112,
    "preview": ".DS_Store\nnode_modules/\ndist/\nnpm-debug.log\ntest/unit/coverage\ntest/e2e/reports\nselenium-debug.log\n.editorconfig"
  },
  {
    "path": "admin/README.md",
    "chars": 899,
    "preview": "# admin\n\n> 博客的后台管理单页\n\n## 开发测试环境\n\n``` bash\nnpm install\nnpm run dev\n```\n开发端口为本机8082\n\n\n## 生产环境\n\n``` bash\nnpm install\nnpm ru"
  },
  {
    "path": "admin/build/build.js",
    "chars": 917,
    "preview": "// https://github.com/shelljs/shelljs\nrequire('./check-versions')()\nrequire('shelljs/global')\nenv.NODE_ENV = 'production"
  },
  {
    "path": "admin/build/check-versions.js",
    "chars": 1182,
    "preview": "var semver = require('semver')\nvar chalk = require('chalk')\nvar packageConfig = require('../package.json')\nvar exec = fu"
  },
  {
    "path": "admin/build/dev-client.js",
    "chars": 245,
    "preview": "/* eslint-disable */\nrequire('eventsource-polyfill')\nvar hotClient = require('webpack-hot-middleware/client?noInfo=true&"
  },
  {
    "path": "admin/build/dev-server.js",
    "chars": 2194,
    "preview": "require('./check-versions')()\nvar config = require('../config')\nif (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.p"
  },
  {
    "path": "admin/build/utils.js",
    "chars": 1951,
    "preview": "var path = require('path')\nvar config = require('../config')\nvar ExtractTextPlugin = require('extract-text-webpack-plugi"
  },
  {
    "path": "admin/build/webpack.base.conf.js",
    "chars": 2484,
    "preview": "var path = require('path')\nvar config = require('../config')\nvar utils = require('./utils')\nvar projectRoot = path.resol"
  },
  {
    "path": "admin/build/webpack.dev.conf.js",
    "chars": 1143,
    "preview": "var config = require('../config')\nvar webpack = require('webpack')\nvar merge = require('webpack-merge')\nvar utils = requ"
  },
  {
    "path": "admin/build/webpack.prod.conf.js",
    "chars": 3250,
    "preview": "var path = require('path')\nvar config = require('../config')\nvar utils = require('./utils')\nvar webpack = require('webpa"
  },
  {
    "path": "admin/config/dev.env.js",
    "chars": 139,
    "preview": "var merge = require('webpack-merge')\nvar prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEnv, {\n  NODE_ENV: "
  },
  {
    "path": "admin/config/index.js",
    "chars": 1316,
    "preview": "// see http://vuejs-templates.github.io/webpack for documentation.\nvar path = require('path')\n\nmodule.exports = {\n  buil"
  },
  {
    "path": "admin/config/prod.env.js",
    "chars": 48,
    "preview": "module.exports = {\n  NODE_ENV: '\"production\"'\n}\n"
  },
  {
    "path": "admin/config/test.env.js",
    "chars": 132,
    "preview": "var merge = require('webpack-merge')\nvar devEnv = require('./dev.env')\n\nmodule.exports = merge(devEnv, {\n  NODE_ENV: '\"t"
  },
  {
    "path": "admin/index.html",
    "chars": 192,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>back</title>\n  </head>\n  <body>\n    <div id=\"app\">"
  },
  {
    "path": "admin/package.json",
    "chars": 2462,
    "preview": "{\n  \"name\": \"admin\",\n  \"version\": \"1.0.0\",\n  \"description\": \"admin spa for blog\",\n  \"author\": \"Smallpath <smallpath2013@"
  },
  {
    "path": "admin/src/App.vue",
    "chars": 1593,
    "preview": "<template>\n  <div id=\"app\">\n    <keep-alive>\n        <router-view></router-view>\n    </keep-alive>\n  </div>\n</template>\n"
  },
  {
    "path": "admin/src/components/Main.vue",
    "chars": 842,
    "preview": "<template>\n  <div class=\"dashboard\">\n    <top></top>\n    <sidebar></sidebar>\n    <div class=\"main\">\n      <router-view :"
  },
  {
    "path": "admin/src/components/containers/Create.vue",
    "chars": 4206,
    "preview": "<template>\n  <el-form ref=\"form\" :model=\"form\" label-width=\"120px\">\n    <el-form-item v-for=\"item in options.items\" :lab"
  },
  {
    "path": "admin/src/components/containers/List.vue",
    "chars": 2926,
    "preview": "<template>\n  <el-table\n    :data=\"list\"\n    v-loading.body=\"isLoading\"\n    border\n    style=\"width: 100%\">\n    <el-table"
  },
  {
    "path": "admin/src/components/containers/Markdown.vue",
    "chars": 9203,
    "preview": "<template>\n  <div class=\"md-panel\">\n    <el-menu default-active=\"1\" class=\"el-menu-demo\" mode=\"horizontal\" @select=\"hand"
  },
  {
    "path": "admin/src/components/containers/Post.vue",
    "chars": 8507,
    "preview": "<template>\n  <el-form ref=\"form\" v-loading.body=\"isLoading\" :model=\"form\" label-width=\"80px\">\n    <el-row :gutter=\"0\">\n "
  },
  {
    "path": "admin/src/components/pages/Dashboard.vue",
    "chars": 2385,
    "preview": "<template>\n  <div class=\"dashboard\">\n    <el-row>\n      <el-col :span=\"24\">\n        <div class=\"info\">\n          <h1>网站概"
  },
  {
    "path": "admin/src/components/pages/Login.vue",
    "chars": 2274,
    "preview": "<template>\n  <div class=\"login\">\n    <el-row type=\"flex\" class=\"row-bg\" justify=\"center\" align=\"middle\">\n      <el-col :"
  },
  {
    "path": "admin/src/components/pages/Logout.vue",
    "chars": 620,
    "preview": "<template>\n</template>\n\n<script>\nimport Api from '../../store/api'\n\nexport default {\n  methods: {\n    logout() {\n      A"
  },
  {
    "path": "admin/src/components/pages/Sidebar.vue",
    "chars": 2843,
    "preview": "<template>\n  <el-row class=\"sidebar\">\n    <el-col :span=\"4\" class=\"full-height\" style=\"overflow-y:auto;\">\n      <el-menu"
  },
  {
    "path": "admin/src/components/pages/Top.vue",
    "chars": 1549,
    "preview": "<template>\n  <div class=\"top\">\n    <el-row class=\"tac\">\n      <el-col :span=\"24\">\n          \n        <el-menu default-ac"
  },
  {
    "path": "admin/src/components/utils/marked.js",
    "chars": 773,
    "preview": "import Marked from 'marked'\nimport hljs from 'highlight.js'\n\nconst renderer = new Marked.Renderer()\nexport const toc = ["
  },
  {
    "path": "admin/src/components/views/CreateEditView.js",
    "chars": 294,
    "preview": "import Item from '../containers/Create.vue'\n\nexport default function(options) {\n  return {\n    name: `${options.name}-cr"
  },
  {
    "path": "admin/src/components/views/CreateListView.js",
    "chars": 290,
    "preview": "import Item from '../containers/List.vue'\n\nexport default function(options) {\n  return {\n    name: `${options.name}-list"
  },
  {
    "path": "admin/src/components/views/CreateMarkdownView.js",
    "chars": 294,
    "preview": "import Item from '../containers/Post.vue'\n\nexport default function(options) {\n  return {\n    name: `${options.name}-mark"
  },
  {
    "path": "admin/src/main.js",
    "chars": 403,
    "preview": "/* eslint-disable */\nimport Vue from 'vue'\n\nimport router from './route/index'\nimport { sync } from 'vuex-router-sync'\ni"
  },
  {
    "path": "admin/src/route/index.js",
    "chars": 15335,
    "preview": "import Vue from 'vue'\nimport VueRouter from 'vue-router'\nVue.use(VueRouter)\n\nimport createListView from '../components/v"
  },
  {
    "path": "admin/src/store/api.js",
    "chars": 1403,
    "preview": "import request from 'axios'\n\nconst root = `/proxyPrefix/api`\n\nconst store = {}\n\nexport default store\n\nstore.request = re"
  },
  {
    "path": "admin/src/store/index.js",
    "chars": 3744,
    "preview": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport api from './api'\n\nVue.use(Vuex)\n\nconst store = new Vuex.Store({\n  s"
  },
  {
    "path": "admin/src/utils/error.js",
    "chars": 392,
    "preview": "export const getChineseDesc = (desc) => {\n  switch (desc) {\n    case 'Token not found':\n      return '请求失败,请确认您已登陆'\n    "
  },
  {
    "path": "admin/static/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "admin/test/e2e/custom-assertions/elementCount.js",
    "chars": 785,
    "preview": "// A custom Nightwatch assertion.\n// the name of the method is the filename.\n// can be used in tests like this:\n//\n//   "
  },
  {
    "path": "admin/test/e2e/nightwatch.conf.js",
    "chars": 1119,
    "preview": "require('babel-register')\nvar config = require('../../config')\n\n// http://nightwatchjs.org/guide#settings-file\nmodule.ex"
  },
  {
    "path": "admin/test/e2e/runner.js",
    "chars": 1022,
    "preview": "// 1. start the dev server using production config\nprocess.env.NODE_ENV = 'testing';\nvar server = require('../../build/d"
  },
  {
    "path": "admin/test/e2e/specs/test.js",
    "chars": 565,
    "preview": "// For authoring Nightwatch tests, see\n// http://nightwatchjs.org/guide#usage\n\nmodule.exports = {\n  'default e2e tests':"
  },
  {
    "path": "admin/test/unit/.eslintrc",
    "chars": 95,
    "preview": "{\n  \"env\": {\n    \"mocha\": true\n  },\n  \"globals\": {\n    \"expect\": true,\n    \"sinon\": true\n  }\n}\n"
  },
  {
    "path": "admin/test/unit/index.js",
    "chars": 561,
    "preview": "// Polyfill fn.bind() for PhantomJS\n/* eslint-disable no-extend-native */\nFunction.prototype.bind = require('function-bi"
  },
  {
    "path": "admin/test/unit/karma.conf.js",
    "chars": 2074,
    "preview": "// This is a karma config file. For more details see\n//   http://karma-runner.github.io/0.13/config/configuration-file.h"
  },
  {
    "path": "admin/test/unit/specs/Hello.spec.js",
    "chars": 356,
    "preview": "import Vue from 'vue'\nimport Hello from 'src/components/Hello'\n\ndescribe('Hello.vue', () => {\n  it('should render correc"
  },
  {
    "path": "docs/.nojekyll",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/README.md",
    "chars": 6539,
    "preview": "# Blog\nA blog system. Based on Vue2, Koa2, MongoDB and Redis\n\n前后端分离 + 服务端渲染的博客系统, 前端 SPA + 后端 RESTful 服务器\n\n# Demo\n前端:[ht"
  },
  {
    "path": "docs/index.html",
    "chars": 531,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Document</title>\n  <meta name=\"description\" co"
  },
  {
    "path": "front/.babelrc",
    "chars": 225,
    "preview": "{\n  \"presets\": [\n    [\"latest\", {\n      \"es2015\": {\n        \"loose\": true,\n        \"modules\": false\n      }\n    }]\n  ],\n"
  },
  {
    "path": "front/.eslintignore",
    "chars": 23,
    "preview": "build/*.js\nconfig/*.js\n"
  },
  {
    "path": "front/.eslintrc.js",
    "chars": 614,
    "preview": "module.exports = {\n  root: true,\n  parser: 'babel-eslint',\n  parserOptions: {\n    sourceType: 'module'\n  },\n  // https:/"
  },
  {
    "path": "front/.gitignore",
    "chars": 137,
    "preview": ".DS_Store\nnode_modules/\ndist/\nnpm-debug.log\nselenium-debug.log\ntest/unit/coverage\ntest/e2e/reports\nstatic/\nserver/mongo."
  },
  {
    "path": "front/README.md",
    "chars": 1496,
    "preview": "## front\n\n> 博客的前台单页, 支持服务端渲染\n\n复制server文件夹中的默认配置`mongo.tpl`, 并命名为`mongo.js`\n\n``` bash\nnpm install\n# 测试环境\nnpm run dev\n# 默认"
  },
  {
    "path": "front/build/build-client.js",
    "chars": 819,
    "preview": "// https://github.com/shelljs/shelljs\nrequire('shelljs/global')\nenv.NODE_ENV = 'production'\n\nvar path = require('path')\n"
  },
  {
    "path": "front/build/setup-dev-server.js",
    "chars": 2089,
    "preview": "const path = require('path')\nconst webpack = require('webpack')\nconst MFS = require('memory-fs')\nconst proxyTable = requ"
  },
  {
    "path": "front/build/utils.js",
    "chars": 1766,
    "preview": "var path = require('path')\nvar config = require('../config')\nvar ExtractTextPlugin = require('extract-text-webpack-plugi"
  },
  {
    "path": "front/build/vue-loader.config.js",
    "chars": 140,
    "preview": "module.exports = {\n  preserveWhitespace: false,\n  postcss: [\n    require('autoprefixer')({\n      browsers: ['last 3 vers"
  },
  {
    "path": "front/build/webpack.base.config.js",
    "chars": 1228,
    "preview": "var config = require('../config')\nvar vueConfig = require('./vue-loader.config')\n\nmodule.exports = {\n  entry: {\n    app:"
  },
  {
    "path": "front/build/webpack.client.config.js",
    "chars": 2887,
    "preview": "const webpack = require('webpack')\nconst base = require('./webpack.base.config')\nconst vueConfig = require('./vue-loader"
  },
  {
    "path": "front/build/webpack.server.config.js",
    "chars": 962,
    "preview": "const webpack = require('webpack')\nconst base = require('./webpack.base.config')\nconst VueSSRPlugin = require('vue-ssr-w"
  },
  {
    "path": "front/config/dev.env.js",
    "chars": 139,
    "preview": "var merge = require('webpack-merge')\nvar prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEnv, {\n  NODE_ENV: "
  },
  {
    "path": "front/config/index.js",
    "chars": 1323,
    "preview": "// see http://vuejs-templates.github.io/webpack for documentation.\nvar path = require('path')\n\nmodule.exports = {\n  buil"
  },
  {
    "path": "front/config/prod.env.js",
    "chars": 48,
    "preview": "module.exports = {\n  NODE_ENV: '\"production\"'\n}\n"
  },
  {
    "path": "front/config/test.env.js",
    "chars": 132,
    "preview": "var merge = require('webpack-merge')\nvar devEnv = require('./dev.env')\n\nmodule.exports = merge(devEnv, {\n  NODE_ENV: '\"t"
  },
  {
    "path": "front/middleware/favicon.js",
    "chars": 604,
    "preview": "const fs = require('fs')\nconst serveFavicon = require('serve-favicon')\nconst log = require('log4js').getLogger('favicon'"
  },
  {
    "path": "front/middleware/serverGoogleAnalytic.js",
    "chars": 2029,
    "preview": "const log = require('log4js').getLogger('google analytic')\n\nconst config = require('../server/config')\nconst request = r"
  },
  {
    "path": "front/package.json",
    "chars": 3022,
    "preview": "{\n  \"name\": \"vue_client_side\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A Vue.js project\",\n  \"author\": \"Smallpath <smallp"
  },
  {
    "path": "front/production.js",
    "chars": 59,
    "preview": "process.env.NODE_ENV = 'production'\nrequire('./server.js')\n"
  },
  {
    "path": "front/server/config.js",
    "chars": 2169,
    "preview": "const isProd = process.env.NODE_ENV === 'production'\nconst request = require('./server-axios')\nconst { ssrPort, serverPo"
  },
  {
    "path": "front/server/model.js",
    "chars": 1667,
    "preview": "const config = require('./mongo.js')\n\nconst mongoose = require('mongoose')\nconst log = require('log4js').getLogger('ssr "
  },
  {
    "path": "front/server/mongo.tpl",
    "chars": 421,
    "preview": "const env = process.env\n\nmodule.exports = {\n  ssrPort: env.ssrPort || 8080,\n  \n  mongoHost: env.mongoHost || '127.0.0.1'"
  },
  {
    "path": "front/server/robots.js",
    "chars": 233,
    "preview": "module.exports = (config) =>\n`User-agent: *\nAllow: /\nSitemap: ${config.siteUrl}/sitemap.xml\n\nUser-agent: YisouSpider\nDis"
  },
  {
    "path": "front/server/rss.js",
    "chars": 1654,
    "preview": "let getUpdatedDate = date => `    <lastBuildDate>${date}</lastBuildDate>\\r\\n`\nlet tail = `  </channel>\n</rss>`\n\nlet api "
  },
  {
    "path": "front/server/server-axios.js",
    "chars": 1097,
    "preview": "const models = require('./model')\nmodule.exports = process.__API__ = {\n  get: function(target, options = {}) {\n    const"
  },
  {
    "path": "front/server/sitemap.js",
    "chars": 817,
    "preview": "let head = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\r\\n`\n\nle"
  },
  {
    "path": "front/server.js",
    "chars": 6767,
    "preview": "const isProd = process.env.NODE_ENV === 'production'\n\nconst log = require('log4js').getLogger('ssr server')\nconst fs = r"
  },
  {
    "path": "front/src/assets/css/article.css",
    "chars": 4994,
    "preview": "#toc {\n  float: right;\n  border: 1px solid #e2e2e2;\n  font-size: 14px;\n  margin: 0 0 15px 20px;\n  max-width: 260px;\n  mi"
  },
  {
    "path": "front/src/assets/css/base.css",
    "chars": 1159,
    "preview": "body {\n  color: #666;\n  font-family: \"Helvetica Neue\", Arial, \"Hiragino Sans GB\", STHeiti, \"Microsoft YaHei\";\n  -webkit-"
  },
  {
    "path": "front/src/assets/css/footer.css",
    "chars": 152,
    "preview": "#footer {\n  border-top: 1px solid #fff;\n  font-size: .9em;\n  line-height: 1.8;\n  padding: 15px;\n  text-align: center\n}\n\n"
  },
  {
    "path": "front/src/assets/css/header.css",
    "chars": 1373,
    "preview": "#header {\n  display: none;\n  background-color: #323436;\n  height: 50px;\n  left: 0;\n  line-height: 50px;\n  overflow: hidd"
  },
  {
    "path": "front/src/assets/css/highlight.css",
    "chars": 1351,
    "preview": "/*\n\nAtom One Dark by Daniel Gamage\nOriginal One Dark Syntax theme from https://github.com/atom/one-dark-syntax\n\nbase:   "
  },
  {
    "path": "front/src/assets/css/icon.css",
    "chars": 1281,
    "preview": "@font-face {\n  font-family: iconfont;\n  src: url(../font/iconfont.eot);\n  src: url(../font/iconfont.eot?#iefix) format(\""
  },
  {
    "path": "front/src/assets/css/pagination.css",
    "chars": 326,
    "preview": ".pagination {\n  border-top: 1px solid #fff;\n  border-bottom: 1px solid #ddd;\n  line-height: 20px;\n  overflow: hidden;\n  "
  },
  {
    "path": "front/src/assets/css/responsive.css",
    "chars": 2352,
    "preview": "@media screen and (max-width:768px) {\n  a.anchor {\n    top: -50px;\n  }\n  #toc {\n    margin: 0;\n    max-width: 100%;\n    "
  },
  {
    "path": "front/src/assets/css/sidebar.css",
    "chars": 1591,
    "preview": "#sidebar {\n  background-color: #202020;\n  height: 100%;\n  left: 0;\n  overflow: auto;\n  -webkit-overflow-scrolling: touch"
  },
  {
    "path": "front/src/assets/js/base.js",
    "chars": 4570,
    "preview": "/* eslint-disable */\nexport default function () {\n  if (typeof window !== 'undefined') {\n\n    (function (win, doc) {\n   "
  },
  {
    "path": "front/src/client-entry.js",
    "chars": 3732,
    "preview": "import Vue from 'vue'\nimport createApp from './main'\nconst { app, appOption, router, store, isProd, preFetchComponent } "
  },
  {
    "path": "front/src/components/App.vue",
    "chars": 516,
    "preview": "<template>\n  <div id=\"app\">\n    <loading-bar :progress=\"progress\"></loading-bar>\n    <sidebar></sidebar>\n    <my-header>"
  },
  {
    "path": "front/src/components/Archive.vue",
    "chars": 1359,
    "preview": "<template>\n\t<div id='main'>\n\t\t<article class=\"post archive\">\n\t\t\t<h1 class=title>{{title}}</h1>\n\t\t\t<div class=\"entry-cont"
  },
  {
    "path": "front/src/components/BlogPager.vue",
    "chars": 1514,
    "preview": "<template>\n  <div id='main'>\n    <section id=\"page-index\">\n      <blog-summary v-for=\"item in items\" :key=\"item.pathName"
  },
  {
    "path": "front/src/components/BlogSummary.vue",
    "chars": 915,
    "preview": "<template>\n  <article class=\"post\">\n    <div class=\"meta\">\n      <div class=\"date\">{{ article.createdAt }}</div>\n    </d"
  },
  {
    "path": "front/src/components/Disqus.vue",
    "chars": 1222,
    "preview": "<template>\n\t<div id=\"disqus_thread\"></div>\n</template>\n\n<script>\n  export default {\n    props: {\n      shortname: {\n    "
  },
  {
    "path": "front/src/components/Footer.vue",
    "chars": 747,
    "preview": "<template>\n  <footer id=\"footer\" class=\"inner\">\n    &copy; 2016&nbsp;-&nbsp; {{ siteInfo.title.value }} &nbsp;-&nbsp;\n  "
  },
  {
    "path": "front/src/components/Header.vue",
    "chars": 564,
    "preview": "<template>\n  <div>\n    <div id=\"header\" :style=\"{ \n        'background-image': sidebarUrl \n        ? 'url(' + sidebarUrl"
  },
  {
    "path": "front/src/components/Loading.vue",
    "chars": 1994,
    "preview": "<template>\n  <div>\n      <div v-if=\"show\" class=\"loading-bar loading-bar--to_right\"\n            :class=\"{ 'loading-bar--"
  },
  {
    "path": "front/src/components/Pagination.vue",
    "chars": 490,
    "preview": "<template>\n  <nav class=\"pagination\">\n    <router-link v-if=\"page > 1\" :to=\"{ query: { page: parseInt(page) - 1} }\" clas"
  },
  {
    "path": "front/src/components/Post.vue",
    "chars": 2729,
    "preview": "<template>\n  <div id='main'>\n    <div id=\"page-post\">\n      <article class=\"post detail\">\n        <div class=\"meta\">\n   "
  },
  {
    "path": "front/src/components/Sidebar.vue",
    "chars": 3774,
    "preview": "<template>\n  <nav id=sidebar class=behavior_1 \n          :class=\"{'sidebar-image': sidebarUrl !== ''}\"\n          :style="
  },
  {
    "path": "front/src/components/Tag.vue",
    "chars": 1316,
    "preview": "<template>\n  <div id='main'>\n    <article class=\"post tags\">\n      <h1 class=title>{{title}}</h1>\n      <div class=\"entr"
  },
  {
    "path": "front/src/components/TagPager.vue",
    "chars": 1413,
    "preview": "<template>\n  <div id='main'>\n    <section id=\"page-index\">\n      <h1 class=\"intro\">标签<a href=\"javascript:void(0)\">{{$rou"
  },
  {
    "path": "front/src/index.template.html",
    "chars": 154,
    "preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\" data-vue-meta-server-rendered>\n  <head>\n    <title></title>\n  </head>\n  <body>\n    <d"
  },
  {
    "path": "front/src/main.js",
    "chars": 649,
    "preview": "import Vue from 'vue'\nimport createRouter from './route'\nimport createStore from './store/index'\nimport { sync } from 'v"
  },
  {
    "path": "front/src/mixin/disqus.js",
    "chars": 573,
    "preview": "const TYPES = ['post', 'page']\n\nexport default {\n  watch: {\n    '$route': 'resetDisqus'\n  },\n  methods: {\n    reset(dsq)"
  },
  {
    "path": "front/src/mixin/image.js",
    "chars": 537,
    "preview": "import { mapGetters } from '../store/vuex'\n\nexport default {\n  computed: {\n    ...mapGetters([\n      'option',\n      'si"
  },
  {
    "path": "front/src/route/create-route-client.js",
    "chars": 683,
    "preview": "const CreatePostView = type => resolve => {\n  return import(/* webpackChunkName: \"CreatePostView\" */ '../views/CreatePos"
  },
  {
    "path": "front/src/route/create-route-server.js",
    "chars": 544,
    "preview": "export const CreatePostView = require('../views/CreatePostView')\nexport const TagPager = require('../components/TagPager"
  },
  {
    "path": "front/src/route/index.js",
    "chars": 1327,
    "preview": "import Vue from 'vue'\nimport VueRouter from 'vue-router'\nimport VueMeta from 'vue-meta'\n\nVue.use(VueRouter)\nVue.use(VueM"
  },
  {
    "path": "front/src/server-entry.js",
    "chars": 1013,
    "preview": "import Vue from 'vue'\nimport createApp from './main'\n\nexport default context => {\n  const { app, appOption, router, stor"
  },
  {
    "path": "front/src/store/api.js",
    "chars": 306,
    "preview": "import api from 'create-api'\n\nconst prefix = `${api.host}/api`\n\nconst store = {}\n\nexport default store\n\nstore.fetch = (m"
  },
  {
    "path": "front/src/store/client-axios.js",
    "chars": 1029,
    "preview": "function get(url, cb) {\n  let xhr\n  try {\n    xhr = new window.XMLHttpRequest()\n  } catch (e) {\n    try {\n      xhr = ne"
  },
  {
    "path": "front/src/store/create-api-client.js",
    "chars": 94,
    "preview": "import axios from './client-axios'\n\nexport default {\n  host: '/proxyPrefix',\n  axios: axios\n}\n"
  },
  {
    "path": "front/src/store/create-api-server.js",
    "chars": 178,
    "preview": "const isProd = process.env.NODE_ENV === 'production'\n\nexport default {\n  host: isProd ? 'http://localhost:3000' : 'http:"
  },
  {
    "path": "front/src/store/index.js",
    "chars": 8050,
    "preview": "import Vue from 'vue'\nimport Vuex from './vuex'\nimport api from './api'\n\nVue.use(Vuex)\n\nexport default function createSt"
  },
  {
    "path": "front/src/store/vuex.js",
    "chars": 2414,
    "preview": "const global = typeof window !== 'undefined' ? window : process\n\nclass Store {\n  constructor({\n    state,\n    actions,\n "
  },
  {
    "path": "front/src/utils/404.js",
    "chars": 360,
    "preview": "module.exports = {\n  pathName: 404,\n  createdAt: '2017-01-01 00:00:00',\n  updatedAt: '2017-01-01 00:00:00',\n  title: '40"
  },
  {
    "path": "front/src/utils/clientGoogleAnalyse.js",
    "chars": 653,
    "preview": "export default function(fullPath) {\n  let screen = window.screen\n  let params = {\n    dt: document.title,\n    dr: fullPa"
  },
  {
    "path": "front/src/views/CreatePostView.js",
    "chars": 1760,
    "preview": "const vuex = require('../store/vuex')\nconst { mapGetters } = vuex\nconst Post = require('../components/Post')\nconst mock4"
  },
  {
    "path": "front/test/e2e/custom-assertions/elementCount.js",
    "chars": 777,
    "preview": "// A custom Nightwatch assertion.\n// the name of the method is the filename.\n// can be used in tests like this:\n//\n//   "
  },
  {
    "path": "front/test/e2e/nightwatch.conf.js",
    "chars": 943,
    "preview": "// http://nightwatchjs.org/guide#settings-file\nmodule.exports = {\n  \"src_folders\": [\"test/e2e/specs\"],\n  \"output_folder\""
  },
  {
    "path": "front/test/e2e/runner.js",
    "chars": 1009,
    "preview": "// 1. start the dev server using production config\nprocess.env.NODE_ENV = 'testing'\nvar server = require('../../build/de"
  },
  {
    "path": "front/test/e2e/specs/test.js",
    "chars": 371,
    "preview": "// For authoring Nightwatch tests, see\n// http://nightwatchjs.org/guide#usage\n\nmodule.exports = {\n  'default e2e tests':"
  },
  {
    "path": "front/test/unit/.eslintrc",
    "chars": 95,
    "preview": "{\n  \"env\": {\n    \"mocha\": true\n  },\n  \"globals\": {\n    \"expect\": true,\n    \"sinon\": true\n  }\n}\n"
  },
  {
    "path": "front/test/unit/index.js",
    "chars": 552,
    "preview": "// Polyfill fn.bind() for PhantomJS\n/* eslint-disable no-extend-native */\nFunction.prototype.bind = require('function-bi"
  },
  {
    "path": "front/test/unit/karma.conf.js",
    "chars": 2055,
    "preview": "// This is a karma config file. For more details see\n//   http://karma-runner.github.io/0.13/config/configuration-file.h"
  },
  {
    "path": "front/test/unit/specs/Hello.spec.js",
    "chars": 349,
    "preview": "import Vue from 'vue'\nimport Hello from 'src/components/Hello'\n\ndescribe('Hello.vue', () => {\n  it('should render correc"
  },
  {
    "path": "pm2.json",
    "chars": 517,
    "preview": "{\n  \"apps\": [{\n    \"name\": \"backend\",\n    \"script\": \"server/entry.js\",\n    \"cwd\": \"\",\n    \"exec_mode\": \"cluster\",\n    \"i"
  },
  {
    "path": "server/.eslintignore",
    "chars": 12,
    "preview": "config/*.js\n"
  },
  {
    "path": "server/.eslintrc.js",
    "chars": 674,
    "preview": "module.exports = {\n  root: true,\n  parser: 'babel-eslint',\n  parserOptions: {\n    sourceType: 'module'\n  },\n  // https:/"
  },
  {
    "path": "server/.gitignore",
    "chars": 88,
    "preview": "node_modules/*\n\nplay.bat\n\nnpm-debug.log\n\n# OSX\n#\n.DS_Store\nconf/config.js\n\n.editorconfig"
  },
  {
    "path": "server/README.md",
    "chars": 564,
    "preview": "# server\n\n> 博客的提供RESTful API的后端\n\n## 设置配置文件\n\n复制conf文件夹中的默认配置`config.tpl`, 并命名为`config.js`\n\n有如下属性可以自行配置:\n\n- `tokenSecret`\n"
  },
  {
    "path": "server/app.js",
    "chars": 1854,
    "preview": "global.Promise = require('bluebird')\n\nconst log = require('./utils/log')\nconst Koa = require('koa')\nconst koaRouter = re"
  },
  {
    "path": "server/blogpack.js",
    "chars": 1312,
    "preview": "\nclass blogpack {\n  constructor(options) {\n    this.config = options.config || {}\n    this.plugins = options.plugins || "
  },
  {
    "path": "server/build/blogpack.base.config.js",
    "chars": 85,
    "preview": "var config = require('../conf/config')\n\nmodule.exports = {\n  config,\n  plugins: []\n}\n"
  },
  {
    "path": "server/build/blogpack.dev.config.js",
    "chars": 1486,
    "preview": "const base = require('./blogpack.base.config')\nconst useRoutesPrefix = '../plugins/beforeUseRoutes'\nconst serverStartPre"
  },
  {
    "path": "server/build/blogpack.prod.config.js",
    "chars": 473,
    "preview": "const devConfig = require('./blogpack.dev.config')\nconst useRoutesPrefix = '../plugins/beforeUseRoutes'\nconst isTest = p"
  },
  {
    "path": "server/conf/config.tpl",
    "chars": 536,
    "preview": "const env = process.env\n\nmodule.exports = {\n  serverPort: env.serverPort || 3000,\n\n  mongoHost: env.mongoHost || '127.0."
  },
  {
    "path": "server/conf/option.js",
    "chars": 772,
    "preview": "module.exports = [\n  {\n    'key': 'analyzeCode',\n    'value': ''\n  },\n  {\n    'key': 'commentType',\n    'value': 'disqus"
  },
  {
    "path": "server/entry.js",
    "chars": 261,
    "preview": "require('babel-register')({\n  plugins: ['transform-async-to-generator'],\n  ignore: function(filename) {\n    if (filename"
  },
  {
    "path": "server/model/mongo.js",
    "chars": 1772,
    "preview": "let config = require('../conf/config')\nlet mongoose = require('mongoose')\nlet log = require('../utils/log')\n\nmongoose.Pr"
  },
  {
    "path": "server/model/redis.js",
    "chars": 598,
    "preview": "const config = require('../conf/config')\nconst redis = require('redis')\nconst bluebird = require('bluebird')\nconst log ="
  },
  {
    "path": "server/mongoRest/actions.js",
    "chars": 2677,
    "preview": "module.exports = function generateActions(model) {\n  return {\n\n    findAll: async function(ctx, next) {\n      try {\n    "
  },
  {
    "path": "server/mongoRest/index.js",
    "chars": 289,
    "preview": "\nconst generateRoutes = require('./routes')\nconst generateActions = require('./actions')\n\nmodule.exports = (router, mode"
  },
  {
    "path": "server/mongoRest/routes.js",
    "chars": 909,
    "preview": "module.exports = (router, modelName, actions, prefix, {\n  beforeRestfulRoutes,\n  afterRestfulRoutes\n}) => {\n  const mode"
  },
  {
    "path": "server/package.json",
    "chars": 1244,
    "preview": "{\n  \"scripts\": {\n    \"start\": \"node entry.js\",\n    \"lint\": \"eslint --ext .js *.js */*.js\",\n    \"test\": \"cross-env NODE_E"
  },
  {
    "path": "server/plugins/beforeRestful/checkAuth.js",
    "chars": 1165,
    "preview": "const redis = require('../../model/redis')\nconst tokenService = require('../../service/token')\n\nmodule.exports = class {"
  },
  {
    "path": "server/plugins/beforeServerStart/initOption.js",
    "chars": 444,
    "preview": "const log = require('../../utils/log')\nconst options = require('../../conf/option')\nconst models = require('../../model/"
  },
  {
    "path": "server/plugins/beforeServerStart/initUser.js",
    "chars": 754,
    "preview": "const log = require('../../utils/log')\nconst config = require('../../conf/config')\nconst models = require('../../model/m"
  },
  {
    "path": "server/plugins/beforeServerStart/installTheme.js",
    "chars": 684,
    "preview": "const log = require('../../utils/log')\nconst fs = require('fs')\nconst path = require('path')\nconst resolve = file => pat"
  },
  {
    "path": "server/plugins/beforeUseRoutes/bodyParser.js",
    "chars": 138,
    "preview": "const bodyParser = require('koa-bodyparser')\n\nmodule.exports = class {\n  async beforeUseRoutes({ app }) {\n    app.use(bo"
  },
  {
    "path": "server/plugins/beforeUseRoutes/logTime.js",
    "chars": 314,
    "preview": "const log = require('../../utils/log')\n\nmodule.exports = class {\n  async beforeUseRoutes({ app, redis }) {\n    app.use(a"
  },
  {
    "path": "server/plugins/beforeUseRoutes/ratelimit.js",
    "chars": 281,
    "preview": "const ratelimit = require('koa-ratelimit')\n\nmodule.exports = class {\n  constructor(options) {\n    this.options = options"
  },
  {
    "path": "server/plugins/beforeUseRoutes/restc.js",
    "chars": 125,
    "preview": "const restc = require('restc')\n\nmodule.exports = class {\n  async beforeUseRoutes({ app }) {\n    app.use(restc.koa2())\n  "
  },
  {
    "path": "server/plugins/mountingRoute/login.js",
    "chars": 1100,
    "preview": "const redis = require('../../model/redis')\nconst tokenService = require('../../service/token')\nconst { user: model } = r"
  },
  {
    "path": "server/plugins/mountingRoute/logout.js",
    "chars": 902,
    "preview": "const redis = require('../../model/redis')\nconst tokenService = require('../../service/token')\n\nmodule.exports = class {"
  },
  {
    "path": "server/plugins/mountingRoute/qiniu.js",
    "chars": 1472,
    "preview": "const qiniu = require('qiniu')\n\nconst fops = 'imageMogr2/format/webp'\n\nconst policy = (name, fileName, { qiniuBucketName"
  },
  {
    "path": "server/service/token.js",
    "chars": 495,
    "preview": "const jwt = require('jsonwebtoken')\nconst config = require('../conf/config')\n\nlet secret = config.tokenSecret\n\nlet expir"
  },
  {
    "path": "server/test/data/index.js",
    "chars": 48,
    "preview": "\nmodule.exports = {\n  post: require('./post')\n}\n"
  },
  {
    "path": "server/test/data/post.js",
    "chars": 546,
    "preview": "module.exports = {\n  create: {\n    'title': '关于',\n    'markdownContent': 'sswww www\\n\\nhaha',\n    'category': '',\n    's"
  },
  {
    "path": "server/test/index.js",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "server/test/integration/postRestful.js",
    "chars": 7404,
    "preview": "const should = require('should')\nconst chai = require('chai')\nconst chaiAsPromised = require('chai-as-promised')\nchai.us"
  },
  {
    "path": "server/test/unit/postQuery.js",
    "chars": 4775,
    "preview": "const should = require('should')\nconst chai = require('chai')\nconst chaiAsPromised = require('chai-as-promised')\nchai.us"
  },
  {
    "path": "server/theme/firekylin.js",
    "chars": 591,
    "preview": "module.exports = {\n  name: 'firekylin',\n  author: 'github.com/75team/firekylin', // migrated by smallpath\n  option: {\n  "
  },
  {
    "path": "server/utils/log.js",
    "chars": 297,
    "preview": "const log4js = require('log4js')\nconst config = require('../conf/config')\nlet log = log4js.getLogger(config.mongoDatabas"
  }
]

About this extraction

This page contains the full source code of the smallpath/blog GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 169 files (245.6 KB), approximately 71.6k tokens, and a symbol index with 65 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!