master c74f1bf2d3a2 cached
74 files
162.0 KB
53.2k tokens
61 symbols
1 requests
Download .txt
Showing preview only (200K chars total). Download the full file or copy to clipboard to get everything.
Repository: fenixsoft/fenix-bookstore-frontend
Branch: master
Commit: c74f1bf2d3a2
Files: 74
Total size: 162.0 KB

Directory structure:
gitextract__w6xb4oj/

├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── build/
│   ├── build.js
│   ├── check-versions.js
│   ├── qcloud.cdn.refresh.js
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── config/
│   ├── dev.env.js
│   ├── index.js
│   └── prod.env.js
├── index.html
├── package.json
├── src/
│   ├── App.vue
│   ├── api/
│   │   ├── index.js
│   │   ├── local/
│   │   │   ├── encrypt-api.js
│   │   │   ├── option-api.js
│   │   │   └── string-api.js
│   │   ├── mock/
│   │   │   ├── index.js
│   │   │   └── json/
│   │   │       ├── accounts.json
│   │   │       ├── advertisements.json
│   │   │       ├── authorization.json
│   │   │       ├── products.json
│   │   │       ├── settlements.json
│   │   │       └── stockpile.json
│   │   └── remote/
│   │       ├── account-api.js
│   │       ├── authorization-api.js
│   │       ├── constants.js
│   │       ├── payment-api.js
│   │       └── warehouse-api.js
│   ├── assets/
│   │   └── css/
│   │       └── global.css
│   ├── components/
│   │   ├── home/
│   │   │   ├── Copyright.vue
│   │   │   ├── NavigationBar.vue
│   │   │   ├── UserInformation.vue
│   │   │   ├── cart/
│   │   │   │   └── PayStepIndicator.vue
│   │   │   ├── detail/
│   │   │   │   └── Checkstand.vue
│   │   │   ├── main/
│   │   │   │   ├── Cabinet.vue
│   │   │   │   └── Carousel.vue
│   │   │   └── warehouse/
│   │   │       ├── ProductManage.vue
│   │   │       └── StockManage.vue
│   │   └── login/
│   │       ├── LoginForm.vue
│   │       └── RegistrationForm.vue
│   ├── main.js
│   ├── pages/
│   │   ├── Login.vue
│   │   └── home/
│   │       ├── CartPage.vue
│   │       ├── CommentPage.vue
│   │       ├── DetailPage.vue
│   │       ├── MainPage.vue
│   │       ├── PaymentPage.vue
│   │       ├── SettlementPage.vue
│   │       ├── WarehousePage.vue
│   │       └── index.vue
│   ├── plugins/
│   │   └── errorhandler-plugin.js
│   ├── router/
│   │   └── index.js
│   └── store/
│       ├── constant.js
│       ├── index.js
│       └── modules/
│           ├── cart.js
│           ├── notification.js
│           ├── products.js
│           └── user.js
├── static/
│   ├── .gitkeep
│   └── board/
│       ├── gitalk.css
│       └── gitalk.html
└── travis_docker_push.sh

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

================================================
FILE: .babelrc
================================================
{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-vue-jsx", "transform-runtime"]
}


================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true


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


================================================
FILE: .eslintrc.js
================================================
// https://eslint.org/docs/user-guide/configuring

module.exports = {
  root: true,
  parserOptions: {
    parser: 'babel-eslint'
  },
  env: {
    browser: true,
  },
  extends: [
    // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
    // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
    'plugin:vue/essential', 
    // https://github.com/standard/standard/blob/master/docs/RULES-en.md
    'standard'
  ],
  // required to lint *.vue files
  plugins: [
    'vue'
  ],
  // add your custom rules here
  rules: {
    // allow async-await
    'generator-star-spacing': 'off',
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
  }
}


================================================
FILE: .gitignore
================================================
.DS_Store
node_modules/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln


================================================
FILE: .postcssrc.js
================================================
// https://github.com/michael-ciniawsky/postcss-load-config

module.exports = {
  "plugins": {
    "postcss-import": {},
    "postcss-url": {},
    // to edit target browsers: use "browserslist" field in package.json
    "autoprefixer": {}
  }
}


================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
  - lts/*
before_install:
  - export TZ='Asia/Shanghai'
install:
  - npm install
script:
  - npm run build:static-web
  - echo "bookstore.icyfenix.cn" >> dist/CNAME
deploy:
  - provider: pages
    skip_cleanup: true
    local_dir: dist
    github_token: $gh_token
    keep_history: true
    target-branch: gh-pages
    on:
      branch: master
  - provider: script
    keep_history: true
    skip_cleanup: true
    script: bash travis_docker_push.sh
    on:
      branch: master
  - provider: script
    keep_history: true
    skip_cleanup: true
    script: npm run deoply:refresh-cdn
    on:
      branch: master


================================================
FILE: Dockerfile
================================================
FROM nginx:alpine

MAINTAINER icyfenix

WORKDIR /usr/share/nginx/html
COPY dist /usr/share/nginx/html

EXPOSE 80
ENTRYPOINT ["nginx", "-g", "daemon off;"]


================================================
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
================================================
# Fenix's BookStore前端工程

<p align="center">
  <a href="https://icyfenix.cn" target="_blank">
    <img width="180" src="https://raw.githubusercontent.com/fenixsoft/awesome-fenix/master/.vuepress/public/images/logo-color.png" alt="logo">
  </a>
</p>
<p align="center">
    <a href="https://iycfenix.cn"  style="display:inline-block"><img src="https://raw.githubusercontent.com/fenixsoft/awesome-fenix/master/.vuepress/public/images/Release-v1.svg"></a>
  <a href="https://travis-ci.com/fenixsoft/fenix-bookstore-frontend" target="_blank"  style="display:inline-block"><img src="https://api.travis-ci.com/fenixsoft/fenix-bookstore-frontend.svg?branch=master" alt="Travis-CI"></a>
  <a href="https://creativecommons.org/licenses/by/4.0/"  target="_blank" style="display:inline-block"><img src="https://raw.githubusercontent.com/fenixsoft/awesome-fenix/master/.vuepress/public/images/DocLicense-CC-red.svg" alt="Document License"></a>
    <a href="https://www.apache.org/licenses/LICENSE-2.0"  target="_blank" style="display:inline-block"><img src="https://raw.githubusercontent.com/fenixsoft/awesome-fenix/master/.vuepress/public/images/License-Apache.svg" alt="License"></a>
    <a href="http://icyfenix.cn/introduction/about-me.html" target="_blank" style="display:inline-block"><img src="https://raw.githubusercontent.com/fenixsoft/awesome-fenix/master/.vuepress/public/images/Author-IcyFenix-blue.svg" alt="Mail to Author"></a>
</p>

如果你此时并不曾了解过什么是“The Fenix Project”,建议先阅读<a href="https://icyfenix.cn/introduction/about-the-fenix-project.html">这部分内容</a>。

Fenix Project的主要目的是展示不同的后端技术架构,相对而言,前端并非其重点。不过,前端的页面是比起后端各种服务来要直观得多,能让使用者更容易理解我们将要做的是一件什么事情。假设你是一名驾驶初学者,合理的学习路径肯定应该是把汽车发动,然后慢慢行驶起来,而不是马上从“引擎动力原理”、“变速箱构造”入手去设法深刻地了解一台汽车。所以,先来运行程序,看看最终的效果是什么样子吧。

## 运行程序

以下几种途径,可以马上浏览最终的效果:

- 从互联网已部署(由提供Travis-CI支持)的网站(由GitHub Pages提供主机,由腾讯云CDN提供国内加速)访问:

> 直接在浏览器访问:[http://bookstore.icyfenix.cn/](http://bookstore.icyfenix.cn/)

- 通过Docker容器方式运行:

> ```bash
> $ docker run -d -p 80:80 --name bookstore icyfenix/bookstore:frontend 
> ```
>
> 然后在浏览器访问:[http://localhost](http://localhost)

- 通过Git上的源码,以开发模式运行:
>```bash
># 克隆获取源码
> $ git clone https://github.com/fenixsoft/fenix-bookstore-frontend.git
> 
> # 进入工程根目录
> $ cd fenix-bookstore-frontend
> 
> # 安装工程依赖
> $ npm install
> 
> # 以开发模式运行,地址为localhost:8080
> $ npm run dev
> ```
> 
> 然后在浏览器访问:[http://localhost:8080](http://localhost:8080)
>

<GitHubWrapper>
<p align="center">
    <img  src="https://raw.githubusercontent.com/fenixsoft/awesome-fenix/master/.vuepress/public/images/sshot.jpg" >
</p>
</GitHubWrapper>

也许你已注意到,以上这些运行方式,均没有涉及到任何的服务端、数据库的部署。现代软件工程里,基于MVVM的工程结构使得前、后端的开发可以完全分离,只要互相约定好服务的位置及模型即可。Fenix's BookStore以开发模式运行时,会自动使用Mock.js拦截住所有的远程服务请求,并以事项准备好的数据来完成对这些请求的响应。

同时,你也应当注意到,以纯前端方式运行的时候,所有对数据的修改请求实际都是无效的。譬如用户注册,无论你输入何种用户名、密码,由于请求的响应是静态预置的,所以最终都会以同一个预设的用户登陆。也是因此,我并没有提供”默认用户“、”默认密码“一类的信息供用户使用,你可以随意输入即可登陆。

不过,那些只维护在前端的状态依然是可以变动的,典型的如对购物车、收藏夹的增删改。让后端服务保持无状态,而把状态维持在前端中的设计,对服务的伸缩性和系统的鲁棒性都有着极大的益处,多数情况下都是值得倡导的良好设计。而其伴随而来的状态数据导致请求头变大、链路安全性等问题,都会在服务端部分专门讨论和解决。

## 构建产品

当你将程序用于正式部署时,一般不应部署开发阶段的程序,而是要进行产品化(production)与精简化(minification),你可以通过以下命令,由node.js驱动webpack来自动完成:

```bash
# 编译前端代码
$ npm run build
```

或者使用--report参数,同时输出依赖分析报告:

```bash
# 编译前端代码并生成报告
$ npm run build --report
```

编译结果存放在/dist目录中,应将其拷贝至Web服务器的根目录使用。对于Fenix Project的各个服务端而言,则通常是拷贝到网关工程中静态资源目录下。

## 与后端联调

同样出于前后端分离的目的,理论上后端通常只应当依据约定的服务协议(接口定位、访问传输方式、参数及模型结构、服务水平协议等)提供服务,并以此为依据进行不依赖前端的独立测试,最终集成时使用的是编译后的前端产品。

不过,在开发期就进行的前后端联合在现今许多企业之中仍是主流形式,由一个人“全栈式”地开发某个功能时更是如此,因此,当要在开发模式中进行联调时,需要修改项目根目录下的main.js文件,使其**不**导入Mock.js,即如下代码所示的条件语句判断为假

```javascript
/**
 * 默认在开发模式中启用mock.js代替服务端请求
 * 如需要同时调试服务端,请修改此处判断条件
 */
// eslint-disable-next-line no-constant-condition
if (process.env.MOCK) {
  require('./api/mock')
}
```

也有其他一些相反的情况,需要在生产包中仍然继续使用Mock.js提供服务时(譬如Docker镜像icyfenix/bookstore:frontend就是如此),同样应修改该条件,使其结果为真,在开发模式依然导入了Mock.js即可。

## 工程结构

Fenix's BookStore的工程结构完全符合vue.js工程的典型习惯,事实上它在建立时就是通过vue-cli初始化的。此工程的结构与其中各个目录的作用主要如下所示:

```
+---build                           webpack编译配置,该目录的内容一般不做改动
+---config                          webpack编译配置,用户需改动的内容提取至此
+---dist                            编译输出结果存放的位置
+---markdown                        与项目无关,用于支持markdown的资源(如图片)
+---src
|   +---api                         本地与远程的API接口
|   |   +---local                   本地服务,如localStorage、加密等
|   |   +---mock                    远程API接口的Mock
|   |   |   \---json                Mock返回的数据
|   |   \---remote                  远程服务
|   +---assets                      资源文件,会被webpack哈希和压缩
|   +---components                  vue.js的组件目录,按照使用页面的结构放置
|   |   +---home
|   |   |   +---cart
|   |   |   +---detail
|   |   |   \---main
|   |   \---login
|   +---pages                       vue.js的视图目录,存放页面级组件
|   |   \---home
|   +---plugins                     vue.js的插件,如全局异常处理器
|   +---router                      vue-router路由配置
|   \---store                       vuex状态配置
|       \---modules                 vuex状态按名空间分隔存放
\---static                          静态资源,编译时原样打包,不会做哈希和压缩
```

## 组件
Fenix's BookStore前端部分基于以下开源组件和免费资源构建:

- [Vue.js](https://cn.vuejs.org/)<br/>
  渐进式JavaScript框架
- [Element](https://element.eleme.cn/#/zh-CN)<br/>
  一套为开发者、设计师和产品经理准备的基于Vue 2.0的桌面端组件库
- [Axios](https://github.com/axios/axios)<br/>
  Promise based HTTP client for the browser and node.js
- [Mock.js](http://mockjs.com/)<br/>
  生成随机数据,拦截 Ajax 请求
- [DesignEvo](https://www.designevo.com/cn)<br/>
  一款由PearlMountain有限公司设计研发的logo设计软件

## 协议

- 本文档代码部分采用[Apache 2.0协议](https://www.apache.org/licenses/LICENSE-2.0)进行许可。遵循许可的前提下,你可以自由地对代码进行修改,再发布,可以将代码用作商业用途。但要求你:
  - **署名**:在原有代码和衍生代码中,保留原作者署名及代码来源信息。
  - **保留许可证**:在原有代码和衍生代码中,保留Apache 2.0协议文件。
  
- 本作品文档部分采用[知识共享署名 4.0 国际许可协议](http://creativecommons.org/licenses/by/4.0/)进行许可。 遵循许可的前提下,你可以自由地共享,包括在任何媒介上以任何形式复制、发行本作品,亦可以自由地演绎、修改、转换或以本作品为基础进行二次创作。但要求你:
  - **署名**:应在使用本文档的全部或部分内容时候,注明原作者及来源信息。
  - **非商业性使用**:不得用于商业出版或其他任何带有商业性质的行为。如需商业使用,请联系作者。
  - **相同方式共享的条件**:在本文档基础上演绎、修改的作品,应当继续以知识共享署名 4.0国际许可协议进行许可。

================================================
FILE: build/build.js
================================================
'use strict'
require('./check-versions')()

process.env.NODE_ENV = 'production'

const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

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

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log('  Output directory:' + (process.env.npm_config_output_path || '../dist'))
    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})


================================================
FILE: build/check-versions.js
================================================
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')

function exec (cmd) {
  return require('child_process').execSync(cmd).toString().trim()
}

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

if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    currentVersion: exec('npm --version'),
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  const warnings = []

  for (let i = 0; i < versionRequirements.length; i++) {
    const 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 (let i = 0; i < warnings.length; i++) {
      const warning = warnings[i]
      console.log('  ' + warning)
    }

    console.log()
    process.exit(1)
  }
}


================================================
FILE: build/qcloud.cdn.refresh.js
================================================
const qcloudSDK = require('qcloud-cdn-node-sdk')

const userConfig = {
  secretId: process.env.CDN_ID,
  secretKey: process.env.CDN_KEY
}
console.log(userConfig)
qcloudSDK.config(userConfig)

qcloudSDK.request('RefreshCdnDir', {
  'dirs.0': 'http://bookstore.icyfenix.cn'
}, (res) => {
  console.log(res)
})


================================================
FILE: build/utils.js
================================================
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')

exports.assetsPath = function (_path) {
  const assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory

  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function (options) {
  options = options || {}

  const cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  const postcssLoader = {
    loader: 'postcss-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]

    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

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

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  const output = []
  const loaders = exports.cssLoaders(options)

  for (const extension in loaders) {
    const loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

exports.createNotifierCallback = () => {
  const notifier = require('node-notifier')

  return (severity, errors) => {
    if (severity !== 'error') return

    const error = errors[0]
    const filename = error.file && error.file.split('!').pop()

    notifier.notify({
      title: packageConfig.name,
      message: severity + ': ' + error.name,
      subtitle: filename || '',
      icon: path.join(__dirname, 'logo.png')
    })
  }
}


================================================
FILE: build/vue-loader.conf.js
================================================
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
  ? config.build.productionSourceMap
  : config.dev.cssSourceMap

module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: isProduction
  }),
  cssSourceMap: sourceMapEnabled,
  cacheBusting: config.dev.cacheBusting,
  transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
  }
}


================================================
FILE: build/webpack.base.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

const createLintingRule = () => ({
  test: /\.(js|vue)$/,
  loader: 'eslint-loader',
  enforce: 'pre',
  include: [resolve('src'), resolve('test')],
  options: {
    formatter: require('eslint-friendly-formatter'),
    emitWarning: !config.dev.showEslintErrorsInOverlay
  }
})

module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },
  module: {
    rules: [
      ...(config.dev.useEslint ? [createLintingRule()] : []),
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('media/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  node: {
    // prevent webpack from injecting useless setImmediate polyfill because Vue
    // source contains it (although only uses it if it's native).
    setImmediate: false,
    // prevent webpack from injecting mocks to Node native modules
    // that does not make sense for the client
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
  }
}


================================================
FILE: build/webpack.dev.conf.js
================================================
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({sourceMap: config.dev.cssSourceMap, usePostCSS: true})
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        {from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html')},
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? {warnings: false, errors: true}
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      favicon: 'src/assets/favicon.ico',
      inject: true
    }),
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
          ? utils.createNotifierCallback()
          : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})


================================================
FILE: build/webpack.prod.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const env = require('../config/prod.env')

const webpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          warnings: false
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css'),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true,
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
    // 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: config.build.index,
      template: 'index.html',
      favicon: 'src/assets/favicon.ico',
      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'
    }),
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        // 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',
      minChunks: Infinity
    }),
    // This instance extracts shared chunks from code splitted chunks and bundles them
    // in a separate chunk, similar to the vendor chunk
    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),

    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

if (config.build.productionGzip) {
  const 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
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig


================================================
FILE: config/dev.env.js
================================================
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

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


================================================
FILE: config/index.js
================================================
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-

    // Use Eslint Loader?
    // If true, your code will be linted during bundling and
    // linting errors and warnings will be shown in the console.
    useEslint: true,
    // If true, eslint errors and warnings will also be shown in the error overlay
    // in the browser.
    showEslintErrorsInOverlay: false,

    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,

    cssSourceMap: true
  },

  build: {
    // Template for index.html
    index: path.resolve(__dirname, (process.env.npm_config_output_path || '../dist') + '/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, process.env.npm_config_output_path || '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',

    /**
     * Source Maps
     */

    productionSourceMap: false,
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',

    // 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'],

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  }
}


================================================
FILE: config/prod.env.js
================================================
'use strict'
module.exports = {
  NODE_ENV: '"production"',
  // 传入了参数--mock的话,生产模式中也仍然采用Mock.JS
  MOCK: process.argv[2] === '--mock'
}


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Fenix's BookStore</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "bookstore",
  "version": "1.0.0",
  "description": "The Fenix Project Client Demo",
  "author": "icyfenix <icyfenix@gmail.com>",
  "private": true,
  "scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "lint": "eslint --ext .js,.vue src",
    "build": "node build/build.js",
    "build:static-web": "node build/build.js --mock",
    "deoply:refresh-cdn": "node build/qcloud.cdn.refresh.js"
  },
  "dependencies": {
    "@chenfengyuan/vue-qrcode": "^1.0.2",
    "crypto": "^1.0.1",
    "default-passive-events": "^1.0.10",
    "v-distpicker": "^1.2.2",
    "vue": "^2.5.2"
  },
  "devDependencies": {
    "autoprefixer": "^7.1.2",
    "axios": "^0.19.2",
    "babel-core": "^6.22.1",
    "babel-eslint": "^8.2.1",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-loader": "^7.1.1",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-plugin-transform-vue-jsx": "^3.5.0",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-stage-2": "^6.22.0",
    "bcryptjs": "^2.4.3",
    "chalk": "^2.0.1",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^0.28.0",
    "element-ui": "^2.13.0",
    "es6-promise": "^4.2.8",
    "eslint": "^4.15.0",
    "eslint-config-standard": "^10.2.1",
    "eslint-friendly-formatter": "^3.0.0",
    "eslint-loader": "^1.7.1",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-node": "^5.2.0",
    "eslint-plugin-promise": "^3.4.0",
    "eslint-plugin-standard": "^3.0.1",
    "eslint-plugin-vue": "^4.0.0",
    "extract-text-webpack-plugin": "^3.0.0",
    "file-loader": "^1.1.4",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "html-webpack-plugin": "^2.30.1",
    "mockjs": "^1.1.0",
    "node-notifier": "^5.1.2",
    "optimize-css-assets-webpack-plugin": "^3.2.0",
    "ora": "^1.2.0",
    "portfinder": "^1.0.13",
    "postcss-import": "^11.0.0",
    "postcss-loader": "^2.0.8",
    "postcss-url": "^7.2.1",
    "qcloud-cdn-node-sdk": "^1.0.0",
    "rimraf": "^2.6.0",
    "semver": "^5.3.0",
    "shelljs": "^0.7.6",
    "uglifyjs-webpack-plugin": "^1.1.1",
    "url-loader": "^0.5.8",
    "vue-loader": "^13.3.0",
    "vue-router": "^3.1.5",
    "vue-style-loader": "^3.0.1",
    "vue-template-compiler": "^2.5.2",
    "vuex": "^3.1.2",
    "webpack": "^3.6.0",
    "webpack-bundle-analyzer": "^2.9.0",
    "webpack-dev-server": "^2.9.1",
    "webpack-merge": "^4.1.0"
  },
  "engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}


================================================
FILE: src/App.vue
================================================
<template>
  <div id="app">
    <transition name="slide-fade">
      <el-alert title="接收到未处理的异常:" type="error" :description="exception.message" show-icon v-if="exception"
                @close="clearMessage">
      </el-alert>
    </transition>
    <router-view/>
  </div>
</template>

<script>
import {mapState, mapMutations} from 'vuex'

export default {
  name: 'App',
  computed: {
    ...mapState({
      exception: state => state.notification.exception
    })
  },
  methods: {
    ...mapMutations('notification', ['clearException']),
    clearMessage () {
      this.clearException()
    }
  }
}
</script>

<style>
  @import url('./assets/css/global.css');

  #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    color: #2c3e50;
  }
</style>


================================================
FILE: src/api/index.js
================================================
import axios from 'axios'
import constants from './remote/constants'
import warehouse from './remote/warehouse-api'
import option from './local/option-api'
import encrypt from './local/encrypt-api'
import auth from './remote/authorization-api'
import account from './remote/account-api'
import payment from './remote/payment-api'
import stringUtil from './local/string-api'

// 设置默认的HTTP访问参数
axios.defaults.timeout = constants.REMOTE_TIMEOUT
axios.defaults.baseURL = constants.REMOTE_BASE_URL

/**
 * HTTP请求拦截器
 * 如果当前Session持有JWT Token,每次请求时服务端时自动带上该令牌
 */
axios.interceptors.request.use(config => {
  // 如果访问的不是OAuth授权服务Endpoint
  // TODO 这里应该做一些更细致的检查,譬如是否跨域,不应将Token发送到域外地址
  if (config.baseURL !== constants.AUTH_BASE_URL) {
    // 并且Session有效,就自动在请求Header中带上JWT令牌
    if (option.isSessionAvailable()) {
      config.headers = {
        'Authorization': constants.AUTH_TOKEN_TYPE + option.getSession().access_token
      }
    }
  }
  return config
}, error => {
  return Promise.reject(error)
})

/**
 * HTTP响应拦截器
 * 将返回非200状态的HTTP CODE和无响应均调用promise reject
 */
axios.interceptors.response.use(res => {
  if (res.data && res.data.code && res.data.code !== 0) {
    return Promise.reject(Error(res.data.message))
  } else {
    return Promise.resolve(res)
  }
}, error => {
  console.error('远程服务未能成功发送:', error)
  // 尝试提取服务端错误
  const res = error.response
  if (res && res.data) {
    console.error(res.data)
    if (res.data.error && res.data.error_description) {
      // OAuth令牌Endpoint的错误格式:{error:"",error_description:""}
      const message = `HTTP Code:${res.status}, 信息:[${res.data.error}] ${res.data.error_description}`
      return Promise.reject(Error(message))
    } else if (res.data.code && res.data.message) {
      // 服务端CommonResponse的错误格式:{code:xx,message:""}
      return Promise.reject(Error(res.data.message))
    } else if (res.data.error && res.data.message) {
      // Jersey的默认错误格式:{status:xx,timestamp:"",error:"",message:xx}
      const message = `HTTP Code:${res.status}, 信息:[${res.data.error}] ${res.data.message}`
      return Promise.reject(Error(message))
    }
  }
  return Promise.reject(error)
})

export default {
  constants,
  // remote
  warehouse,
  account,
  auth,
  payment,
  // local
  option,
  encrypt,
  stringUtil
}


================================================
FILE: src/api/local/encrypt-api.js
================================================
const crypto = require('crypto')
const bcrypt = require('bcryptjs')

const CLIENT_SALT = '$2a$10$o5L.dWYEjZjaejOmN3x4Qu'

export default {

  /**
   * 默认编码
   * 采用MD5加密,HEX编码,加盐,Bcrypt加密,返回
   *
   * @param source 待加密的字符串原文
   */
  defaultEncode (source) {
    return bcrypt.hashSync(crypto.createHash('md5').update(source).digest('hex'), CLIENT_SALT).substring(CLIENT_SALT.length)
  },

  /**
   * gravatar头像服务中要求的编码
   */
  gravatarEncode (email) {
    const lower = (email || 'default_avatar').toLowerCase()
    const hash = crypto.createHash('md5').update(lower).digest('hex')
    return `https://www.gravatar.com/avatar/${hash}?d=mp`
  }
}


================================================
FILE: src/api/local/option-api.js
================================================
const LOCAL_SESSION_KEY = 'Client-Session'

/**
 * 本地选项
 * 可以持久存在在客户端本地或者Session的设置信息
 * 通过统一的API来对外部屏蔽掉localStorage和sessionStorage的差异
 */
export default {
  setSession (session) {
    if (session.rememberMe) {
      localStorage.setItem(LOCAL_SESSION_KEY, JSON.stringify(session))
    } else {
      sessionStorage.setItem(LOCAL_SESSION_KEY, JSON.stringify(session))
    }
  },

  getSession () {
    // 如果本地缓存中的Session被破坏了(无法进行JSON反序列化),就返回空对象
    try {
      return JSON.parse(sessionStorage.getItem(LOCAL_SESSION_KEY) || localStorage.getItem(LOCAL_SESSION_KEY))
    } catch (e) {
      return {}
    }
  },

  removeSession () {
    sessionStorage.removeItem(LOCAL_SESSION_KEY)
    localStorage.removeItem(LOCAL_SESSION_KEY)
  },

  hasSession () {
    return !!(sessionStorage.getItem(LOCAL_SESSION_KEY) || localStorage.getItem(LOCAL_SESSION_KEY))
  },

  isSessionAvailable () {
    return this.hasSession() && this.getSession().expires > new Date().getTime()
  },

  isSessionRefreshable () {
    return !!this.getSession().refresh_token
  },

  isAdministrator () {
    if (!this.hasSession()) {
      return false
    }
    const authorities = this.getSession().authorities
    return authorities && authorities.includes('ROLE_ADMIN')
  }
}


================================================
FILE: src/api/local/string-api.js
================================================
export default {
  /**
   * 去除HTML标签
   */
  pureText: text => text.replace(/<\/?[^>]*>/g, ''),

  /**
   * 将HTML中的<p></P>转换为回车
   */
  transToReturn: text => text.replace(/<p>/g, '').replace(/<\/p>/g, '\n'),

  /**
   * 将回车转换为<p>
   */
  transToHTML: text => '<p>' + text.replace(/\n*$/g, '').replace(/\n/g, '</p> <p>') + '</p>'
}


================================================
FILE: src/api/mock/index.js
================================================
const MockJS = require('mockjs')

/**
 * Mock的请求不会真正发送,在Network面板看不到,输出日志以便调试使用
 */
const loadJSON = (options, file) => {
  const json = require('./json/' + file)
  console.debug(`REQUEST:${options.type} ${options.url}:`, options)
  console.debug('RESPONSE:', json)
  return json
}

const success = options => {
  const repo = {code: 0}
  console.debug(`REQUEST:${options.type} ${options.url}:`, options)
  console.debug('RESPONSE:', repo)
  return repo
}

const failure = options => {
  const repo = {code: 1, message: '远程调用已正确发出,但静态Mock运行模式并没有服务端支持,此操作未产生效果'}
  console.debug(`REQUEST:${options.type} ${options.url}:`, options)
  console.debug('RESPONSE:', repo)
  return repo
}

/**
 * 被Mock的各个请求
 */
MockJS.mock('/restful/products', 'get', o => loadJSON(o, 'products.json'))
MockJS.mock('/restful/advertisements', 'get', o => loadJSON(o, 'advertisements.json'))
MockJS.mock('/restful/products', 'post', o => failure(o))
MockJS.mock('/restful/products', 'put', o => failure(o))
MockJS.mock(/\/restful\/products\/stockpile\/.*/, 'get', o => loadJSON(o, 'stockpile.json'))
MockJS.mock(/\/restful\/products\/stockpile\/.*/, 'patch', o => failure(o))
MockJS.mock(/\/restful\/products\/.*/, 'get', o => {
  let json = loadJSON(o, 'products.json')
  let id = /\/restful\/products\/(.*)/.exec(o.url)[1]
  return json.find(book => id === book.id.toString())
})
MockJS.mock(/\/oauth\/token.*/, 'get', o => loadJSON(o, 'authorization.json'))
MockJS.mock(/\/restful\/accounts\/.*/, 'get', o => loadJSON(o, 'accounts.json'))
MockJS.mock('/restful/accounts', 'post', o => success(o))
MockJS.mock('/restful/accounts', 'put', o => success(o))
MockJS.mock('/restful/settlements', 'post', o => loadJSON(o, 'settlements.json'))
MockJS.mock(/\/restful\/pay\/.*/, 'patch', o => failure(o))
MockJS.mock(/\/restful\/products\/.*/, 'delete', o => failure(o))


================================================
FILE: src/api/mock/json/accounts.json
================================================
{
  "id": 1,
  "username": "icyfenix",
  "name": "周志明",
  "avatar": "",
  "telephone": "18888888888",
  "email": "icyfenix@gmail.com",
  "location": "唐家湾港湾大道科技一路3号远光软件股份有限公司"
}


================================================
FILE: src/api/mock/json/advertisements.json
================================================
[
  {
    "id": "fenix",
    "image": "/static/carousel/fenix2.png",
    "productId": 8
  },
  {
    "id": "ai",
    "image": "/static/carousel/ai.png",
    "productId": 2
  },
  {
    "id": "jvm",
    "image": "/static/carousel/jvm3.png",
    "productId": 1
  }
]


================================================
FILE: src/api/mock/json/authorization.json
================================================
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJpY3lmZW5peCIsInNjb3BlIjpbIkFMTCJdLCJleHAiOjE1ODQyNTA3MDQsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwianRpIjoiMTNmNGNlMWQtNmY2OC00NzQxLWI5YzYtMzkyNzU1OGQ5NzRlIiwiY2xpZW50X2lkIjoiYm9va3N0b3JlX2Zyb250ZW5kIiwidXNlcm5hbWUiOiJpY3lmZW5peCJ9.82awQU4IcLVXr7w6pxcUCWrcEHKq-LRT7ggPT_ZPhE0",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJpY3lmZW5peCIsInNjb3BlIjpbIkFMTCJdLCJhdGkiOiIxM2Y0Y2UxZC02ZjY4LTQ3NDEtYjljNi0zOTI3NTU4ZDk3NGUiLCJleHAiOjE1ODU1MzU5MDQsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwianRpIjoiY2IwN2ZjZjEtMjViZS00MDRjLTkwNzctY2U5ZTlhZjFjOWEwIiwiY2xpZW50X2lkIjoiYm9va3N0b3JlX2Zyb250ZW5kIiwidXNlcm5hbWUiOiJpY3lmZW5peCJ9.-gNKkhspN1XfVybmS3Rnz2AYFdteZN4kvdEmC4g-aYk",
  "expires_in": 10799,
  "scope": "ALL",
  "authorities": [
    "ROLE_USER",
    "ROLE_ADMIN"
  ],
  "username": "icyfenix",
  "jti": "13f4ce1d-6f68-4741-b9c6-3927558d974e"
}


================================================
FILE: src/api/mock/json/products.json
================================================
[
  {
    "id": 8,
    "title": "凤凰架构:构建可靠的大型分布式系统",
    "price": 0.0,
    "rate": 0.0,
    "description": "<p>这是一部以“如何构建一套可靠的分布式大型软件系统”为叙事主线的开源文档,是一幅帮助开发人员整理现代软件架构各条分支中繁多知识点的技能地图。文章《<a href='https://icyfenix.cn/introduction/about-the-fenix-project.html' target=_blank>什么是“凤凰架构”</a>》详细阐述了这部文档的主旨、目标与名字的来由,文章《<a href='https://icyfenix.cn/exploration/guide/quick-start.html' target=_blank>如何开始</a>》简述了文档每章讨论的主要话题与内容详略分布</p>",
    "cover": "/static/cover/fenix.png",
    "detail": "/static/desc/fenix.jpg",
    "specifications": [
      {
        "id": 64,
        "item": "ISBN",
        "value": "9787111349662"
      },
      {
        "id": 69,
        "item": "装帧",
        "value": "在线"
      },
      {
        "id": 66,
        "item": "页数",
        "value": "409"
      },
      {
        "id": 68,
        "item": "出版年",
        "value": "2020-6"
      },
      {
        "id": 65,
        "item": "书名",
        "value": "凤凰架构"
      },
      {
        "id": 68,
        "item": "副标题",
        "value": "构建可靠的大型分布式系统"
      },
      {
        "id": 63,
        "item": "作者",
        "value": "周志明"
      },
      {
        "id": 67,
        "item": "出版社",
        "value": "机械工业出版社"
      }
    ]
  },
  {
    "id": 1,
    "title": "深入理解Java虚拟机(第3版)",
    "price": 129.0,
    "rate": 9.6,
    "description": "<p>这是一部从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典,繁体版在台湾也颇受欢迎。</p><p>自2011年上市以来,前两个版本累计印刷36次,销量超过30万册,两家主要网络书店的评论近90000条,内容上近乎零差评,是原创计算机图书领域不可逾越的丰碑,第3版在第2版的基础上做了重大修订,内容更丰富、实战性更强:根据新版JDK对内容进行了全方位的修订和升级,围绕新技术和生产实践新增逾10万字,包含近50%的全新内容,并对第2版中含糊、瑕疵和错误内容进行了修正。</p><p>全书一共13章,分为五大部分:</p><p>第一部分(第1章)走近Java</p><p>系统介绍了Java的技术体系、发展历程、虚拟机家族,以及动手编译JDK,了解这部分内容能对学习JVM提供良好的指引。</p><p>第二部分(第2~5章)自动内存管理</p><p>详细讲解了Java的内存区域与内存溢出、垃圾收集器与内存分配策略、虚拟机性能监控与故障排除等与自动内存管理相关的内容,以及10余个经典的性能优化案例和优化方法;</p><p>第三部分(第6~9章)虚拟机执行子系统</p><p>深入分析了虚拟机执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎,以及多个类加载及其执行子系统的实战案例;</p><p>第四部分(第10~11章)程序编译与代码优化</p><p>详细讲解了程序的前、后端编译与优化,包括前端的易用性优化措施,如泛型、主动装箱拆箱、条件编译等的内容的深入分析;以及后端的性能优化措施,如虚拟机的热点探测方法、HotSpot 的即时编译器、提前编译器,以及各种常见的编译期优化技术;</p><p>第五部分(第12~13章)高效并发</p><p>主要讲解了Java实现高并发的原理,包括Java的内存模型、线程与协程,以及线程安全和锁优化。</p><p>全书以实战为导向,通过大量与实际生产环境相结合的案例分析和展示了解决各种Java技术难题的方案和技巧。</p>",
    "cover": "/static/cover/jvm3.jpg",
    "detail": "/static/desc/jvm3.jpg",
    "specifications": [
      {
        "id": 9,
        "item": "装帧",
        "value": "平装"
      },
      {
        "id": 7,
        "item": "出版社",
        "value": "机械工业出版社"
      },
      {
        "id": 2,
        "item": "副标题",
        "value": "JVM高级特性与最佳实践"
      },
      {
        "id": 3,
        "item": "ISBN",
        "value": "9787111641247"
      },
      {
        "id": 4,
        "item": "书名",
        "value": "深入理解Java虚拟机(第3版)"
      },
      {
        "id": 5,
        "item": "页数",
        "value": "540"
      },
      {
        "id": 6,
        "item": "丛书",
        "value": "华章原创精品"
      },
      {
        "id": 8,
        "item": "出版年",
        "value": "2019-12"
      },
      {
        "id": 1,
        "item": "作者",
        "value": "周志明"
      }
    ]
  },
  {
    "id": 2,
    "title": "智慧的疆界",
    "price": 69.0,
    "rate": 9.1,
    "description": "<p>这是一部对人工智能充满敬畏之心的匠心之作,由《深入理解Java虚拟机》作者耗时一年完成,它将带你从奠基人物、历史事件、学术理论、研究成果、技术应用等5个维度全面读懂人工智能。</p><p>本书以时间为主线,用专业的知识、通俗的语言、巧妙的内容组织方式,详细讲解了人工智能这个学科的全貌、能解决什么问题、面临怎样的困难、尝试过哪些努力、取得过多少成绩、未来将向何方发展,尽可能消除人工智能的神秘感,把阳春白雪的人工智能从科学的殿堂推向公众面前。</p>",
    "cover": "/static/cover/ai.jpg",
    "detail": "/static/desc/ai.jpg",
    "specifications": [
      {
        "id": 16,
        "item": "出版年",
        "value": "2018-1-1"
      },
      {
        "id": 13,
        "item": "副标题",
        "value": "从图灵机到人工智能"
      },
      {
        "id": 14,
        "item": "页数",
        "value": "413"
      },
      {
        "id": 15,
        "item": "出版社",
        "value": "机械工业出版社"
      },
      {
        "id": 12,
        "item": "书名",
        "value": "智慧的疆界"
      },
      {
        "id": 10,
        "item": "作者",
        "value": "周志明"
      },
      {
        "id": 17,
        "item": "装帧",
        "value": "平装"
      },
      {
        "id": 11,
        "item": "ISBN",
        "value": "9787111610496"
      }
    ]
  },
  {
    "id": 3,
    "title": "Java虚拟机规范(Java SE 8)",
    "price": 79.0,
    "rate": 7.7,
    "description": "<p>本书完整而准确地阐释了Java虚拟机各方面的细节,围绕Java虚拟机整体架构、编译器、class文件格式、加载、链接与初始化、指令集等核心主题对Java虚拟机进行全面而深入的分析,深刻揭示Java虚拟机的工作原理。同时,书中不仅完整地讲述了由Java SE 8所引入的新特性,例如对包含默认实现代码的接口方法所做的调用,还讲述了为支持类型注解及方法参数注解而对class文件格式所做的扩展,并阐明了class文件中各属性的含义,以及字节码验证的规则。</p>",
    "cover": "/static/cover/jvms8.jpg",
    "detail": "",
    "specifications": [
      {
        "id": 18,
        "item": "作者",
        "value": "Tim Lindholm / Frank Yellin 等"
      },
      {
        "id": 24,
        "item": "出版社",
        "value": "机械工业出版社"
      },
      {
        "id": 25,
        "item": "出版年",
        "value": "2015-6"
      },
      {
        "id": 26,
        "item": "装帧",
        "value": "平装"
      },
      {
        "id": 23,
        "item": "页数",
        "value": "330"
      },
      {
        "id": 21,
        "item": "丛书",
        "value": "Java核心技术系列"
      },
      {
        "id": 20,
        "item": "原作名",
        "value": "The Java Virtual Machine Specification, Java SE 8 Edition"
      },
      {
        "id": 19,
        "item": "译者",
        "value": "爱飞翔 / 周志明 / 等 "
      },
      {
        "id": 22,
        "item": "ISBN",
        "value": "9787111501596"
      }
    ]
  },
  {
    "id": 4,
    "title": "深入理解Java虚拟机(第2版)",
    "price": 79.0,
    "rate": 9.0,
    "description": "<p>《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》内容简介:第1版两年内印刷近10次,4家网上书店的评论近4?000条,98%以上的评论全部为5星级的好评,是整个Java图书领域公认的经典著作和超级畅销书,繁体版在台湾也十分受欢迎。第2版在第1版的基础上做了很大的改进:根据最新的JDK 1.7对全书内容进行了全面的升级和补充;增加了大量处理各种常见JVM问题的技巧和最佳实践;增加了若干与生产环境相结合的实战案例;对第1版中的错误和不足之处的修正;等等。第2版不仅技术更新、内容更丰富,而且实战性更强。</p><p>《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》共分为五大部分,围绕内存管理、执行子系统、程序编译与优化、高效并发等核心主题对JVM进行了全面而深入的分析,深刻揭示了JVM的工作原理。</p><p>第一部分从宏观的角度介绍了整个Java技术体系、Java和JVM的发展历程、模块化,以及JDK的编译,这对理解书中后面内容有重要帮助。</p><p>第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见虚拟机监控与故障处理工具的原理和使用方法。</p><p>第三部分分析了虚拟机的执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎。</p><p>第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果;</p><p>第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。</p>",
    "cover": "/static/cover/jvm2.jpg",
    "detail": "/static/desc/jvm2.jpg",
    "specifications": [
      {
        "id": 31,
        "item": "页数",
        "value": "433"
      },
      {
        "id": 32,
        "item": "丛书",
        "value": "华章原创精品"
      },
      {
        "id": 28,
        "item": "副标题",
        "value": "JVM高级特性与最佳实践"
      },
      {
        "id": 29,
        "item": "ISBN",
        "value": "9787111421900"
      },
      {
        "id": 34,
        "item": "出版年",
        "value": "2013-9-1"
      },
      {
        "id": 35,
        "item": "装帧",
        "value": "平装"
      },
      {
        "id": 27,
        "item": "作者",
        "value": "周志明"
      },
      {
        "id": 30,
        "item": "书名",
        "value": "深入理解Java虚拟机(第2版)"
      },
      {
        "id": 33,
        "item": "出版社",
        "value": "机械工业出版社"
      }
    ]
  },
  {
    "id": 5,
    "title": "Java虚拟机规范(Java SE 7)",
    "price": 69.0,
    "rate": 8.9,
    "description": "<p>本书整合了自1999年《Java虚拟机规范(第2版)》发布以来Java世界所出现的技术变化。另外,还修正了第2版中的许多错误,以及对目前主流Java虚拟机实现来说已经过时的内容。最后还处理了一些Java虚拟机和Java语言概念的模糊之处。</p><p>2004年发布的Java SE 5.0版为Java语言带来了翻天覆地的变化,但是对Java虚拟机设计的影响则相对较小。在Java SE 7这个版本中,我们扩充了class文件格式以便支持新的Java语言特性,譬如泛型和变长参数方法等。</p>",
    "cover": "/static/cover/jvms.jpg",
    "detail": "/static/desc/jvms.jpg",
    "specifications": [
      {
        "id": 41,
        "item": "页数",
        "value": "316"
      },
      {
        "id": 42,
        "item": "出版社",
        "value": "机械工业出版社"
      },
      {
        "id": 36,
        "item": "作者",
        "value": "Tim Lindholm / Frank Yellin 等"
      },
      {
        "id": 37,
        "item": "译者",
        "value": "周志明 / 薛笛 / 吴璞渊 / 冶秀刚"
      },
      {
        "id": 39,
        "item": "副标题",
        "value": "从图灵机到人工智能"
      },
      {
        "id": 45,
        "item": "装帧",
        "value": "平装"
      },
      {
        "id": 44,
        "item": "出版年",
        "value": "2014-1"
      },
      {
        "id": 38,
        "item": "原作名",
        "value": "The Java Virtual Machine Specification, Java SE 7 Edition"
      },
      {
        "id": 43,
        "item": "丛书",
        "value": "Java核心技术系列"
      },
      {
        "id": 40,
        "item": "ISBN",
        "value": "9787111445159"
      }
    ]
  },
  {
    "id": 6,
    "title": "深入理解OSGi",
    "price": 79.0,
    "rate": 7.7,
    "description": "<p>本书是原创Java技术图书领域继《深入理解Java虚拟机》后的又一实力之作,也是全球首本基于最新OSGi R5.0规范的著作。理论方面,既全面解读了OSGi规范,深刻揭示了OSGi原理,详细讲解了OSGi服务,又系统地介绍了Equinox框架的使用方法,并通过源码分析了该框架的工作机制;实践方面,不仅包含一些典型的案例,还总结了大量的最佳实践,极具实践指导意义。</p><p>全书共14章,分4个部分。第一部分(第1章):走近OSGi,主要介绍了什么是OSGi以及为什么要使用OSGi。第二部分(第2~4章):OSGi规范与原理,对最新的OSGi R5.0中的核心规范进行了全面的解读,首先讲解了OSGi模块的建立、描述、依赖关系的处理,然后讲解了Bundle的启动原理和调度管理,最后讲解了与本地及远程服务相关的内容。第三部分:OSGi服务与Equinox应用实践(第5~11章),不仅详细讲解了OSGi服务纲要规范和企业级规范中最常用的几个子规范和服务的技术细节,还通过一个基于Equinox的BBS案例演示了Equinox的使用方法,最重要的是还通过源码分析了Equinox关键功能的实现机制和原理。第四部分:最佳实践(第12~14章),总结了大量关于OSGi的最佳实践,包括从Bundle如何命名、模块划分、依赖关系处理到保持OSGi动态性、管理程序启动顺序、使用API基线管理模块版本等各方面的实践技巧,此外还介绍了Spring DM的原理以及如何在OSGi环节中进行程序测试。</p>",
    "cover": "/static/cover/osgi.jpg",
    "detail": "/static/desc/OSGi.jpg",
    "specifications": [
      {
        "id": 46,
        "item": "作者",
        "value": "周志明 / 谢小明 "
      },
      {
        "id": 49,
        "item": "书名",
        "value": "智慧的疆界"
      },
      {
        "id": 50,
        "item": "丛书",
        "value": "华章原创精品"
      },
      {
        "id": 47,
        "item": "副标题",
        "value": "Equinox原理、应用与最佳实践"
      },
      {
        "id": 53,
        "item": "出版年",
        "value": "2013-2-25"
      },
      {
        "id": 54,
        "item": "装帧",
        "value": "平装"
      },
      {
        "id": 52,
        "item": "出版社",
        "value": "机械工业出版社"
      },
      {
        "id": 48,
        "item": "ISBN",
        "value": "9787111408871"
      },
      {
        "id": 51,
        "item": "页数",
        "value": "432"
      }
    ]
  },
  {
    "id": 7,
    "title": "深入理解Java虚拟机",
    "price": 69.0,
    "rate": 8.6,
    "description": "<p>作为一位Java程序员,你是否也曾经想深入理解Java虚拟机,但是却被它的复杂和深奥拒之门外?没关系,本书极尽化繁为简之妙,能带领你在轻松中领略Java虚拟机的奥秘。本书是近年来国内出版的唯一一本与Java虚拟机相关的专著,也是唯一一本同时从核心理论和实际运用这两个角度去探讨Java虚拟机的著作,不仅理论分析得透彻,而且书中包含的典型案例和最佳实践也极具现实指导意义。</p><p>全书共分为五大部分。第一部分从宏观的角度介绍了整个Java技术体系的过去、现在和未来,以及如何独立地编译一个OpenJDK7,这对理解后面的内容很有帮助。第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见的虚拟机的监控与调试工具的原理和使用方法。第三部分分析了虚拟机的执行子系统,包括Class的文件结构以及如何存储和访问Class中的数据;虚拟机的类创建机制以及类加载器的工作原理和它对虚拟机的意义;虚拟机字节码的执行引擎以及它在实行代码时涉及的内存结构。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。</p>",
    "cover": "/static/cover/jvm1.jpg",
    "detail": "",
    "specifications": [
      {
        "id": 55,
        "item": "作者",
        "value": "周志明"
      },
      {
        "id": 59,
        "item": "页数",
        "value": "387"
      },
      {
        "id": 61,
        "item": "出版年",
        "value": "2011-6"
      },
      {
        "id": 60,
        "item": "出版社",
        "value": "机械工业出版社"
      },
      {
        "id": 62,
        "item": "装帧",
        "value": "平装"
      },
      {
        "id": 56,
        "item": "副标题",
        "value": "JVM高级特性与最佳实践"
      },
      {
        "id": 57,
        "item": "ISBN",
        "value": "9787111349662"
      },
      {
        "id": 58,
        "item": "书名",
        "value": "深入理解Java虚拟机"
      }
    ]
  }
]


================================================
FILE: src/api/mock/json/settlements.json
================================================
{
  "id": 0,
  "createTime": "2020-03-13T16:04:53.388+0000",
  "payId": "0904ff25-819b-42d1-9651-dffd79a7893e",
  "totalPrice": 141.0,
  "expires": 120000,
  "paymentLink": "https://localhost:8080/pay/modify/0904ff25-819b-42d1-9651-dffd79a7893e?state=PAYED",
  "payState": "WAITING"
}


================================================
FILE: src/api/mock/json/stockpile.json
================================================
{
  "id": 1,
  "product_id": 1,
  "amount": 10,
  "frozen": 10
}


================================================
FILE: src/api/remote/account-api.js
================================================
import axios from 'axios'
import api from '@/api'

export default {

  /**
   * 根据用户名查询用户信息
   */
  getAccountByUsername (username) {
    return axios.get(`/accounts/${username}`)
  },

  /**
   * 注册的新用户
   */
  registerAccount (account) {
    // 构造新的用户提交,避免影响界面显示
    return axios.post('/accounts', {
      ...account,
      password: api.encrypt.defaultEncode(account.password)
    })
  },

  /**
   * 更新用户信息
   */
  updateAccount (account) {
    return axios.put('/accounts', account)
  }
}


================================================
FILE: src/api/remote/authorization-api.js
================================================
import axios from 'axios'
import api from '@/api'
import constants from '@/api/remote/constants'

export default {

  /**
   * 通过OAuth2的密码模式,使用用户名、密码进行登陆,完成认证与授权
   *
   * 加密/校验的过程为:明文 <-> 客户端MD5(客户端加盐) <--Over TLS--> 服务端BCrypt <-> 数据库存储
   * 注意,即使使用HTTPS保证信道安全,客户端加密也是必要的,能有效抑制服务端滥用(明文存储、输出日志)或者服务端程序被攻破而获取客户端传输过来的明文密码的风险
   */
  login (username, password) {
    return axios.get('/token', {
      // 认证、授权的API均以/oauth开头,而不是默认的/restful
      baseURL: '/oauth',
      params: {
        username,
        password: api.encrypt.defaultEncode(password),
        grant_type: constants.AUTH_GRANT_TYPE,
        client_id: constants.AUTH_CLIENT_ID,
        client_secret: constants.AUTH_CLIENT_SECRET
      }
    })
  },

  /**
   * OAuth2的令牌刷新
   *
   * 在access_token过期时调用,通过refresh_token换取新的refresh_token
   */
  refresh (refreshToken) {
    return axios.get('/token', {
      // 认证、授权的API均以/oauth开头,而不是默认的/restful
      baseURL: '/oauth',
      params: {
        refresh_token: refreshToken,
        grant_type: constants.AUTH_REFRESH_TYPE,
        client_id: constants.AUTH_CLIENT_ID,
        client_secret: constants.AUTH_CLIENT_SECRET
      }
    })
  }
}


================================================
FILE: src/api/remote/constants.js
================================================
export default {
  // 远程服务约定成功操作代码
  REMOTE_OPERATION_SUCCESS: 0,

  // HTTP 请求超时时间(毫秒)
  REMOTE_TIMEOUT: 30000,
  // 资源服务请求前缀
  REMOTE_BASE_URL: '/restful',

  // 认证服务请求前缀
  AUTH_BASE_URL: '/oauth',
  // 验证的Token类型,正常来说应该是取服务端返回的,不过这个不会变,就写在这里了
  AUTH_TOKEN_TYPE: 'bearer ',
  // 授权类型:OAuth2的密码模式
  AUTH_GRANT_TYPE: 'password',
  // 授权类型:OAuth2的令牌刷新
  AUTH_REFRESH_TYPE: 'refresh_token',
  // Client ID
  AUTH_CLIENT_ID: 'bookstore_frontend',
  // Client Secret
  AUTH_CLIENT_SECRET: 'bookstore_secret'
}


================================================
FILE: src/api/remote/payment-api.js
================================================
import axios from 'axios'

export default {
  /**
   * 提交要购买的商品和配送信息到服务端
   * 服务端会进行库存检查、配种地址检查等校验,如果结果满足的话,会执行该结算单,返回订单号和支付二维码
   */
  submitSettlement (settlement) {
    return axios.post('/settlements', settlement)
  },

  /**
   * 将支付单据的状态变为完成
   * 其实就是付款了,因为没有此演示项目实际支付的过程
   */
  accomplishPayment (id) {
    return axios.patch(`/pay/${id}?state=PAYED`)
  },

  /**
   * 将支付单据状态变为取消
   */
  cancelPayment (id) {
    return axios.patch(`/pay/${id}?state=CANCEL`)
  }

}


================================================
FILE: src/api/remote/warehouse-api.js
================================================
import axios from 'axios'

export default {

  /**
   * 无过滤条件,获取全部的产品
   */
  getAllProducts () {
    return axios.get('/products')
  },

  /**
   * 根据ID查询指定的唯一产品
   */
  getUniqueProductById (id) {
    return axios.get(`/products/${id}`)
  },

  /**
   * 取轮播广告
   */
  getAdvertisements () {
    return axios.get('/advertisements')
  },

  /**
   * 更新指定商品
   */
  updateProduct (product) {
    return axios.put(`/products`, product)
  },

  /**
   * 新建指定的商品
   */
  createProduct (product) {
    return axios.post(`/products`, product)
  },

  /**
   * 删除指定商品
   */
  removeProduct (productId) {
    return axios.delete(`/products/${productId}`)
  },

  /**
   * 获取指定商品的库存情况
   */
  queryStock (productId) {
    return axios.get(`/products/stockpile/${productId}`)
  },

  /**
   * 修改商品库存
   */
  updateStock (productId, amount) {
    return axios.patch(`/products/stockpile/${productId}?amount=${amount}`)
  }

}


================================================
FILE: src/assets/css/global.css
================================================
html {
  height: 100%
}

body {
  margin: 0;
  background-color: #ededed;
  overflow-x: hidden;
  /* 解决el-images为了预览模式设置hidden导致滚动条消失的问题 */
  overflow-y: auto !important;
  padding-right: 0 !important;
  height: 100%;
}

#app {
  height: 100%;
  min-height: 100%;
}

.el-container {
  min-height: 100%;
}

/* 以下为组件公用的样式 */

.box-card {
  width: 100%;
  font-family: Helvetica Neue, PingFang SC, Hiragino Sans GB, Heiti SC, Microsoft YaHei, WenQuanYi Micro Hei, sans-serif;
}

.header {
  text-align: left;
  font-weight: bolder;
  font-size: 18px;
  color: #666;
  background-color: #FAFAFA;
}

.content {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  text-align: left;
}

.comment {
  font-size: 12px;
  font-weight: normal;
}

.sub-title {
  display: block;
  font-weight: bold;
  margin-bottom: 20px;
}

/* 以下为对Element-UI部分样式的调整 */

.el-main, .el-header, .el-footer {
  padding: 0;
}

.el-main {
  display: flex;
  justify-content: center;
  height: 100%;
  min-height: 100%;
  overflow: hidden;
}

.el-footer {
  height: auto !important;
}

.el-card__header {
  background-color: #FAFAFA;
}

.el-input-number.is-controls-right[class*=small] [class*=decrease], .el-input-number.is-controls-right[class*=small] [class*=increase] {
  line-height: 18px;
}

.el-input--small .el-input__inner {
  height: 39px;
  line-height: 39px;
}

.slide-fade-enter-active {
  transition: all .5s ease;
}

.slide-fade-leave-active {
  transition: all .5s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}

.slide-fade-enter, .slide-fade-leave-to {
  transform: translateX(10px);
  opacity: 0;
}


================================================
FILE: src/components/home/Copyright.vue
================================================
<template>
  <div style="display: block">
    <el-row :gutter="20">
      <el-col :span="6">
        <div>
          <h1>版权信息</h1>
          <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">
            <img src="@/assets/cc-logo.png" style="height: 45px"/>
          </a>
          <span>
          本作品采用<a rel="license" href="http://creativecommons.org/licenses/by/4.0/">知识共享署名 4.0 国际许可协议</a>进行许可。
          <br/>
          <br/>
          您可以自由地:
          <ul>
            <li>共享 — 在任何媒介上以任何形式复制、发行本作品</li>
            <li>演绎 — 修改、转换或以本作品为基础进行二次创作</li>
          </ul>
          只要您遵守许可协议条款中署名、非商业性使用、相同方式共享的条件,许可人就无法收回您的这些权利。
        </span>
        </div>
      </el-col>

      <el-col :span="6">
        <div>
          <h1>社区与帮助</h1>
          <ul>
            <li><a href="">更新日志</a></li>
            <li><a href="https://icyfenix.cn/exploration/projects">在GitHub网站上获取源码</a></li>
            <li><a href="https://icyfenix.cn">关于Fenix's Project</a></li>
            <li><a href="">在Gitter.im上在线讨论</a></li>
          </ul>
        </div>
      </el-col>
      <el-col :span="6">
        <div>
          <h1>程序资源</h1>
          <span>
          Fenix's BookStore前端部分基于以下开源组件和免费资源构建:
        </span>
          <ul>
            <li><a href="https://cn.vuejs.org/">Vue.js</a><br/>渐进式JavaScript框架</li>
            <li><a href="https://element.eleme.cn/#/zh-CN">Element</a><br/>一套为开发者、设计师和产品经理准备的基于Vue 2.0的桌面端组件库</li>
            <li><a href="https://github.com/axios/axios">Axios</a><br/>Promise based HTTP client for the browser and
              node.js
            </li>
            <li><a href="http://mockjs.com/">Mock.js</a><br/>生成随机数据,拦截 Ajax 请求</li>
            <li><a href="https://www.designevo.com/cn">DesignEvo</a><br/>一款由PearlMountain有限公司设计研发的logo设计软件</li>
          </ul>
        </div>
      </el-col>
      <el-col :span="6">
        <div>
          <h1>支持作者</h1>
          <span>
          可扫描以下二维码在微信公众号上关注更新文章:
        </span>
          <!--          <img src="@/assets/qrcode.png" style="height: 150px">-->
          <qrcode value="http://weixin.qq.com/r/tEz07EbEQRs_rQKP9xmm" :options="qrcode_options"></qrcode>
          <span>
          在微信、微博、GitHub网站上关注、点赞亦是对作者的支持:
        </span>
          <ul class="contact_icons">
            <li><a href="#" target="_blank"><img src="@/assets/icons/weixin.png"></a></li>
            <li><a href="https://weibo.com/icyfenix" target="_blank"><img src="@/assets/icons/weibo.png"></a></li>
            <li><a href="https://github.com/fenixsoft" target="_blank"><img src="@/assets/icons/github.png"></a></li>
          </ul>
        </div>
      </el-col>
    </el-row>
    <el-row>
      <el-col :span="24" type="flex" justify="center">
        <hr>
        <span style="text-align: center">
          Copyright © 2020 网站备案信息:<a href="http://beian.miit.gov.cn">粤ICP备18088957号-1</a>
        </span>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import VueQrcode from '@chenfengyuan/vue-qrcode'

export default {
  name: 'Copyright',
  components: {
    [VueQrcode.name]: VueQrcode
  },
  data () {
    return {
      qrcode_options: {
        width: 150,
        margin: 1,
        color: {
          dark: '#eee',
          light: '#292A2D'
        }
      }
    }
  }
}
</script>

<style scoped>
  a {
    color: #fff;
    text-decoration: none;
  }

  h1 {
    font-size: 12px;
    font-weight: bold;
  }

  span {
    display: block;
    padding: 10px 0;
  }

  ul {
    padding-inline-start: 20px;
  }

  li {
    padding: 4px 0;
  }

  hr {
    height: 0;
    width: 90%;
    border: 1px solid #666;
    border-bottom: 0px;
  }

  .contact_icons {
    padding-inline-start: 0;
  }

  .contact_icons > li {
    display: inline-block;
    padding: 0 5px;
  }

  .contact_icons > li > a > img {
    width: 30px;
    height: 30px;
  }

  .el-row {
    padding: 20px;
    background-color: #292A2D;
    color: #fff;
    font-size: 12px;
  }

</style>


================================================
FILE: src/components/home/NavigationBar.vue
================================================
<template>
  <div>
    <div class="nav-bar-container">
      <div class="left-action-bar">
        <img src="@/assets/logo-gray-light.png" class="icon">
      </div>
      <el-menu :default-active="activeIndex" mode="horizontal" :router="true" class="nav-bar"
               text-color="#CCCCCC" background-color="#292A2D" active-text-color="#FFFFFF">
        <el-menu-item index="/">凤凰书社</el-menu-item>
        <el-menu-item index="/cart">购物车</el-menu-item>
        <el-menu-item index="/warehouse" :disabled="!isAdministrator">商品库存</el-menu-item>
        <el-menu-item index="/comment">留言板</el-menu-item>
        <el-submenu index="2">
          <template slot="title">相关信息</template>
          <el-menu-item index="#">
            <a href="http://icyfenix.cn/introduction/about-the-fenix-project.html" target="_blank">Fenix?这是什么?</a>
          </el-menu-item>
          <el-submenu index="#">
            <template slot="title">选择一种服务端</template>
            <el-menu-item index="#1">
              <a href="http://icyfenix.pub/architecture/monolithic-architecture/springboot-base-arch.html"
                 target="_blank">单体架构 By
                SpringBoot</a>
            </el-menu-item>
            <el-menu-item index="#">
              <a href="http://icyfenix.pub/architecture/microservices-architecture/springcloud-base-arch.html"
                 target="_blank">微服务架构 By
                SpringCloud</a>
            </el-menu-item>
            <el-menu-item index="#">
              <a href="http://icyfenix.pub/architecture/microservices-architecture/kubernetes-base-arch.html"
                 target="_blank">微服务架构 By
                Kubernetes</a>
            </el-menu-item>
            <el-menu-item index="#">
              <a href="http://icyfenix.cn/architecture/serverless-architecture/serverless-arch-knative.html"
                 target="_blank">无服务架构 By Knative</a>
            </el-menu-item>
          </el-submenu>
          <el-submenu index="#2">
            <template slot="title">真想买一本书?</template>
            <el-menu-item index="#">
              <a href="https://item.jd.com/63246908517.html" target="_blank">《深入理解Java虚拟机(第三版)》 @ 京东</a>
            </el-menu-item>
            <el-menu-item index="#">
              <a href="https://item.jd.com/34377092907.html" target="_blank">《智慧的疆界》 @ 京东</a>
            </el-menu-item>
            <el-menu-item index="#">
              <a href="https://item.jd.com/17021405508.html" target="_blank">《Java虚拟机规范(Java SE 7)》 @ 京东</a>
            </el-menu-item>
            <el-menu-item index="#">其他几本别买了,不推荐</el-menu-item>
          </el-submenu>
        </el-submenu>
      </el-menu>
      <div class="right-action-bar">
        <div class="right-action">
          <UserInformation/>
        </div>
      </div>
    </div>
  </div>
</template>

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

export default {
  components: {
    UserInformation
  },
  data () {
    return {
      activeIndex: '/'
    }
  },
  computed: {
    ...mapGetters('user', ['isAdministrator'])
  },
  methods: {}
}
</script>

<style scoped>
  .icon {
    height: 40px;
    padding: 7px 0 0 20px;
    cursor: pointer;
  }

  .nav-bar-container {
    background-color: #292A2D;
    text-align: center;
    height: 60px;
  }

  .nav-bar {
    display: inline-block;
  }

  .left-action-bar {
    display: inline-block;
    float: left;
    height: 100%;
  }

  .right-action-bar {
    display: inline-block;
    float: right;
    height: 100%;
  }

  .right-action {
    padding: 9px 15px;
  }

  .right-action > i {
    margin-right: 10px;
    color: #ccc;
    font-size: 20px;
    line-height: 60px;
  }

  .right-action > i:hover {
    color: #fff;
  }

  a {
    color: rgb(204, 204, 204);
    text-decoration: none;
  }
</style>


================================================
FILE: src/components/home/UserInformation.vue
================================================
<template>
  <el-popover placement="top" width="250" v-model="visible" trigger="click">
    <div class="container">
      <a href="http://cn.gravatar.com/" target="_blank">
        <el-avatar :size="64" fit="fill" :src="account.avatar"></el-avatar>
      </a>
      <span style="display: block">{{account.name}}</span>
      <el-form ref="account_form" :model="account" :rules="rules" size="mini" class="account_form">
        <el-form-item size="mini" prop="email">
          <el-input v-model="account.email">
            <template slot="prepend"><i class="el-icon-receiving"></i></template>
          </el-input>
        </el-form-item>
        <el-form-item size="mini" prop="telephone">
          <el-input v-model="account.telephone">
            <template slot="prepend"><i class="el-icon-phone-outline"></i></template>
          </el-input>
        </el-form-item>
        <el-form-item size="mini" prop="location">
          <el-input v-model="account.location">
            <template slot="prepend"><i class="el-icon-map-location"></i></template>
          </el-input>
        </el-form-item>
      </el-form>
      <div style="text-align: center; margin: 5px 0 5px 0">
        <el-button size="mini" type="primary" plain @click="modifyAccount">更新信息</el-button>
        <el-button size="mini" type="danger" plain @click="exitLogin">退出登录</el-button>
      </div>
    </div>
    <el-button :icon="isAuthorized ? 'el-icon-user-solid' : 'el-icon-user'" slot="reference" circle
               @click="changeUserStatue"></el-button>
  </el-popover>
</template>

<script>
import api from '@/api'
import {mapState, mapGetters, mapMutations, mapActions} from 'vuex'

export default {
  name: 'UserInformation',
  data () {
    return {
      trigger: 'manual',
      visible: false,
      rules: {
        email: [
          {required: true, message: '请填写邮箱', trigger: 'blur'},
          {type: 'email', message: '不符合邮箱格式', trigger: 'blur'}
        ],
        telephone: [
          {required: true, message: '请填写手机', trigger: 'blur'}
        ],
        location: [
          {required: true, message: '请填写地址', trigger: 'blur'}
        ]
      }
    }
  },
  created () {
    if (this.isAuthorized) {
      // Session是具有有效期的,设置更新令牌的触发器
      this.refreshSessionTrigger()
      // Session中有用户,而账号中没有,说明是通过“保存当前登陆状态”得到的,从服务端获取一下用户信息
      if (!this.account.username) {
        this.refreshAccount()
      }
    }
  },
  computed: {
    ...mapGetters('user', ['isAuthorized']),
    ...mapState('user', ['account', 'session'])
  },
  methods: {
    ...mapMutations('user', ['updateAccount', 'clearSession']),
    ...mapActions('user', ['refreshSessionTrigger']),
    /**
     * 检查用户状态
     * 没有登陆的话,转向登陆页面
     */
    changeUserStatue () {
      if (!this.isAuthorized) {
        this.$router.push('/login')
      }
    },
    /**
     * 从服务端请求用户信息,更新到vuex中
     */
    async refreshAccount () {
      let {data} = await api.account.getAccountByUsername(this.session.username)
      // 上传头像的功能不做了,直接用Gravatar的服务
      data.avatar = api.encrypt.gravatarEncode(data.email)
      this.updateAccount(data)
    },

    /**
     * 退出登陆
     */
    exitLogin () {
      this.clearSession()
      this.visible = false
    },

    /**
     * 更新账户信息
     */
    modifyAccount () {
      this.$refs['account_form'].validate(valid => valid ? this.submitModification() : false)
    },
    /**
     * 向服务端提交账户更新
     */
    async submitModification () {
      try {
        await api.account.updateAccount(this.account)
        this.$notify({title: '成功', message: '账号信息已成功更新', type: 'success'})
      } catch (e) {
        this.$notify({title: '失败', message: e.message, type: 'error'})
      }
    }
  }
}
</script>

<style scoped>
  .container {
    display: block;
    text-align: center;
  }

  .account_form {
    padding-top: 15px;
  }
</style>


================================================
FILE: src/components/home/cart/PayStepIndicator.vue
================================================
<template>
  <el-card class="box-card" style="margin-top: 20px">
    <div slot="header" class="header">
      <span>购买流程</span>
    </div>
    <div class="content" style="padding: 0 100px">
      <el-steps :active="step" align-center>
        <el-step title="我的购物车" description="在购物车中确认每件商品的价格、数量"></el-step>
        <el-step title="我的结算单" description="在结算单中确认配送地址、支付信息"></el-step>
        <el-step title="支付" description="通过微信、支付宝完成付款,等待收货"></el-step>
      </el-steps>
    </div>
  </el-card>
</template>

<script>
export default {
  name: 'PayStepIndicator',
  props: {
    step: Number
  }
}
</script>

<style scoped>

</style>


================================================
FILE: src/components/home/detail/Checkstand.vue
================================================
<template>
  <div class="sale">
    <ul class="sale_ul">
      <li>
        <label>零&nbsp;&nbsp;售&nbsp;&nbsp;价:</label>
        <div class="sale_content price">¥{{product.price.toFixed(2)}}</div>
      </li>
      <li>
        <label>促销信息:</label>
        <div class="sale_content">
          <div style="padding-bottom: 5px;">
            <el-tag type="danger" effect="plain">加价购</el-tag>
            &nbsp;或满15元另加5.90元,即可换购热销商品
          </div>
          <div>
            <el-tag type="danger" effect="plain">送赠品</el-tag>
            &nbsp;购满两本,送限量赠彩虹数据线1条
          </div>
        </div>
      </li>
      <li>
        <label>即刻配送:</label>
        <div class="sale_content" style="padding-top: 8px;">
          <el-switch v-model="purchase.delivery"/>
        </div>
      </li>
      <li>
        <label>配&nbsp;&nbsp;送&nbsp;&nbsp;至:</label>
        <div class="sale_content">
          <v-distpicker :province="purchase.address.province" :city="purchase.address.city"
                        :area="purchase.address.area" @selected="onAddressSelected"></v-distpicker>
          <span class="address_info"><b>有货</b> 由本店发货, 并提供售后服务. 18:00前下单,预计明天送达</span>
        </div>
      </li>
    </ul>
    <div style="padding: 0 10px 0 18px; display: inline">
      <el-input-number v-model="purchase.amount" controls-position="right" :min="1" :max="10" size="small"
                       style="height: 39px; line-height: 39px;"/>
    </div>
    <el-button type="danger" icon="el-icon-shopping-cart-full" @click="addCart">加入购物车</el-button>
    <el-button type="danger" plain icon="el-icon-goods" @click="buyNow">立即购买</el-button>
  </div>
</template>

<script>
import VDistpicker from 'v-distpicker'
import {mapMutations, mapActions} from 'vuex'

export default {
  name: 'Checkstand',
  components: {
    VDistpicker
  },
  model: {
    prop: 'purchase',
    event: 'place-order'
  },
  props: {
    purchase: Object,
    product: Object
  },
  methods: {
    ...mapMutations('cart', ['adjustCartItems']),
    ...mapActions('cart', ['setupSettlementBillWithDefaultValue']),
    /**
     * 加入购物车
     **/
    addCart () {
      let payload = {
        ...this.product,
        amount: this.purchase.amount || 1
      }
      this.adjustCartItems(payload)
      this.$notify({
        title: '成功',
        message: '恭喜你,该商品已成功添加到购物车',
        type: 'success'
      })
    },
    /**
     * 立即购买
     */
    buyNow () {
      let payload = {
        purchase: this.purchase,
        items: [{
          ...this.product,
          amount: this.purchase.amount || 1
        }]
      }
      this.setupSettlementBillWithDefaultValue(payload)
      this.$router.push('/settle')
    },
    /**
     * 地址选择控件的绑定事件,该控件未支持v-model
     */
    onAddressSelected (address) {
      this.purchase.address.province = address.province.value
      this.purchase.address.city = address.city.value
      this.purchase.address.area = address.area.value
    }
  }
}
</script>

<style scoped>
  .sale {
    padding-top: 10px;
    font-size: 14px;
    display: inline-block;
    max-width: 610px;
  }

  .sale_ul {
    list-style-type: none;
    padding-inline-start: 20px;
  }

  .sale_content {
    display: inline-block;
    padding-bottom: 20px;
    font-size: 14px;
    max-width: 465px;
  }

  label {
    display: inline-table;
    vertical-align: top;
    width: 85px;
    padding: 10px 0;
  }

  .price {
    color: red;
    font-size: 24px;
  }

  .address_info {
    display: block;
    padding: 5px;
    font-size: 12px;
    color: #666;
  }
</style>


================================================
FILE: src/components/home/main/Cabinet.vue
================================================
<template>
  <el-card class="box-card">
    <div slot="header" class="header">
      <span>热销书籍</span>
    </div>
    <el-row :gutter="0">
      <el-col :span="6" v-for="book in books" :key="book.id" class="book-container">
        <el-image :src="book.cover" class="image" @click="loadDetail(book.id)"/>
        <div style="padding: 14px;">
          <span id="price">¥ {{book.price.toFixed(2)}}</span>
          <span id="title">{{book.title}}</span>
          <span id="description">{{pureText(book.description)}}</span>
          <div id="actions">
            <el-button icon="el-icon-money" @click="goDirectSettlement(book)" circle></el-button>
            <el-button :icon="isInCart(book.id) ? 'el-icon-s-goods' : 'el-icon-goods'" circle
                       @click="updateCart(book.id)"></el-button>
            <el-button :icon="isFavorite(book.id) ? 'el-icon-star-on' : 'el-icon-star-off'" circle
                       @click="updateFavorite(book.id)"></el-button>
          </div>
        </div>
      </el-col>
    </el-row>
  </el-card>
</template>

<script>
import api from '@/api'
import {mapState, mapMutations, mapActions} from 'vuex'

export default {
  name: 'Cabinet',
  data () {
    return {
      books: []
    }
  },
  computed: {
    ...mapState('user', ['favorite', 'account']),
    ...mapState('cart', ['items'])
  },
  async created () {
    this.books = (await api.warehouse.getAllProducts()).data
  },
  methods: {
    ...mapMutations('user', ['addFavorite', 'removeFavorite']),
    ...mapMutations('cart', ['addCartItem', 'removeCartItem']),
    ...mapActions('cart', ['setupSettlementBillWithDefaultValue']),
    /**
     * 判断是否在收藏夹中
     **/
    isFavorite (id) {
      return this.favorite.includes(id)
    },
    /**
     * 判断是否在购物车中
     **/
    isInCart (id) {
      return this.items.find(item => item.id === id)
    },
    /**
     *快捷添加收藏夹,点一下加入,再点移除
     **/
    updateFavorite (id) {
      this.isFavorite(id) ? this.removeFavorite(id) : this.addFavorite(id)
      this.$notify({
        title: '成功',
        message: '恭喜你,已成功更新收藏夹',
        iconClass: 'el-icon-star-on',
        type: 'success'
      })
    },
    /**
     * 快捷添加购物车,点一下加入,再点移除(哪怕有多件)
     */
    updateCart (id) {
      this.isInCart(id) ? this.removeCartItem(id) : this.addCartItem({...this.books.find(i => i.id === id)})
      this.$notify({
        title: '成功',
        message: '恭喜你,已成功更新购物车',
        iconClass: 'el-icon-s-goods',
        type: 'success'
      })
    },
    /**
     * 转到商品详情页面
     */
    loadDetail (id) {
      this.$router.push(`/detail/${id}`)
    },
    /**
     * 去除HTML标签
     */
    pureText (text) {
      return api.stringUtil.pureText(text)
    },
    /**
     * 直接支付购买
     */
    goDirectSettlement (product) {
      let item = {...product, amount: 1}
      this.setupSettlementBillWithDefaultValue({items: [item]})
      this.$router.push('/settle')
    }
  }
}
</script>

<style scoped>
  .image {
    width: 300px;
    height: 300px;
  }

  #price {
    font-family: Arial, serif;
    font-size: 18px;
    font-weight: bolder;
    color: #d44d44;
    display: block;
  }

  #title {
    font-size: 14px;
    font-weight: 700;
    line-height: 1.2;
    margin: 0 8px;
    color: #333;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
    padding: 5px 0 10px 0;
    display: block;
  }

  #description {
    font-size: 12px;
    color: #999;
    text-align: left;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
  }

  #actions {
    padding: 10px 10px 0 0;
  }

  .book-container {
    padding: 20px 0;
    border: 1px solid #fff;
    transition: .2s;
  }

  .book-container:hover {
    border: 1px solid #ddd;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
    cursor: pointer;
  }

</style>


================================================
FILE: src/components/home/main/Carousel.vue
================================================
<template>
  <el-carousel :interval="5000" type="card" height="400px">
    <el-carousel-item v-for="item in advertisements" :key="item.id">
      <el-image :src="item.image" class="image" @click="loadDetail(item.productId)"/>
    </el-carousel-item>
  </el-carousel>
</template>

<script>
import api from '@/api'

export default {
  name: 'Carousel',
  data () {
    return {
      advertisements: []
    }
  },
  async created () {
    this.advertisements = (await api.warehouse.getAdvertisements()).data
  },
  methods: {
    loadDetail (productId) {
      this.$router.push(`/detail/${productId}`)
    }
  }
}
</script>

<style scoped>
  .el-carousel__item h3 {
    color: #475669;
    font-size: 14px;
    opacity: 0.75;
    line-height: 200px;
    margin: 0;
  }

  .image {
    border: 1px solid #ddd;
    border-radius: 15px;
  }
</style>


================================================
FILE: src/components/home/warehouse/ProductManage.vue
================================================
<template>
  <el-form :model="product" label-position="left">
    <el-form-item label="商品名称" label-width="100px">
      <el-input v-model="product.title" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="商品售价" label-width="100px">
      <el-input v-model="product.price" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="商品评分" label-width="100px">
      <el-input v-model="product.rate" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="封面图片" label-width="100px">
      <el-input v-model="product.cover" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="详情图片" label-width="100px">
      <el-input v-model="product.detail" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="商品规格">
      <div style="padding-top: 40px">
        <el-tag :key="spec.item" v-for="spec in product.specifications" closable size="medium"
                :disable-transitions="false" @close="removeSpecification(spec)">
          {{spec.item+':'+spec.value}}
        </el-tag>
        <el-input class="input-new-tag" v-if="inputVisible" v-model="inputValue" ref="saveTagInput" size="small"
                  @keyup.enter.native="handleInputConfirm" @blur="handleInputConfirm">
        </el-input>
        <el-button v-else class="button-new-tag" size="small" @click="showInput">+ New Spec</el-button>
      </div>
    </el-form-item>
    <el-form-item label="商品描述">
      <el-input type="textarea" v-model="product.description" :autosize="{ minRows: 7, maxRows: 7}"></el-input>
    </el-form-item>
    <div style="text-align: right; padding-right: 40px">
      <el-button @click="$emit('dismiss')">取 消</el-button>
      <el-button type="primary" @click="submitProduct">确 定</el-button>
    </div>
  </el-form>
</template>

<script>
import api from '@/api'

export default {
  name: 'ProductManage',
  props: {
    createMode: Boolean,
    product: Object
  },
  data () {
    return {
      inputVisible: false,
      inputValue: ''
    }
  },
  methods: {
    refresh () {
    },

    /**
     * 提交产品修改到服务端
     */
    async submitProduct () {
      // 移除所有HTML标签,防止XSS
      let desc = api.stringUtil.pureText(this.product.description)
      // 将回车转回HTML的<p>
      desc = api.stringUtil.transToHTML(desc)

      const remote = this.createMode ? api.warehouse.createProduct : api.warehouse.updateProduct
      try {
        await remote({
          ...this.product,
          description: desc
        })
        this.$notify({title: '操作成功', message: (this.createMode ? '商品已成功创建' : '商品信息已修改'), type: 'success'})
        this.$emit('updated')
      } catch (e) {
        this.$notify({title: '操作失败', message: e.message, type: 'error'})
      }
    },

    /**
     * 移除一个产品规格标签
     */
    removeSpecification (spec) {
      this.product.specifications.splice(this.product.specifications.indexOf(spec), 1)
    },

    /**
     * 增加一个产品规格标签
     */
    showInput () {
      this.inputVisible = true
      this.$nextTick(_ => {
        this.$refs.saveTagInput.$refs.input.focus()
      })
    },

    /**
     * 处理产品规格标签的输入确认
     */
    handleInputConfirm () {
      let inputValue = this.inputValue
      if (inputValue) {
        inputValue = inputValue.replace(':', ':')
        if (inputValue.indexOf(':') > 0) {
          const spec = inputValue.split(':')
          this.product.specifications.push({
            item: spec[0],
            value: spec[1],
            productId: this.product.id
          })
        } else {
          this.$alert('产品规格应该以“项目:值”的形式录入')
        }
      }
      this.inputVisible = false
      this.inputValue = ''
    }
  }
}
</script>

<style scoped>
  .el-tag + .el-tag {
    margin-left: 10px;
  }

  .button-new-tag {
    margin-left: 10px;
    height: 32px;
    line-height: 30px;
    padding-top: 0;
    padding-bottom: 0;
  }

  .input-new-tag {
    width: 90px;
    margin-left: 10px;
    vertical-align: bottom;
  }
</style>


================================================
FILE: src/components/home/warehouse/StockManage.vue
================================================
<template>
  <div>
    <span class="title">{{this.product.title}}</span>
    <el-form :model="product" label-position="right" :inline="true">
      <el-form-item label="可用库存" label-width="135px">
        <el-input-number v-model="stock.amount"></el-input-number>
      </el-form-item>
      <el-form-item label="冻结库存" label-width="135px">
        <el-input-number v-model="stock.frozen" disabled></el-input-number>
      </el-form-item>
    </el-form>
    <div style="text-align: right; padding-right: 40px">
      <el-button @click="$emit('dismiss')">取 消</el-button>
      <el-button type="primary" @click="submitStock">确 定</el-button>
    </div>
  </div>
</template>

<script>
import api from '@/api'

export default {
  name: 'StockManage',
  props: {
    product: Object,
    stock: Object
  },
  data () {
    return {}
  },
  methods: {
    /**
     * 修改指定商品的库存数量
     */
    async submitStock () {
      try {
        await api.warehouse.updateStock(this.product.id, this.stock.amount)
        this.$notify({title: '操作成功', message: (this.createMode ? '商品已成功创建' : '商品信息已修改'), type: 'success'})
        this.$emit('dismiss')
      } catch (e) {
        this.$notify({title: '操作失败', message: e.message, type: 'error'})
      }
    }
  }
}
</script>

<style scoped>
  .title {
    font-size: 20px;
    padding-bottom: 30px;
    display: block;
  }
</style>


================================================
FILE: src/components/login/LoginForm.vue
================================================
<template>
  <div>
    <img src="../../assets/logo-color.png" class="logo">
    <span class="title">Fenix's Bookstore</span>
    <el-form :model="authorization" :rules="rules" ref="login-form" class="login-form">
      <el-form-item prop="name">
        <el-input placeholder="请输入用户" v-model="authorization.name">
          <template slot="prepend"><i class="el-icon-user"></i></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input placeholder="请输入密码" show-password v-model="authorization.password">
          <template slot="prepend"><i class="el-icon-unlock"></i></template>
        </el-input>
      </el-form-item>
      <el-select placeholder="请选择语言" style="width: 370px" v-model="authorization.language">
        <el-option label="          中文" value="zhCN"/>
        <el-option label="          英文(无效,国际化预留)" value="enUS"/>
        <template slot="prefix">
          <div class="select-prefix"><i class="el-icon-map-location"></i></div>
        </template>
      </el-select>
      <template slot="prepend"><i class="el-icon-map-location"></i></template>
      <div class="actions">
        <el-checkbox v-model="authorization.rememberMe" class="check">
          自动登录
        </el-checkbox>
        <el-button type="text" style="float: right; display: inline-block; padding: 0 10px 0 0"
                   v-on:click="$emit('changeMode')">注册新用户
        </el-button>
        <el-button type="primary" style="width: 100%; display: block; margin: 50px 0 0 0" @click="login">登录</el-button>
      </div>
      <hr>
      <div style="text-align: center; ">
        登录代表你已同意
        <el-tooltip effect="dark" content="演示用途,并没有写" placement="bottom">
          <el-button type="text">用户协议</el-button>
        </el-tooltip>
        和
        <el-tooltip effect="dark" content="也是没有写" placement="bottom">
          <el-button type="text" style="margin-left: 0">隐私政策</el-button>
        </el-tooltip>
      </div>
    </el-form>
  </div>
</template>

<script>
export default {
  name: 'LoginForm',
  data () {
    return {
      authorization: {
        name: '',
        password: '',
        language: 'zhCN',
        rememberMe: false
      },
      rules: {
        name: [
          {required: true, message: '请输入用户名称', trigger: 'blur'}
        ],
        password: [
          {required: true, message: '请输入密码', trigger: 'blur'}
        ]
      }
    }
  },
  methods: {
    login () {
      this.$refs['login-form'].validate((valid) => {
        if (valid) {
          this.$emit('login', this.authorization)
        } else {
          return false
        }
      })
    }
  }
}
</script>

<style scoped>
  .logo {
    width: 120px;
    height: 120px;
    display: block;
    padding: 70px 0 0 165px;
  }

  .title {
    width: 100%;
    display: block;
    text-align: center;
    padding-top: 20px;
    line-height: 1em;
    color: #333;
    font-size: 20px;
    font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif
  }

  .login-form {
    padding: 50px 40px;
  }

  .actions {
    padding: 20px 0;
    width: 100%;
    display: block;
  }

  hr {
    height: 0;
    width: 90%;
    border: 1px solid #BBB;
    border-bottom: 0px;
  }

  .check {
    float: left;
    display: inline-block;
    padding-left: 10px
  }

  .select-prefix {
    /*border-right: 0;*/
    border: 1px solid #DCDFE6;
    background-color: #F5F7FA;
    color: #909399;
    vertical-align: middle;
    display: table-cell;
    position: relative;
    border-radius: 4px;
    padding: 0 20px;
    height: 38px;
    left: -5px;
  }

  i {
    font-size: 18px;
  }

</style>


================================================
FILE: src/components/login/RegistrationForm.vue
================================================
<template>
  <div>
    <img src="../../assets/logo-color.png" class="logo">
    <span class="title">新用户注册</span>
    <el-form ref="account_form" :model="account" :rules="rules" label-position="left" class="account_form">
      <el-form-item prop="user">
        <el-input placeholder="请输入用户名" v-model="account.username">
          <template slot="prepend"><i class="el-icon-user"></i></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input placeholder="请输入密码" show-password v-model="account.password">
          <template slot="prepend"><i class="el-icon-unlock"></i></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="name">
        <el-input placeholder="请输入真实姓名" v-model="account.name">
          <template slot="prepend"><i class="el-icon-user"></i></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="email">
        <el-input placeholder="请输入邮箱" v-model="account.email">
          <template slot="prepend"><i class="el-icon-receiving"></i></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="telephone">
        <el-input placeholder="请输入手机" v-model="account.telephone">
          <template slot="prepend"><i class="el-icon-phone-outline"></i></template>
        </el-input>
      </el-form-item>
      <div class="actions">
        <el-button type="primary" class="action_button" @click="registerAccount">注册</el-button>
        <el-button class="action_button" @click="$emit('changeMode')">返回
        </el-button>
      </div>
    </el-form>
  </div>
</template>

<script>
import api from '@/api'
import {mapMutations} from 'vuex'

export default {
  name: 'RegistrationForm',
  data () {
    return {
      account: {
        username: '',
        email: '',
        password: '',
        telephone: ''
      },
      rules: {
        username: [
          {required: true, message: '请填写用户名', trigger: 'blur'}
        ],
        name: [
          {required: true, message: '请填写真实姓名', trigger: 'blur'}
        ],
        password: [
          {required: true, message: '请填写密码', trigger: 'blur'}
        ],
        email: [
          {required: true, message: '请填写邮箱', trigger: 'blur'},
          {type: 'email', message: '不符合邮箱格式', trigger: 'blur'}
        ],
        telephone: [
          {required: true, message: '请填写手机', trigger: 'blur'}
        ]
      }
    }
  },
  methods: {
    ...mapMutations('user', ['setupSession']),
    /**
     * 用户注册
     */
    registerAccount () {
      this.$refs['account_form'].validate((valid) => {
        if (valid) {
          this.submitRegistration()
        } else {
          return false
        }
      })
    },
    /**
     * 向服务端提交注册信息
     */
    async submitRegistration () {
      try {
        let {data} = await api.account.registerAccount(this.account)
        if (data.code === api.constants.REMOTE_OPERATION_SUCCESS) {
          // 注册成功后自动登陆该用户,并转向首页
          data = (await api.auth.login(this.account.username, this.account.password)).data
          data.rememberMe = false
          data.language = 'zhCN'
          this.setupSession(data)
          this.$router.push('/')
          return
        }
      } catch (e) {
        // 没有什么别的处理了,显示错误信息告知用户
        console.error(e)
        this.$alert(e.message, '出现异常')
      }
    }
  }
}
</script>

<style scoped>
  .logo {
    width: 120px;
    height: 120px;
    display: block;
    padding: 70px 0 0 165px;
  }

  .title {
    width: 100%;
    display: block;
    text-align: center;
    padding-top: 20px;
    line-height: 1em;
    color: #333;
    font-size: 20px;
    font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif
  }

  .account_form {
    padding: 50px 40px 50px 40px;
  }

  .actions {
    width: 100%;
    display: block;
    text-align: center;
  }

  .action_button {
    width: 150px;
  }

  hr {
    height: 0;
    width: 90%;
    border: 1px solid #BBB;
    border-bottom: 0;
  }

  i {
    font-size: 18px;
  }

</style>


================================================
FILE: src/main.js
================================================
import Vue from 'vue'
import 'default-passive-events'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import App from './App'
import store from './store'
import router from './router'
import errorPlugin from './plugins/errorhandler-plugin'

/**
 * 默认在开发模式中启用mock.js代替服务端请求
 * 如需要同时调试服务端,请修改此处判断条件
 */
// eslint-disable-next-line no-constant-condition
if (process.env.MOCK) {
  require('./api/mock')
}

Vue.use(ElementUI)
/**
 * 全局异常处理,将所有没有捕获的异常统一显示出来
 */
Vue.use(errorPlugin, {
  errorHandler: (error, vm, info) => {
    // console.error(error)
    store.commit('notification/setException', error)
  }
})

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  router,
  components: {App},
  template: '<App/>'
})


================================================
FILE: src/pages/Login.vue
================================================
<template>
  <div class="bg">
    <div class="dialog dialog-shadow">
      <LoginForm v-if="!registrationMode" v-on:changeMode="registrationMode = !registrationMode" @login="login"/>
      <RegistrationForm v-if="registrationMode" v-on:changeMode="registrationMode = !registrationMode"/>
    </div>
  </div>
</template>

<script>
import {mapMutations} from 'vuex'
import api from '@/api'
import LoginForm from '../components/login/LoginForm'
import RegistrationForm from '../components/login/RegistrationForm'

export default {
  name: 'Login',
  components: {
    LoginForm,
    RegistrationForm
  },
  data () {
    return {
      // 表示登陆还是注册状态
      registrationMode: false
    }
  },
  computed: {
    /**
     *  记录登陆成功后要转向的地址,该地址由全局路由拦截器自动设置,如果没设置,默认转向首页
     **/
    nextPath () {
      return this.$route.query.redirect ? this.$route.query.redirect : '/'
    }
  },
  methods: {
    ...mapMutations('user', ['setupSession']),
    /**
     * 处理登录表单中的登录事件
     */
    async login (authorization) {
      try {
        let {data} = await api.auth.login(authorization.name, authorization.password)
        data.rememberMe = authorization.rememberMe
        data.language = authorization.language
        this.setupSession(data)
        // 转向处理页面
        this.$router.push(this.nextPath)
      } catch (e) {
        this.$alert(e.message, '出现异常')
      }
    }
  }
}
</script>

<style scoped>
  .bg {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background-image: url("../assets/bg.png");
    background-repeat: repeat;
  }

  .dialog {
    width: 450px;
    height: 642px;
    border: 1px solid #dadada;
    border-radius: 10px;
    top: 45%;
    left: 50%;
    margin-top: -371px;
    margin-left: -225px;
    position: absolute;
    background-image: url("../assets/bg2.png");
    background-repeat: repeat;
  }

  .dialog-shadow {
    -webkit-box-shadow: 0 9px 30px -6px rgba(0, 0, 0, .2), 0 18px 20px -10px rgba(0, 0, 0, .04), 0 18px 20px -10px rgba(0, 0, 0, .04), 0 10px 20px -10px rgba(0, 0, 0, .04);
    -moz-box-shadow: 0 9px 30px -6px rgba(0, 0, 0, .2), 0 18px 20px -10px rgba(0, 0, 0, .04), 0 18px 20px -10px rgba(0, 0, 0, .04), 0 10px 20px -10px rgba(0, 0, 0, .04);
    box-shadow: 0 9px 30px -6px rgba(0, 0, 0, .2), 0 18px 20px -10px rgba(0, 0, 0, .04), 0 18px 20px -10px rgba(0, 0, 0, .04), 0 10px 20px -10px rgba(0, 0, 0, .04);
  }
</style>


================================================
FILE: src/pages/home/CartPage.vue
================================================
<template>
  <div>
    <el-card class="box-card">
      <div slot="header" class="header">
        <span>我的购物车</span>
        <span class="comment">温馨提示:产品是否购买成功,以最终支付为准哦,请尽快完成结算</span>
      </div>
      <div class="content">
        <el-table :data="items" ref="cartTable" @selection-change="handleSelectionChange" style="width: 100%">
          <el-table-column type="selection" width="55" fixed show-overflow-tooltip></el-table-column>
          <el-table-column label="图片" width="150">
            <template slot-scope="scope">
              <img :src="scope.row.cover" style="width: 120px"/>
            </template>
          </el-table-column>
          <el-table-column prop="title" label="商品名称" sortable></el-table-column>
          <el-table-column prop="price" label="单价" width="100" sortable></el-table-column>
          <el-table-column label="数量" width="170" sortable>
            <template slot-scope="scope">
              <el-input-number size="mini" :min="0" :max="10" :value="scope.row.amount"
                               @change="(newValue,oldValue)=>{adjustAmount(scope.row,newValue,oldValue)}"></el-input-number>
            </template>
          </el-table-column>
          <el-table-column label="小计" width="120" sortable>
            <template slot-scope="scope">
              <span class="subtotal">{{scope.row.price * scope.row.amount}} 元</span>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="120">
            <template slot-scope="scope">
              <el-button plain size="mini" type="danger" @click="removeItem(scope.row.id)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <div class="actions">
          {{`购物车中共计 ${items.length} 件商品,已选择其中 ${multipleSelection.length} 件`}}
          <div class="total">
            总计: <span class="pay_price">{{this.total}}</span> 元
            <div class="pay_action">
              <el-button size="large" type="primary" style="position: relative; top: -6px"
                         :disabled="this.total<=0" @click="goSettlement">¥ 选好了,去结算
              </el-button>
            </div>
          </div>
        </div>
      </div>
    </el-card>
    <PayStepIndicator :step="1"/>
  </div>
</template>

<script>
import {mapState, mapMutations, mapActions} from 'vuex'
import PayStepIndicator from '@/components/home/cart/PayStepIndicator'

export default {
  name: 'CartPage',
  components: {
    PayStepIndicator
  },
  data () {
    return {
      multipleSelection: []
    }
  },
  computed: {
    ...mapState('cart', ['items']),
    /**
     * 商品总价
     **/
    total () {
      return this.multipleSelection.reduce((sum, product) => sum + (product.price * product.amount), 0)
    }
  },
  mounted () {
    // 转载时默认全选所有商品
    this.toggleSelection(this.items)
  },
  methods: {
    ...mapMutations('cart', ['adjustCartItems', 'removeCartItem']),
    ...mapActions('cart', ['setupSettlementBillWithDefaultValue']),
    /**
     * 调整购物车中产品的数量
     */
    adjustAmount (product, currentValue, oldValue) {
      let item = {...product}
      item.amount = currentValue - oldValue
      this.adjustCartItems(item)
    },
    /**
     * 删除购物车中的产品
     **/
    removeItem (id) {
      this.removeCartItem(id)
      // 删除后,原本选中的项目要继续保持选中
      this.$nextTick(() => this.toggleSelection(this.items))
    },
    /**
     * 选中购物车表格中指定商品
     */
    toggleSelection (rows) {
      if (rows) {
        rows.forEach(row => {
          this.$refs.cartTable.toggleRowSelection(row)
        })
      } else {
        this.$refs.cartTable.clearSelection()
      }
    },
    /**
     *转到结算页面付款
     **/
    goSettlement () {
      this.setupSettlementBillWithDefaultValue({
        items: this.multipleSelection
      })
      this.$router.push('/settle')
    },
    /**
     * 维护表格选择集
     */
    handleSelectionChange (val) {
      this.multipleSelection = val
    }
  }
}
</script>

<style scoped>
  .subtotal {
    color: red;
    font-weight: bold;
    font-size: 16px;
  }

  .total {
    float: right;
  }

  .actions {
    margin-top: 20px;
    line-height: 32px
  }

  .pay_action {
    display: inline-block;
    width: 180px;
    margin: 0 20px 10px 20px;
  }

  .pay_price {
    color: red;
    font-size: 32px;
    font-weight: bold
  }

</style>


================================================
FILE: src/pages/home/CommentPage.vue
================================================
<template>
  <el-card class="box-card" :body-style="{ padding: '0 10px 0 10px' }">
    <div slot="header" class="header">
      <span>讨论</span>
      <span class="comment">本功能通过Gitalk使用GitHub的Issues提供服务,请使用GitHub账号登录</span>
    </div>
    <div>
      <iframe src="/static/board/gitalk.html" style="width: 1300px; height: 1000px" frameborder="0"></iframe>
    </div>
  </el-card>
</template>

<script>
export default {
  name: 'CommentPage'
}
</script>

<style scoped>
  .body {
    padding: 0;
  }
</style>


================================================
FILE: src/pages/home/DetailPage.vue
================================================
<template>
  <div id="information">
    <el-card class="box-card">
      <div slot="header" class="header">
        <span>{{book.title}}</span>
      </div>
      <el-row class="content">
        <el-col :span="6"><img id="cover" :src="book.cover"></el-col>
        <el-col :span="6">
          <div style="padding-top: 30px">
            <span v-for="spec in book.specifications" :key="spec.item" class="spec">{{spec.item}}:{{spec.value}}</span>
            <span class="spec" style="display: inline-block;">豆瓣评分:</span>
            <el-rate :value="book.rate/2" disabled style="display: inline-block;"/>
            <span style="color:#ff9900; font-size: 14px">{{book.rate}}</span>
          </div>
        </el-col>
        <el-col :span="12">
          <el-divider direction="vertical" class="devider"></el-divider>
          <Checkstand :purchase="purchase" :product="book"></Checkstand>
        </el-col>
      </el-row>
    </el-card>
    <el-card class="box-card" style="margin-top: 20px">
      <div slot="header" class="header">
        <span>内容简介</span>
      </div>
      <div class="content description" v-html="book.description">
      </div>
    </el-card>
    <el-card class="box-card" style="margin-top: 20px">
      <div slot="header" class="header">
        <span>详情介绍</span>
      </div>
      <img v-if="book.detail" :src="book.detail"/>
      <span v-else class="content">本书暂无详细介绍</span>
    </el-card>
  </div>
</template>

<script>
import api from '@/api'
import Checkstand from '@/components/home/detail/Checkstand'

export default {
  name: 'DetailPage',
  components: {
    Checkstand
  },
  props: {
    id: String
  },
  data () {
    return {
      purchase: {
        amount: 1,
        delivery: true,
        address: {province: '广东省', city: '广州市', area: '海珠区'}
      },
      book: {
        price: 0,
        specifications: {}
      }
    }
  },
  async created () {
    this.book = (await api.warehouse.getUniqueProductById(this.id)).data
  }
}
</script>

<style scoped>
  .bg {
    width: 100%;
    background-color: #fff;
  }

  #cover {
    width: 250px;
    height: 250px;
    float: left;
    padding: 20px;
    display: inline-block;
  }

  .spec {
    display: block;
    font-size: 14px;
    line-height: 25px;
    color: #666;
  }

  .devider {
    display: inline-block;
    height: 345px;
    vertical-align: top;
  }

  .description {
    font-size: 14px;
    line-height: 24px;
    text-indent: 2em;
  }
</style>


================================================
FILE: src/pages/home/MainPage.vue
================================================
<template>
  <div>
    <Carousel/>
    <Cabinet/>
  </div>
</template>

<script>
import Carousel from '@/components/home/main/Carousel'
import Cabinet from '@/components/home/main/Cabinet'

export default {
  name: 'MainPage',
  components: {
    Carousel,
    Cabinet
  }
}
</script>

<style scoped>

</style>


================================================
FILE: src/pages/home/PaymentPage.vue
================================================
<template>
  <div>
    <el-card class="box-card" style="margin-top: 20px">
      <div slot="header" class="header">
        <span>我的购物车</span>
        <span v-if="isPaymentReady" class="comment">订单已提交成功,请尽快付款!</span>
      </div>
      <div class="content">
        <div v-if="isPaymentReady">
          <span class="sub-title">购买成功!</span>
          <div style="text-align: center">
            <div>{{message}}</div>
            <qrcode :value="payment.paymentLink" :options="qrcode_options"></qrcode>
            <div>支付总额:<span class="price">{{payment.totalPrice.toFixed(2)}}</span></div>
            <div>提示:本程序为演示,扫描以上二维码并不会实际触发支付<br/>
              <el-link type="primary" @click="accomplishPayment">点击模拟扫描</el-link>
              或也可以
              <el-link type="danger" @click="cancelPayment">点击取消购买</el-link>
            </div>
          </div>
        </div>
        <div v-else>
          <span class="sub-title">购买失败!</span>
          <span>失败原因:{{payment.message || '没有收到服务端的支付结算数据'}}</span>
        </div>
      </div>
    </el-card>
    <PayStepIndicator :step="3"/>
  </div>
</template>

<script>
import PayStepIndicator from '@/components/home/cart/PayStepIndicator'
import VueQrcode from '@chenfengyuan/vue-qrcode'
import {mapState} from 'vuex'
import api from '@/api'

export default {
  name: 'PaymentPage',
  components: {
    PayStepIndicator,
    [VueQrcode.name]: VueQrcode
  },
  data () {
    return {
      countdown: {
        min: 0,
        sec: 0,
        timeout: false
      },
      isPaymentFinish: false,
      qrcode_options: {
        width: 300,
        color: {dark: '#666'}
      }
    }
  },
  computed: {
    ...mapState('cart', ['payment']),
    /**
     * 是否成功生成支付单
     **/
    isPaymentReady () {
      return !!this.payment.payId
    },
    message () {
      if (this.isPaymentFinish) {
        return '恭喜,你已经成功付款!'
      } else {
        if (!this.countdown.timeout) {
          return `商品已准备完成,请在 ${this.countdown.min} 分钟${this.countdown.sec}秒内完成支付,订单号码:${this.payment.payId}`
        } else {
          return '你的订单已经取消,如仍需要,请重新购买商品。'
        }
      }
    }
  },
  mounted: function () {
    this.countDown()
  },
  methods: {
    /**
     * 商品支付超期的倒计时
     */
    countDown () {
      const msec = this.payment.expires - new Date().getTime()
      if (msec > 0) {
        this.countdown.min = parseInt(msec / 1000 / 60 % 60)
        this.countdown.sec = parseInt(msec / 1000 % 60)
        setTimeout(() => {
          this.countDown()
        }, 1000)
      } else {
        this.countdown.timeout = true
      }
    },
    /**
     * 取消商品支付
     */
    async cancelPayment () {
      try {
        await api.payment.cancelPayment(this.payment.payId)
        this.countdown.timeout = true
        this.$notify({title: '操作成功', message: '订单已被取消', type: 'info'})
      } catch (e) {
        this.$notify({title: '操作失败', message: e.message, type: 'error'})
      }
    },
    /**
     * 完成商品支付
     */
    async accomplishPayment () {
      try {
        await api.payment.accomplishPayment(this.payment.payId)
        this.isPaymentFinish = true
        this.$notify({title: '操作成功', message: '订单已完成支付', type: 'success'})
      } catch (e) {
        this.$notify({title: '操作失败', message: e.message, type: 'error'})
      }
    }
  }
}
</script>

<style scoped>
  .price {
    color: red;
    font-size: 18px;
    font-weight: bold;
    line-height: 32px;
  }
</style>


================================================
FILE: src/pages/home/SettlementPage.vue
================================================
<template>
  <div>
    <el-card class="box-card">
      <div slot="header" class="header">
        <span>我的结算单</span>
        <span class="comment">温馨提示:产品是否购买成功,以最终支付为准哦,请尽快完成结算</span>
      </div>
      <div class="content">
        <span class="sub-title">收件人信息:</span>
        <el-form label-width="80px" :model="purchase" style="display: inline-block; width: 700px">
          <el-form-item label="姓名">
            <el-input v-model="purchase.name"></el-input>
          </el-form-item>
          <el-form-item label="电话">
            <el-input v-model="purchase.telephone"></el-input>
          </el-form-item>
          <el-form-item label="城市">
            <v-distpicker @selected="onAddressSelected" :province="purchase.address.province"
                          :city="purchase.address.city" :area="purchase.address.area">
            </v-distpicker>
          </el-form-item>
          <el-form-item label="地址">
            <el-input v-model="purchase.location"></el-input>
          </el-form-item>
        </el-form>
        <div class="cover">
          <el-carousel height="350px">
            <el-carousel-item v-for="item in settlement.items" :key="item.id">
              <img :src="item.cover"/>
            </el-carousel-item>
          </el-carousel>
        </div>
        <span class="sub-title">支付方式:</span>
        <div style="padding: 0 0 20px 80px;">
          <el-radio v-model="purchase.pay" label="wechat" style="width: 130px" border> 微信支付</el-radio>
          <el-radio v-model="purchase.pay" label="alipay" style="width: 130px" border> 支付宝</el-radio>
        </div>
        <span class="sub-title">结算金额:</span>
        <div style="width: 100%">
          <span class="label">{{settlement.items.length}} 件商品,总商品金额:</span><span class="value"> {{totalAmount.toFixed(2)}} 元</span>
          <span class="label">运费:</span><span class="value"> 12.00 元</span>
          <span class="label">折扣:</span><span class="value"> 0.00 元</span>
          <div class="total">
            <span class="label" style="line-height: 40px">应付总额:</span><span class="value-large"> {{(totalAmount+12).toFixed(2)}}</span>
            <span class="value value-small">寄送至:{{fullAddress}}, 收件人:{{this.purchase.name}}, 电话:{{this.purchase.telephone}}</span>
          </div>
        </div>
        <el-button type="primary" class="submit-button" @click="prepareSettlement">提交订单</el-button>
      </div>
    </el-card>
    <el-card class="box-card" style="margin-top: 20px">
      <div slot="header" class="header">
        <span>送货清单</span>
      </div>
      <div class="content">
        <el-table :data="settlement.items" ref="settlementTable" style="width: 100%">
          <el-table-column prop="title" label="商品名称" sortable></el-table-column>
          <el-table-column prop="price" label="单价" width="100" sortable></el-table-column>
          <el-table-column prop="amount" label="数量" width="100" sortable></el-table-column>
          <el-table-column label="小计" width="100" sortable>
            <template slot-scope="scope">
              <span class="sub-total">{{scope.row.price * scope.row.amount}} 元</span>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-card>
    <PayStepIndicator :step="2"/>
  </div>
</template>

<script>
import VDistpicker from 'v-distpicker'
import PayStepIndicator from '@/components/home/cart/PayStepIndicator'
import {mapState, mapMutations, mapActions} from 'vuex'

export default {
  name: 'SettlementPage',
  components: {
    VDistpicker,
    PayStepIndicator
  },
  data () {
    return {
      // 为了便于页面修改,而不直接改变VUEX状态,最后统一提交,使用一个结构相同的中间对象
      purchase: {
        name: '',
        telephone: '',
        delivery: true,
        pay: 'wechat',
        address: {province: '广东省', city: '广州市', area: '海珠区'},
        location: ''
      }
    }
  },
  /**
   * 页面初始化时,从登陆用户中取默认值
   */
  created () {
    // 两层默认值:首先取登录信息中的用户数据,然后取页面转向之前设置的购买人数据。
    // 当用户以游客(未登录)身份购买时,当时无法获取默认购买人,需要转向登录页面,以登陆信息为默认值
    Object.assign(this.purchase, this.account)
    Object.assign(this.purchase, this.settlement.purchase)
  },
  computed: {
    ...mapState('user', ['account']),
    ...mapState('cart', ['settlement']),
    /**
     * 邮寄地址全称
     */
    fullAddress () {
      return `${this.purchase.address.province}  ${this.purchase.address.city} ${this.purchase.address.area} ${this.purchase.location || ''}`
    },
    /**
     * 总金额(不含运费)
     */
    totalAmount () {
      return this.settlement.items.reduce((sum, product) => sum + (product.price * product.amount), 0)
    }
  },
  methods: {
    ...mapMutations('cart', ['setupSettlementBill']),
    ...mapActions('cart', ['submitSettlement', 'setupSettlementBillWithDefaultValue']),
    /**
     * 地址选择控件的绑定事件,该控件未支持v-model
     */
    onAddressSelected (address) {
      this.purchase.address.province = address.province.value
      this.purchase.address.city = address.city.value
      this.purchase.address.area = address.area.value
    },
    /**
     * 将当前结算单据发送到服务端,并从服务端返回支付信息
     */
    async prepareSettlement () {
      this.setupSettlementBillWithDefaultValue({
        purchase: {...this.purchase, location: this.fullAddress}
      })
      await this.submitSettlement()
      this.$router.push('/pay')
    }
  }
}
</script>

<style scoped>
  .sub-total {
    color: red;
    font-weight: bold;
    font-size: 16px;
  }

  .cover {
    display: inline-block;
    float: right;
    width: 350px;
    position: relative;
    top: -30px;
    left: -100px;
  }

  .label, .value {
    color: #666;
    line-height: 24px;
  }

  .label {
    display: inline-block;
    width: 1100px;
    text-align: right;
  }

  .value {
    float: right;
    padding-right: 20px;
  }

  .value-large {
    color: red;
    font-size: 32px;
    font-weight: bold;
    float: right;
    padding-right: 20px;
  }

  .value-small {
    clear: both;
    font-size: 12px;
    color: #999;
    display: block;
  }

  .total {
    width: 100%;
    height: 70px;
    margin-top: 10px;
    padding-top: 10px;
    background-color: #f5f5f5;
    border: 1px;
    border-radius: 2px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)
  }

  .submit-button {
    float: right;
    margin: 20px;
    width: 200px;
    font-size: 24px;
    line-height: 32px;
    font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
  }

  .el-carousel__item h3 {
    color: #475669;
    font-size: 14px;
    opacity: 0.75;
    line-height: 150px;
    margin: 0;
  }

  .el-carousel__item:nth-child(2n) {
    background-color: #99a9bf;
  }

  .el-carousel__item:nth-child(2n+1) {
    background-color: #d3dce6;
  }
</style>


================================================
FILE: src/pages/home/WarehousePage.vue
================================================
<template>
  <el-card class="box-card">
    <div slot="header" class="header">
      <span>商品及库存管理</span>
      <span style="float: right;">
        <el-button type="primary" size="mini"
                   @click="stockMode=false; createMode=dialogFormVisible=true">新增商品</el-button>
      </span>
    </div>
    <div class="content">
      <el-table :data="products" ref="productTable" style="width: 100%">
        <el-table-column label="图片" width="150">
          <template slot-scope="scope">
            <img :src="scope.row.cover" style="width: 120px"/>
          </template>
        </el-table-column>
        <el-table-column prop="title" label="商品名称" sortable width="250"></el-table-column>
        <el-table-column prop="rate" label="评分" width="80" sortable></el-table-column>
        <el-table-column label="商品简介" sortable>
          <template slot-scope="scope">
            <span class="description">{{ pureText(scope.row.description) }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="price" label="单价" width="80" sortable></el-table-column>
        <el-table-column label="操作" width="140">
          <template slot-scope="scope">
            <el-link type="primary" @click="manageProduct(scope.row)">修改</el-link>
            <el-link type="primary" @click="manageStock(scope.row)">库存</el-link>
            <el-popconfirm confirmButtonText='确定' cancelButtonText='我手抖了' icon="el-icon-info" iconColor="red"
                           title="确定删除这个商品吗?" @onConfirm="removeProduct(scope.row.id)">
              <el-link slot="reference" type="danger">删除</el-link>
            </el-popconfirm>
          </template>
        </el-table-column>
      </el-table>
      <el-dialog title="商品信息" :visible.sync="dialogFormVisible">
        <ProductManage :product="product" :create-mode="createMode" v-show="!stockMode"
                       @dismiss="dialogFormVisible = false"
                       @updated="loadProducts"></ProductManage>
        <StockManage :product="product" :stock="stock" v-show="stockMode"
                     @dismiss="dialogFormVisible = false"></StockManage>
      </el-dialog>
    </div>
  </el-card>
</template>

<script>
import api from '@/api'
import {mapMutations} from 'vuex'
import ProductManage from '@/components/home/warehouse/ProductManage'
import StockManage from '@/components/home/warehouse/StockManage'

export default {
  name: 'WarehousePage',
  components: {
    ProductManage,
    StockManage
  },
  data () {
    return {
      products: [],
      product: {
        title: '',
        price: 0,
        rate: 0,
        cover: '',
        desc: '',
        description: '',
        specifications: []
      },
      stock: {
        amount: 0,
        frozen: 0
      },
      createMode: false,
      stockMode: false,
      dialogFormVisible: false
    }
  },
  created () {
    this.loadProducts()
  },
  methods: {
    ...mapMutations('cart', ['removeCartItem']),
    /**
     * 去除HTML标签
     */
    pureText (text) {
      return api.stringUtil.pureText(text)
    },
    /**
     * 加载全部商品信息
     */
    async loadProducts () {
      this.dialogFormVisible = false
      this.products = (await api.warehouse.getAllProducts()).data
    },
    /**
     * 修改特定商品
     */
    manageProduct (product) {
      // 属性都是单层的原始数据,做一次浅拷贝就够用了
      this.product = Object.assign(this.product, product)
      this.product.description = api.stringUtil.transToReturn(product.description)
      this.createMode = false
      this.stockMode = false
      this.dialogFormVisible = true
    },
    /**
     * 调整库存
     */
    async manageStock (product) {
      // 每次打开都实时请求一次库存
      let {data} = await api.warehouse.queryStock(product.id)
      this.stock = data
      this.product = product
      this.stockMode = true
      this.dialogFormVisible = true
    },
    /**
     *删除选定产品
     */
    async removeProduct (id) {
      try {
        await api.warehouse.removeProduct(id)
        // 如果购物车上有这个商品,也删除掉
        // 购物车是存储在客户端本地的,如果其他客户端保存了被删除的商品,需要在购物车页面处理,暂时没有做
        this.removeCartItem(id)
        this.loadProducts()
        this.$notify({title: '操作成功', message: '商品已删除', type: 'success'})
      } catch (e) {
        this.$notify({title: '操作失败', message: e.message, type: 'error'})
      }
    }
  }
}
</script>

<style scoped>
  .description {
    font-size: 12px;
    color: #999;
    text-align: left;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
  }
</style>


================================================
FILE: src/pages/home/index.vue
================================================
<template>
<!--  <div class="home-box">-->
    <el-container>
      <el-header direction-="vertical">
        <NavigationBar/>
      </el-header>
      <el-main>
        <div class="container">
          <router-view></router-view>
        </div>
      </el-main>
      <el-footer>
        <Copyright/>
      </el-footer>
    </el-container>
<!--  </div>-->
</template>

<script>
import NavigationBar from '@/components/home/NavigationBar'
import Copyright from '@/components/home/Copyright'

export default {
  name: 'index.vue',
  components: {
    Copyright,
    NavigationBar
  }
}
</script>

<style scoped>
  .container {
    display: block;
    text-align: center;
    margin: 40px;
    width: 1320px;
    height: 100%;
  }
</style>


================================================
FILE: src/plugins/errorhandler-plugin.js
================================================
/**
 * 针对vue\vuex\vue-router中的同步、异步异常信息做全局处理
 */
import Router from 'vue-router'

function isPromise (ret) {
  return (ret && typeof ret.then === 'function' && typeof ret.catch === 'function')
}

/**
 * 默认的全局异常处理器
 */
const defaultErrorHandler = (error, vm, info) => {
  console.error('接收到未处理的全局异常:', error)
  console.error(vm)
  console.error(info)
}

/**
 * 用于处理Promise的异步异常
 */
function registerActionHandle (actions, errorHandler) {
  Object.keys(actions).forEach(key => {
    let fn = actions[key]
    actions[key] = function (...args) {
      let ret = fn.apply(this, args)
      if (isPromise(ret)) {
        return ret.catch(errorHandler)
      } else { // 默认错误处理
        return ret
      }
    }
  })
}

const registerVuex = (instance, errorHandler) => {
  if (instance.$options['store']) {
    let actions = instance.$options['store']['_actions'] || {}
    if (actions) {
      let tempActions = {}
      Object.keys(actions).forEach(key => {
        tempActions[key] = actions[key][0]
      })
      registerActionHandle(tempActions, errorHandler)
    }
  }
}
const registerVue = (instance, errorHandler) => {
  if (instance.$options.methods) {
    let actions = instance.$options.methods || {}
    if (actions) {
      registerActionHandle(actions, errorHandler)
    }
  }
}

export default {
  install: (Vue, options) => {
    let errorHandler
    if (options && typeof options.errorHandler === 'function') {
      errorHandler = options.errorHandler
    } else {
      errorHandler = defaultErrorHandler
    }
    Vue.config.errorHandler = errorHandler
    Vue.mixin({
      beforeCreate () {
        registerVue(this, errorHandler)
        registerVuex(this, errorHandler)
      }
    })
    Vue.prototype.$throw = errorHandler

    /**
     * vue-router 3.1.x的兼容性修正:
     * 由于前面全局路由拦截器会使得不合法的页面请求跳转到登录页,push该页面的方法会被认为是路由失败。
     * 3.1.x将router.push方法返回一个Promise,如果不在push后面使用catch方法处理异常,将会把异常抛出到global的处理器中,然后在控制台打印警告,3.0.x无此问题
     * 以下代码用于兼容性修正,避免强制性地要求push方法后添加catch()
     */
    const originalPush = Router.prototype.push
    Router.prototype.push = function push (location, onResolve, onReject) {
      if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
      return originalPush.call(this, location).catch(error => error && errorHandler(error))
    }
  }
}


================================================
FILE: src/router/index.js
================================================
import Vue from 'vue'
import Router from 'vue-router'
import store from '@/store'

Vue.use(Router)

const router = new Router({
  routes: [
    {
      // 首页框架容器
      path: '/',
      component: () => import('@/pages/home/index'),
      children: [
        {
          // 书店首页
          path: '/',
          component: () => import('@/pages/home/MainPage')
        }, {
          // 商品详情页
          path: '/detail/:id',
          component: () => import('@/pages/home/DetailPage'),
          props: true
        }, {
          // 购物车
          path: '/cart',
          meta: {requireAuthentication: true},
          component: () => import('@/pages/home/CartPage')
        }, {
          // 商品结算页
          path: '/settle',
          meta: {requireAuthentication: true},
          component: () => import('@/pages/home/SettlementPage')
        }, {
          // 商品付款页
          path: '/pay',
          meta: {requireAuthentication: true},
          component: () => import('@/pages/home/PaymentPage')
        }, {
          // 库存管理页
          path: '/warehouse',
          meta: {requireAuthentication: true, requireAdministrator: true},
          component: () => import('@/pages/home/WarehousePage')
        }, {
          // 评论页
          path: '/comment',
          component: () => import('@/pages/home/CommentPage')
        }
      ]
    }, {
      // 登录页
      path: '/login',
      name: 'Login',
      component: () => import('@/pages/Login')
    }
  ]
})

/**
 * 全局路由拦截器
 * 用于实现在路由元数据中配置了需要认证的页面,如未检测到授权信息(默认为JWT令牌),或授权超时,着转向到登陆页面,登陆后跳转回原路由页面继续操作
 */
router.beforeEach((to, from, next) => {
  if (to.meta.requireAuthentication && !store.getters['user/isAuthorized']) {
    // 未登陆,则直接转到登陆
    next({name: 'Login', query: {redirect: to.fullPath}})
  } else {
    // 已登陆,但不是管理员,则转到首页
    if (to.meta.requireAdministrator && !store.getters['user/isAdministrator']) {
      next({path: '/'})
    } else {
      next()
    }
  }
})

export default router


================================================
FILE: src/store/constant.js
================================================
// cart
export const CART_ADD_PRODUCT_TO_CART = 'CART_ADD_PRODUCT_TO_CART' // 添加购物车
export const CART_DEL_PRODUCT_TO_CART = 'CART_DEL_PRODUCT_TO_CART' // 删除购物车
export const CART_ADD_PRODUCT_QUANTITY = 'CART_ADD_PRODUCT_QUANTITY' // 添加商品数量
export const CART_DEL_PRODUCT_QUANTITY = 'CART_DEL_PRODUCT_QUANTITY' // 减少商品数量
export const CART_SET_CHECKOUT_STATUS = 'CART_SET_CHECKOUT_STATUS' // 改变商品购买状态的
export const CART_SET_CHECKOUT_STATUS_ALL = 'CART_SET_CHECKOUT_STATUS_ALL' // 一键改变所有商品购买状态的方法

// products
export const PRODUCTS_SET_PRODUCT = 'PRODUCTS_SET_PRODUCT' // 获取所有商品的列表

// user
export const USER_CHANGE_LOGIN = 'USER_CHANGE_LOGIN' // 改变用户的登陆状态
export const USER_EXIT_STATUS = 'USER_EXIT_STATUS' // 退出登录状态


================================================
FILE: src/store/index.js
================================================
import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart'
import products from './modules/products'
import user from './modules/user'
import notification from './modules/notification'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    user,
    notification,
    cart,
    products
  }
})


================================================
FILE: src/store/modules/cart.js
================================================
import api from '@/api'

/**
 * 购物车状态数据
 * 一共存有三类状态:
 *  - 购物车元素(items)
 *  - 结算单(settlement),即本次打算购买的内容,以及派送信息
 *  - 支付单(payment),将结算单发送到服务端后,服务端返回有货物、可派送,此时包含支付二维码等付款信息
 * 购物车与结算账单中存储的数组元素,内容上与商品对象相同,唯一只是多了一个表示数量的amount字段
 */
const state = {
  // 购物车里的商品
  // 在购物车中的与在结算账单中含义并不相同,毕竟不是所有人都能随意清空购物车
  items: [],
  // 结算账单,这个是进入结算页面之前设置好的
  settlement: {
    // 账单配送地址
    purchase: {
      name: '',
      telephone: '',
      delivery: true,
      address: {province: '广东省', city: '广州市', area: '海珠区'},
      location: ''
    },
    // 账单内容
    items: []
  },
  // 支付信息,由服务端返回,包括支付结果Code,支付单ID和用于付款的二维码
  payment: {
    code: -1,
    id: '',
    qrcode: '',
    expires: 0
  }
}

const getters = {}

const mutations = {
  /**
   * 调整在购物车中指定产品的数量
   * 如购物车中已有该产品则直接修改数量,如果没有,将对象的浅拷贝存入购物车
   * 数量可为负数,用于对购物车中产品的调减,如产品调减后结果小于零,则直接将数量归零,同时最大值也不允许超过10
   */
  adjustCartItems (state, product) {
    let item = state.items.find(item => item.id === product.id)
    if (item) {
      item.amount = Math.min(10, (item.amount + product.amount) || 0)
    } else {
      product.amount = Math.min(10, product.amount || 0)
      state.items.push({...product})
    }
  },

  /**
   * 添加一个商品到购物车之中
   * 如果购物车中已经有了这个商品,就把数量加1
   */
  addCartItem (state, product) {
    let item = state.items.find(i => i.id === product.id)
    if (item) {
      item.amount++
    } else {
      state.items.push({...product, amount: 1})
    }
  },

  /**
   * 删除购物车中指定产品
   * 购物车可以存在数量为0的产品,并不会删除,如需彻底从购物车移除产品,应使用本方法
   * 如果购物车中原本就没有该产品,则不会有任何效果
   */
  removeCartItem (state, id) {
    state.items = state.items.filter(i => i.id !== id)
  },

  /**
   * 设置结算单
   * 外部一般不调用该方法,而是使用Actions中的setupSettlementBillWithDefaultValue
   */
  setupSettlementBill (state, settlement) {
    state.settlement = settlement
  },

  /**
   * 设置支付信息
   * 外部一般不调用该方法,而是使用Actions中的submitSettlement
   */
  receivePayment (state, payment) {
    state.payment = payment
  }
}

const actions = {
  /**
   * 设置结算账单
   * 为了便于使用,配送人、地址等信息,如果对应字段没有被设置,会取用户账号的信息信息作为默认值(即默认收件人是用户自己)
   */
  setupSettlementBillWithDefaultValue ({state, rootState, commit}, settlement) {
    // 设置结算单的默认值,传入的结算单就不需要包括所有的字段
    const defaultPurchase = {
      name: rootState.user.account.name,
      telephone: rootState.user.account.telephone,
      delivery: true,
      address: {province: '广东省', city: '广州市', area: '海珠区'},
      location: rootState.user.account.location
    }
    settlement.purchase = Object.assign(defaultPurchase, settlement.purchase || {})
    commit('setupSettlementBill', Object.assign(state.settlement, settlement))
  },

  /**
   * 提交要购买的商品和配送信息到服务端
   * 在调用此方法之前,通常应该调用setupSettlementBillWithDefaultValue来设置VUEX中的结算单据信息
   */
  async submitSettlement ({state, commit}) {
    // 这里提交的数据(items数组)可以清理一下,只提交id即可,减少网络传输的数据
    const settlement = {
      items: state.settlement.items.map(i => {
        return {amount: i.amount, id: i.id}
      }),
      purchase: state.settlement.purchase
    }
    let res
    try {
      res = (await api.payment.submitSettlement(settlement)).data
      // 将超时的相对时间转为绝对时间
      res.expires += new Date().getTime()
    } catch (e) {
      res = {message: e.message}
    }
    commit('receivePayment', res)
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}


================================================
FILE: src/store/modules/notification.js
================================================
const state = {
  // 没有被捕获,抛到全局的异常
  exception: null,
  // 服务端发来的通知消息
  notification: null
}

const getters = {}

const mutations = {
  /**
   * 设置全局异常,会在vue\vuex的异常处理中注册
   *
   * @param state
   * @param exception
   */
  setException (state, exception) {
    state.exception = exception
  },

  /**
   * 清理全局异常
   *
   * @param state
   */
  clearException (state) {
    state.exception = null
  }
}

const actions = {}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}


================================================
FILE: src/store/modules/products.js
================================================
const state = {
  favorite: []
}

const getters = {}

const mutations = {}

const actions = {
  // vuex 给actions 的 commit 提交到 mutations => state
  // getAllProducts ({commit}) { // 所有的api请求都放在actions中
  //   fetchGet('/cart').then(res => {
  //     let allProducts = res.data.list.list
  //     commit(types.PRODUCTS_SET_PRODUCT, allProducts)
  //   })
  // }
}

export default {
  namespaced: true, // 添加命名空间
  state,
  getters,
  mutations,
  actions
}


================================================
FILE: src/store/modules/user.js
================================================
import api from '@/api'

const EMPTY_SESSION = () => ({
  username: null,
  scope: '',
  expires: 0,
  access_token: '',
  refresh_token: '',
  authorities: [],
  token_type: 'bearer',
  jti: '',
  // 以下为客户端自定义信息
  rememberMe: false,
  language: ''
})

const EMPTY_ACCOUNT = () => ({
  id: null,
  username: null,
  name: '',
  avatar: '',
  telephone: '',
  email: '',
  location: ''
})

// 如果本地存储有缓存的Session信息,启动时尝试从该信息中恢复
// 这里采用Object.assign主要是为了对服务端做一个容错,以便客户端依赖的所有属性都至少会有一个默认值,而不会出现服务端未返回时的undefined
// 注意,将JWT存储在sessionStorage或localStorage中是存在XSS风险的,专门写一篇文章来分析JWT这方面的内容
const state = {
  session: api.option.hasSession() ? Object.assign(EMPTY_SESSION(), api.option.getSession()) : EMPTY_SESSION(),
  account: EMPTY_ACCOUNT(),
  // 收藏夹,只存在本地,暂时没有什么用
  favorite: []
}

const getters = {
  /**
   * 检查授权是否有效
   * 生效要求:持有JWT令牌,且并未超出令牌期限
   */
  isAuthorized: state => !!state.session.access_token && (state.session.expires > new Date().getTime()),
  /**
   * 检查是否管理员
   * 生效要求:已获得登录授权,并且角色中存在ROLE_ADMIN
   */
  isAdministrator: (state, getters) => getters.isAuthorized && state.session.authorities.includes('ROLE_ADMIN')
}

const mutations = {

  /**
   * 登陆成功,设置登录状态
   *
   * @param state
   * @param session 客户端的Session
   *
   * 服务端不存储状态,登陆成功后用户状态信息通过JWT返回浏览器,存储在客户端Session中,其结构为:
   * { username, scope, expires, access_token, refresh_token, token_type, jti }
   * 另,根据用户在客户端登陆时的选项,以下内容也被添加到客户端Session中:
   * { rememberMe, location }
   */
  setupSession (state, session) {
    // 服务端传来的session过期时间是相对时间(因为服务端、客户端的时间可能不一致),存储到本地时,转为绝对时间戳
    // 注意服务端返回的时间单位是秒,客户端的时间戳是毫秒
    session.expires = new Date().getTime() + (session.expires_in * 1000)
    Object.assign(state.session, session)
    api.option.setSession(session)
  },

  /**
   * 退出登陆状态
   * 清理vuex中的所有状态(Session、Account),以及本地存储中的Session
   */
  clearSession (state) {
    state.session = EMPTY_SESSION()
    state.account = EMPTY_ACCOUNT()
    api.option.removeSession()
  },

  /**
   * 更新用户账号资料
   */
  updateAccount (state, account) {
    Object.assign(state.account, account)
  },

  /**
   * 增加一项收藏
   */
  addFavorite (state, id) {
    state.favorite = [...state.favorite, id]
  },

  /**
   * 删除一项收藏
   */
  removeFavorite (state, id) {
    state.favorite = state.favorite.filter(x => x !== id)
  }
}

const actions = {
  /**
   * 根据过期时间刷新OAuth令牌的触发器
   */
  refreshSessionTrigger ({dispatch, commit, state}) {
    // Session是具有有效期的,设置更新令牌的触发器
    let timeout = state.session.expires - new Date().getTime()
    if (timeout > 0) {
      console.log(`Session将在:${timeout}毫秒后过期,届时会重刷新令牌`)
      setTimeout(() => {
        dispatch('refreshSession', {commit, state}).then(() => {
          dispatch('refreshSessionTrigger', {commit, state})
        })
      }, timeout)
    }
  },

  /**
   * 向服务端请求新的访问令牌
   */
  async refreshSession ({commit, state}) {
    try {
      let {data} = await api.auth.refresh(state.session.refresh_token)
      commit('setupSession', data)
    } catch (e) {
      // 刷新失败,就清理掉当前的用户
      commit('clearSession')
    }
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}


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


================================================
FILE: static/board/gitalk.css
================================================
@font-face {
  font-family: octicons-link;
  src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
}

.markdown-body {
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  line-height: 1.5;
  color: #24292e;
  /*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";*/
  font-size: 16px;
  line-height: 1.5;
  word-wrap: break-word;
}

.markdown-body .pl-c {
  color: #6a737d;
}

.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
  color: #005cc5;
}

.markdown-body .pl-e,
.markdown-body .pl-en {
  color: #6f42c1;
}

.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
  color: #24292e;
}

.markdown-body .pl-ent {
  color: #22863a;
}

.markdown-body .pl-k {
  color: #d73a49;
}

.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
  color: #032f62;
}

.markdown-body .pl-v,
.markdown-body .pl-smw {
  color: #e36209;
}

.markdown-body .pl-bu {
  color: #b31d28;
}

.markdown-body .pl-ii {
  color: #fafbfc;
  background-color: #b31d28;
}

.markdown-body .pl-c2 {
  color: #fafbfc;
  background-color: #d73a49;
}

.markdown-body .pl-c2::before {
  content: "^M";
}

.markdown-body .pl-sr .pl-cce {
  font-weight: bold;
  color: #22863a;
}

.markdown-body .pl-ml {
  color: #735c0f;
}

.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
  font-weight: bold;
  color: #005cc5;
}

.markdown-body .pl-mi {
  font-style: italic;
  color: #24292e;
}

.markdown-body .pl-mb {
  font-weight: bold;
  color: #24292e;
}

.markdown-body .pl-md {
  color: #b31d28;
  background-color: #ffeef0;
}

.markdown-body .pl-mi1 {
  color: #22863a;
  background-color: #f0fff4;
}

.markdown-body .pl-mc {
  color: #e36209;
  background-color: #ffebda;
}

.markdown-body .pl-mi2 {
  color: #f6f8fa;
  background-color: #005cc5;
}

.markdown-body .pl-mdr {
  font-weight: bold;
  color: #6f42c1;
}

.markdown-body .pl-ba {
  color: #586069;
}

.markdown-body .pl-sg {
  color: #959da5;
}

.markdown-body .pl-corl {
  text-decoration: underline;
  color: #032f62;
}

.markdown-body .octicon {
  display: inline-block;
  vertical-align: text-top;
  fill: currentColor;
}

.markdown-body a {
  background-color: transparent;
  -webkit-text-decoration-skip: objects;
}

.markdown-body a:active,
.markdown-body a:hover {
  outline-width: 0;
}

.markdown-body strong {
  font-weight: inherit;
}

.markdown-body strong {
  font-weight: bolder;
}

.markdown-body h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

.markdown-body img {
  border-style: none;
}

.markdown-body svg:not(:root) {
  overflow: hidden;
}

.markdown-body code,
.markdown-body kbd,
.markdown-body pre {
  font-family: monospace, monospace;
  font-size: 1em;
}

.markdown-body hr {
  -webkit-box-sizing: content-box;
          box-sizing: content-box;
  height: 0;
  overflow: visible;
}

.markdown-body input {
  font: inherit;
  margin: 0;
}

.markdown-body input {
  overflow: visible;
}

.markdown-body [type="checkbox"] {
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  padding: 0;
}

.markdown-body * {
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
}

.markdown-body input {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
}

.markdown-body a {
  color: #0366d6;
  text-decoration: none;
}

.markdown-body a:hover {
  text-decoration: underline;
}

.markdown-body strong {
  font-weight: 600;
}

.markdown-body hr {
  height: 0;
  margin: 15px 0;
  overflow: hidden;
  background: transparent;
  border: 0;
  border-bottom: 1px solid #dfe2e5;
}

.markdown-body hr::before {
  display: table;
  content: "";
}

.markdown-body hr::after {
  display: table;
  clear: both;
  content: "";
}

.markdown-body table {
  border-spacing: 0;
  border-collapse: collapse;
}

.markdown-body td,
.markdown-body th {
  padding: 0;
}

.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
  margin-top: 0;
  margin-bottom: 0;
}

.markdown-body h1 {
  font-size: 32px;
  font-weight: 600;
}

.markdown-body h2 {
  font-size: 24px;
  font-weight: 600;
}

.markdown-body h3 {
  font-size: 20px;
  font-weight: 600;
}

.markdown-body h4 {
  font-size: 16px;
  font-weight: 600;
}

.markdown-body h5 {
  font-size: 14px;
  font-weight: 600;
}

.markdown-body h6 {
  font-size: 12px;
  font-weight: 600;
}

.markdown-body p {
  margin-top: 0;
  margin-bottom: 10px;
}

.markdown-body blockquote {
  margin: 0;
}

.markdown-body ul,
.markdown-body ol {
  padding-left: 0;
  margin-top: 0;
  margin-bottom: 0;
}

.markdown-body ol ol,
.markdown-body ul ol {
  list-style-type: lower-roman;
}

.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
  list-style-type: lower-alpha;
}

.markdown-body dd {
  margin-left: 0;
}

.markdown-body code {
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
  font-size: 12px;
}

.markdown-body pre {
  margin-top: 0;
  margin-bottom: 0;
  font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
}

.markdown-body .octicon {
  vertical-align: text-bottom;
}

.markdown-body .pl-0 {
  padding-left: 0 !important;
}

.markdown-body .pl-1 {
  padding-left: 4px !important;
}

.markdown-body .pl-2 {
  padding-left: 8px !important;
}

.markdown-body .pl-3 {
  padding-left: 16px !important;
}

.markdown-body .pl-4 {
  padding-left: 24px !important;
}

.markdown-body .pl-5 {
  padding-left: 32px !important;
}

.markdown-body .pl-6 {
  padding-left: 40px !important;
}

.markdown-body::before {
  display: table;
  content: "";
}

.markdown-body::after {
  display: table;
  clear: both;
  content: "";
}

.markdown-body>*:first-child {
  margin-top: 0 !important;
}

.markdown-body>*:last-child {
  margin-bottom: 0 !important;
}

.markdown-body a:not([href]) {
  color: inherit;
  text-decoration: none;
}

.markdown-body .anchor {
  float: left;
  padding-right: 4px;
  margin-left: -20px;
  line-height: 1;
}

.markdown-body .anchor:focus {
  outline: none;
}

.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre {
  margin-top: 0;
  margin-bottom: 16px;
}

.markdown-body hr {
  height: 0.25em;
  padding: 0;
  margin: 24px 0;
  background-color: #e1e4e8;
  border: 0;
}

.markdown-body blockquote {
  padding: 0 1em;
  color: #6a737d;
  border-left: 0.25em solid #dfe2e5;
}

.markdown-body blockquote>:first-child {
  margin-top: 0;
}

.markdown-body blockquote>:last-child {
  margin-bottom: 0;
}

.markdown-body kbd {
  display: inline-block;
  padding: 3px 5px;
  font-size: 11px;
  line-height: 10px;
  color: #444d56;
  vertical-align: middle;
  background-color: #fafbfc;
  border: solid 1px #c6cbd1;
  border-bottom-color: #959da5;
  border-radius: 3px;
  -webkit-box-shadow: inset 0 -1px 0 #959da5;
          box-shadow: inset 0 -1px 0 #959da5;
}

.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
  margin-top: 24px;
  margin-bottom: 16px;
  font-weight: 600;
  line-height: 1.25;
}

.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
  color: #1b1f23;
  vertical-align: middle;
  visibility: hidden;
}

.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
  text-decoration: none;
}

.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
  visibility: visible;
}

.markdown-body h1 {
  padding-bottom: 0.3em;
  font-size: 2em;
  border-bottom: 1px solid #eaecef;
}

.markdown-body h2 {
  padding-bottom: 0.3em;
  font-size: 1.5em;
  border-bottom: 1px solid #eaecef;
}

.markdown-body h3 {
  font-size: 1.25em;
}

.markdown-body h4 {
  font-size: 1em;
}

.markdown-body h5 {
  font-size: 0.875em;
}

.markdown-body h6 {
  font-size: 0.85em;
  color: #6a737d;
}

.markdown-body ul,
.markdown-body ol {
  padding-left: 2em;
}

.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
  margin-top: 0;
  margin-bottom: 0;
}

.markdown-body li>p {
  margin-top: 16px;
}

.markdown-body li+li {
  margin-top: 0.25em;
}

.markdown-body dl {
  padding: 0;
}

.markdown-body dl dt {
  padding: 0;
  margin-top: 16px;
  font-size: 1em;
  font-style: italic;
  font-weight: 600;
}

.markdown-body dl dd {
  padding: 0 16px;
  margin-bottom: 16px;
}

.markdown-body table {
  display: block;
  width: 100%;
  overflow: auto;
}

.markdown-body table th {
  font-weight: 600;
}

.markdown-body table th,
.markdown-body table td {
  padding: 6px 13px;
  border: 1px solid #dfe2e5;
}

.markdown-body table tr {
  background-color: #fff;
  border-top: 1px solid #c6cbd1;
}

.markdown-body table tr:nth-child(2n) {
  background-color: #f6f8fa;
}

.markdown-body img {
  max-width: 100%;
  -webkit-box-sizing: content-box;
          box-sizing: content-box;
  background-color: #fff;
}

.markdown-body code {
  padding: 0;
  padding-top: 0.2em;
  padding-bottom: 0.2em;
  margin: 0;
  font-size: 85%;
  background-color: rgba(27,31,35,0.05);
  border-radius: 3px;
}

.markdown-body code::before,
.markdown-body code::after {
  letter-spacing: -0.2em;
  content: "\A0";
}

.markdown-body pre {
  word-wrap: normal;
}

.markdown-body pre>code {
  padding: 0;
  margin: 0;
  font-size: 100%;
  word-break: normal;
  white-space: pre;
  background: transparent;
  border: 0;
}

.markdown-body .highlight {
  margin-bottom: 16px;
}

.markdown-body .highlight pre {
  margin-bottom: 0;
  word-break: normal;
}

.markdown-body .highlight pre,
.markdown-body pre {
  padding: 16px;
  overflow: auto;
  font-size: 85%;
  line-height: 1.45;
  background-color: #f6f8fa;
  border-radius: 3px;
}

.markdown-body pre code {
  display: inline;
  max-width: auto;
  padding: 0;
  margin: 0;
  overflow: visible;
  line-height: inherit;
  word-wrap: normal;
  background-color: transparent;
  border: 0;
}

.markdown-body pre code::before,
.markdown-body pre code::after {
  content: normal;
}

.markdown-body .full-commit .btn-outline:not(:disabled):hover {
  color: #005cc5;
  border-color: #005cc5;
}

.markdown-body kbd {
  display: inline-block;
  padding: 3px 5px;
  font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
  line-height: 10px;
  color: #444d56;
  vertical-align: middle;
  background-color: #fafbfc;
  border: solid 1px #d1d5da;
  border-bottom-color: #c6cbd1;
  border-radius: 3px;
  -webkit-box-shadow: inset 0 -1px 0 #c6cbd1;
          box-shadow: inset 0 -1px 0 #c6cbd1;
}

.markdown-body :checked+.radio-label {
  position: relative;
  z-index: 1;
  border-color: #0366d6;
}

.markdown-body .task-list-item {
  list-style-type: none;
}

.markdown-body .task-list-item+.task-list-item {
  margin-top: 3px;
}

.markdown-body .task-list-item input {
  margin: 0 0.2em 0.25em -1.6em;
  vertical-align: middle;
}

.markdown-body hr {
  border-bottom-color: #eee;
}
/* variables */
/* functions & mixins */
/* variables - calculated */
/* styles */
.gt-container {
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  font-size: 16px;
/* loader */
/* error */
/* initing */
/* no int */
/* link */
/* meta */
/* popup */
/* header */
/* comments */
/* comment */
}
.gt-container * {
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
}
.gt-container a {
  color: #6190e8;
}
.gt-container a:hover {
  color: #81a6ed;
  border-color: #81a6ed;
}
.gt-container a.is--active {
  color: #333;
  cursor: default !important;
}
.gt-container a.is--active:hover {
  color: #333;
}
.gt-container .hide {
  display: none !important;
}
.gt-container .gt-svg {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: sub;
}
.gt-container .gt-svg svg {
  width: 100%;
  height: 100%;
  fill: #6190e8;
}
.gt-container .gt-ico {
  display: inline-block;
}
.gt-container .gt-ico-text {
  margin-left: 0.3125em;
}
.gt-container .gt-ico-github {
  width: 100%;
  height: 100%;
}
.gt-container .gt-ico-github .gt-svg {
  width: 100%;
  height: 100%;
}
.gt-container .gt-ico-github svg {
  fill: inherit;
}
.gt-container .gt-spinner {
  position: relative;
}
.gt-container .gt-spinner::before {
  content: '';
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  position: absolute;
  top: 3px;
  width: 0.75em;
  height: 0.75em;
  margin-top: -0.1875em;
  margin-left: -0.375em;
  border-radius: 50%;
  border: 1px solid #fff;
  border-top-color: #6190e8;
  -webkit-animation: gt-kf-rotate 0.6s linear infinite;
          animation: gt-kf-rotate 0.6s linear infinite;
}
.gt-container .gt-loader {
  position: relative;
  border: 1px solid #999;
  -webkit-animation: ease gt-kf-rotate 1.5s infinite;
          animation: ease gt-kf-rotate 1.5s infinite;
  display: inline-block;
  font-style: normal;
  width: 1.75em;
  height: 1.75em;
  line-height: 1.75em;
  border-radius: 50%;
}
.gt-container .gt-loader:before {
  content: '';
  position: absolute;
  display: block;
  top: 0;
  left: 50%;
  margin-top: -0.1875em;
  margin-left: -0.1875em;
  width: 0.375em;
  height: 0.375em;
  background-color: #999;
  border-radius: 50%;
}
.gt-container .gt-avatar {
  display: inline-block;
  width: 3.125em;
  height: 3.125em;
}
@media (max-width: 479px) {
  .gt-container .gt-avatar {
    width: 2em;
    height: 2em;
  }
}
.gt-container .gt-avatar img {
  width: 100%;
  height: auto;
  border-radius: 3px;
}
.gt-container .gt-avatar-github {
  width: 3em;
  height: 3em;
}
@media (max-width: 479px) {
  .gt-container .gt-avatar-github {
    width: 1.875em;
    height: 1.875em;
  }
}
.gt-container .gt-btn {
  padding: 0.75em 1.25em;
  display: inline-block;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  border: 1px solid #6190e8;
  border-radius: 5px;
  background-color: #6190e8;
  color: #fff;
  outline: none;
  font-size: 0.75em;
}
.gt-container .gt-btn-text {
  font-weight: 400;
}
.gt-container .gt-btn-loading {
  position: relative;
  margin-left: 0.5em;
  display: inline-block;
  width: 0.75em;
  height: 1em;
  vertical-align: top;
}
.gt-container .gt-btn.is--disable {
  cursor: not-allowed;
  opacity: 0.5;
}
.gt-container .gt-btn-login {
  margin-right: 0;
}
.gt-container .gt-btn-preview {
  background-color: #fff;
  color: #6190e8;
}
.gt-container .gt-btn-preview:hover {
  background-color: #f2f2f2;
  border-color: #81a6ed;
}
.gt-container .gt-btn-public:hover {
  background-color: #81a6ed;
  border-color: #81a6ed;
}
.gt-container .gt-error {
  text-align: center;
  margin: 0.625em;
  color: #ff3860;
}
.gt-container .gt-initing {
  padding: 1.25em 0;
  text-align: center;
}
.gt-container .gt-initing-text {
  margin: 0.625em auto;
  font-size: 92%;
}
.gt-container .gt-no-init {
  padding: 1.25em 0;
  text-align: center;
}
.gt-container .gt-link {
  border-bottom: 1px dotted #6190e8;
}
.gt-container .gt-link-counts,
.gt-container .gt-link-project {
  text-decoration: none;
}
.gt-container .gt-meta {
  margin: 0 0 1.25em 0;
  padding: 1em 0;
  position: relative;
  border-bottom: 1px solid #e9e9e9;
  font-size: 1em;
  position: relative;
  z-index: 10;
}
.gt-container .gt-meta:before,
.gt-container .gt-meta:after {
  content: " ";
  display: table;
}
.gt-container .gt-meta:after {
  clear: both;
}
.gt-container .gt-counts {
  margin: 0 0.625em 0 0;
}
.gt-container .gt-user {
  float: right;
  margin: 0;
  font-size: 92%;
}
.gt-container .gt-user-pic {
  width: 16px;
  height: 16px;
  vertical-align: top;
  margin-right: 0.5em;
}
.gt-container .gt-user-inner {
  display: inline-block;
  cursor: pointer;
}
.gt-container .gt-user .gt-ico {
  margin: 0 0 0 0.3125em;
}
.gt-container .gt-user .gt-ico svg {
  fill: inherit;
}
.gt-container .gt-user .is--poping .gt-ico svg {
  fill: #6190e8;
}
.gt-container .gt-version {
  color: #a1a1a1;
  margin-left: 0.375em;
}
.gt-container .gt-copyright {
  margin: 0 0.9375em 0.5em;
  border-top: 1px solid #e9e9e9;
  padding-top: 0.5em;
}
.gt-container .gt-popup {
  position: absolute;
  right: 0;
  top: 2.375em;
  background: #fff;
  display: inline-block;
  border: 1px solid #e9e9e9;
  padding: 0.625em 0;
  font-size: 0.875em;
  letter-spacing: 0.5px;
}
.gt-container .gt-popup .gt-action {
  cursor: pointer;
  display: block;
  margin: 0.5em 0;
  padding: 0 1.125em;
  position: relative;
  text-decoration: none;
}
.gt-container .gt-popup .gt-action.is--active:before {
  content: '';
  width: 0.25em;
  height: 0.25em;
  background: #6190e8;
  position: absolute;
  left: 0.5em;
  top: 0.4375em;
}
.gt-container .gt-header {
  position: relative;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}
.gt-container .gt-header-comment {
  -webkit-box-flex: 1;
      -ms-flex: 1;
          flex: 1;
  margin-left: 1.25em;
}
@media (max-width: 479px) {
  .gt-container .gt-header-comment {
    margin-left: 0.875em;
  }
}
.gt-container .gt-header-textarea {
  padding: 0.75em;
  display: block;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  width: 100%;
  min-height: 5.125em;
  max-height: 15em;
  border-radius: 5px;
  border: 1px solid rgba(0,0,0,0.1);
  font-size: 0.875em;
  word-wrap: break-word;
  resize: vertical;
  background-color: #f6f6f6;
  outline: none;
  -webkit-transition: all 0.25s ease;
  transition: all 0.25s ease;
}
.gt-container .gt-header-textarea:hover {
  background-color: #fbfbfb;
}
.gt-container .gt-header-preview {
  padding: 0.75em;
  border-radius: 5px;
  border: 1px solid rgba(0,0,0,0.1);
  background-color: #f6f6f6;
}
.gt-container .gt-header-controls {
  position: relative;
  margin: 0.75em 0 0;
}
.gt-container .gt-header-controls:before,
.gt-container .gt-header-controls:after {
  content: " ";
  display: table;
}
.gt-container .gt-header-controls:after {
  clear: both;
}
@media (max-width: 479px) {
  .gt-container .gt-header-controls {
    margin: 0;
  }
}
.gt-container .gt-header-controls-tip {
  font-size: 0.875em;
  color: #6190e8;
  text-decoration: none;
  vertical-align: sub;
}
@media (max-width: 479px) {
  .gt-container .gt-header-controls-tip {
    display: none;
  }
}
.gt-container .gt-header-controls .gt-btn {
  float: right;
  margin-left: 1.25em;
}
@media (max-width: 479px) {
  .gt-container .gt-header-controls .gt-btn {
    float: none;
    width: 100%;
    margin: 0.75em 0 0;
  }
}
.gt-container:after {
  content: '';
  position: fixed;
  bottom: 100%;
  left: 0;
  right: 0;
  top: 0;
  opacity: 0;
}
.gt-container.gt-input-focused {
  position: relative;
}
.gt-container.gt-input-focused:after {
  content: '';
  position: fixed;
  bottom: 0%;
  left: 0;
  right: 0;
  top: 0;
  background: #000;
  opacity: 0.6;
  -webkit-transition: opacity 0.3s, bottom 0s;
  transition: opacity 0.3s, bottom 0s;
  z-index: 9999;
}
.gt-container.gt-input-focused .gt-header-comment {
  z-index: 10000;
}
.gt-container .gt-comments {
  padding-top: 1.25em;
}
.gt-container .gt-comments-null {
  text-align: center;
}
.gt-container .gt-comments-controls {
  margin: 1.25em 0;
  text-align: center;
}
.gt-container .gt-comment {
  position: relative;
  padding: 0.625em 0;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}
.gt-container .gt-comment-content {
  -webkit-box-flex: 1;
      -ms-flex: 1;
          flex: 1;
  margin-left: 1.25em;
  padding: 0.75em 1em;
  background-color: #f9f9f9;
  overflow: auto;
  -webkit-transition: all ease 0.25s;
  transition: all ease 0.25s;
}
.gt-container .gt-comment-content:hover {
  -webkit-box-shadow: 0 0.625em 3.75em 0 #f4f4f4;
          box-shadow: 0 0.625em 3.75em 0 #f4f4f4;
}
@media (max-width: 479px) {
  .gt-container .gt-comment-content {
    margin-left: 0.875em;
    padding: 0.625em 0.75em;
  }
}
.gt-container .gt-comment-header {
  margin-bottom: 0.5em;
  font-size: 0.875em;
  position: relative;
}
.gt-container .gt-comment-block-1 {
  float: right;
  height: 1.375em;
  width: 2em;
}
.gt-container .gt-comment-block-2 {
  float: right;
  height: 1.375em;
  width: 4em;
}
.gt-container .gt-comment-username {
  font-weight: 500;
  color: #6190e8;
  text-decoration: none;
}
.gt-container .gt-comment-username:hover {
  text-decoration: underline;
}
.gt-container .gt-comment-text {
  margin-left: 0.5em;
  color: #a1a1a1;
}
.gt-container .gt-comment-date {
  margin-left: 0.5em;
  color: #a1a1a1;
}
.gt-container .gt-comment-like,
.gt-container .gt-comment-edit,
.gt-container .gt-comment-reply {
  position: absolute;
  height: 1.375em;
}
.gt-container .gt-comment-like:hover,
.gt-container .gt-comment-edit:hover,
.gt-container .gt-comment-reply:hover {
  cursor: pointer;
}
.gt-container .gt-comment-like {
  top: 0;
  right: 2em;
}
.gt-container .gt-comment-edit,
.gt-container .gt-comment-reply {
  top: 0;
  right: 0;
}
.gt-container .gt-comment-body {
  color: #333 !important;
}
.gt-container .gt-comment-body .email-hidden-toggle a {
  display: inline-block;
  height: 12px;
  padding: 0 9px;
  font-size: 12px;
  font-weight: 600;
  line-height: 6px;
  color: #444d56;
  text-decoration: none;
  vertical-align: middle;
  background: #dfe2e5;
  border-radius: 1px;
}
.gt-container .gt-comment-body .email-hidden-toggle a:hover {
  background-color: #c6cbd1;
}
.gt-container .gt-comment-body .email-hidden-reply {
  display: none;
  white-space: pre-wrap;
}
.gt-container .gt-comment-body .email-hidden-reply .email-signature-reply {
  padding: 0 15px;
  margin: 15px 0;
  color: #586069;
  border-left: 4px solid #dfe2e5;
}
.gt-container .gt-comment-body .email-hidden-reply.expanded {
  display: block;
}
.gt-container .gt-comment-admin .gt-comment-content {
  background-color: #f6f9fe;
}
@-webkit-keyframes gt-kf-rotate {
  0% {
    -webkit-transform: rotate(0);
            transform: rotate(0);
  }
  100% {
    -webkit-transform: rotate(360deg);
            transform: rotate(360deg);
  }
}
@keyframes gt-kf-rotate {
  0% {
    -webkit-transform: rotate(0);
            transform: rotate(0);
  }
  100% {
    -webkit-transform: rotate(360deg);
            transform: rotate(360deg);
  }
}

/*# sourceMappingURL=gitalk.css.map*/


================================================
FILE: static/board/gitalk.html
================================================
<link rel="stylesheet" href="gitalk.css">
<script src="gitalk.min.js"></script>
<html>
	<body>
		<div id="container"></div>
	</body>
	<script>
	var gitalk = new Gitalk({
		clientID: '5d62946f158cf1316fc4',
		clientSecret: '7011b262282f7cdc7e3c4e015b39cd0187e480d9',
		repo: 'fenix-bookstore-frontend',
		owner: 'fenixsoft',
		admin: ['fenixsoft'],
		id: 'pure_frontend_bookstore',
		title: 'fenix_bookstore_comment',
		distractionFreeMode: false
	})
	gitalk.render('container')
	</script>
</html>


================================================
FILE: travis_docker_push.sh
================================================
#!/bin/bash
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
docker build -t bookstore:frontend .
docker images
docker tag bookstore:frontend $DOCKER_USERNAME/bookstore:frontend
docker push $DOCKER_USERNAME/bookstore:frontend
Download .txt
gitextract__w6xb4oj/

├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── build/
│   ├── build.js
│   ├── check-versions.js
│   ├── qcloud.cdn.refresh.js
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── config/
│   ├── dev.env.js
│   ├── index.js
│   └── prod.env.js
├── index.html
├── package.json
├── src/
│   ├── App.vue
│   ├── api/
│   │   ├── index.js
│   │   ├── local/
│   │   │   ├── encrypt-api.js
│   │   │   ├── option-api.js
│   │   │   └── string-api.js
│   │   ├── mock/
│   │   │   ├── index.js
│   │   │   └── json/
│   │   │       ├── accounts.json
│   │   │       ├── advertisements.json
│   │   │       ├── authorization.json
│   │   │       ├── products.json
│   │   │       ├── settlements.json
│   │   │       └── stockpile.json
│   │   └── remote/
│   │       ├── account-api.js
│   │       ├── authorization-api.js
│   │       ├── constants.js
│   │       ├── payment-api.js
│   │       └── warehouse-api.js
│   ├── assets/
│   │   └── css/
│   │       └── global.css
│   ├── components/
│   │   ├── home/
│   │   │   ├── Copyright.vue
│   │   │   ├── NavigationBar.vue
│   │   │   ├── UserInformation.vue
│   │   │   ├── cart/
│   │   │   │   └── PayStepIndicator.vue
│   │   │   ├── detail/
│   │   │   │   └── Checkstand.vue
│   │   │   ├── main/
│   │   │   │   ├── Cabinet.vue
│   │   │   │   └── Carousel.vue
│   │   │   └── warehouse/
│   │   │       ├── ProductManage.vue
│   │   │       └── StockManage.vue
│   │   └── login/
│   │       ├── LoginForm.vue
│   │       └── RegistrationForm.vue
│   ├── main.js
│   ├── pages/
│   │   ├── Login.vue
│   │   └── home/
│   │       ├── CartPage.vue
│   │       ├── CommentPage.vue
│   │       ├── DetailPage.vue
│   │       ├── MainPage.vue
│   │       ├── PaymentPage.vue
│   │       ├── SettlementPage.vue
│   │       ├── WarehousePage.vue
│   │       └── index.vue
│   ├── plugins/
│   │   └── errorhandler-plugin.js
│   ├── router/
│   │   └── index.js
│   └── store/
│       ├── constant.js
│       ├── index.js
│       └── modules/
│           ├── cart.js
│           ├── notification.js
│           ├── products.js
│           └── user.js
├── static/
│   ├── .gitkeep
│   └── board/
│       ├── gitalk.css
│       └── gitalk.html
└── travis_docker_push.sh
Download .txt
SYMBOL INDEX (61 symbols across 16 files)

FILE: build/check-versions.js
  function exec (line 7) | function exec (cmd) {

FILE: build/utils.js
  function generateLoaders (line 33) | function generateLoaders (loader, loaderOptions) {

FILE: build/webpack.base.conf.js
  function resolve (line 7) | function resolve (dir) {

FILE: build/webpack.dev.conf.js
  constant HOST (line 13) | const HOST = process.env.HOST
  constant PORT (line 14) | const PORT = process.env.PORT && Number(process.env.PORT)

FILE: build/webpack.prod.conf.js
  method minChunks (line 85) | minChunks (module) {

FILE: src/api/local/encrypt-api.js
  constant CLIENT_SALT (line 4) | const CLIENT_SALT = '$2a$10$o5L.dWYEjZjaejOmN3x4Qu'
  method defaultEncode (line 14) | defaultEncode (source) {
  method gravatarEncode (line 21) | gravatarEncode (email) {

FILE: src/api/local/option-api.js
  constant LOCAL_SESSION_KEY (line 1) | const LOCAL_SESSION_KEY = 'Client-Session'
  method setSession (line 9) | setSession (session) {
  method getSession (line 17) | getSession () {
  method removeSession (line 26) | removeSession () {
  method hasSession (line 31) | hasSession () {
  method isSessionAvailable (line 35) | isSessionAvailable () {
  method isSessionRefreshable (line 39) | isSessionRefreshable () {
  method isAdministrator (line 43) | isAdministrator () {

FILE: src/api/remote/account-api.js
  method getAccountByUsername (line 9) | getAccountByUsername (username) {
  method registerAccount (line 16) | registerAccount (account) {
  method updateAccount (line 27) | updateAccount (account) {

FILE: src/api/remote/authorization-api.js
  method login (line 13) | login (username, password) {
  method refresh (line 32) | refresh (refreshToken) {

FILE: src/api/remote/payment-api.js
  method submitSettlement (line 8) | submitSettlement (settlement) {
  method accomplishPayment (line 16) | accomplishPayment (id) {
  method cancelPayment (line 23) | cancelPayment (id) {

FILE: src/api/remote/warehouse-api.js
  method getAllProducts (line 8) | getAllProducts () {
  method getUniqueProductById (line 15) | getUniqueProductById (id) {
  method getAdvertisements (line 22) | getAdvertisements () {
  method updateProduct (line 29) | updateProduct (product) {
  method createProduct (line 36) | createProduct (product) {
  method removeProduct (line 43) | removeProduct (productId) {
  method queryStock (line 50) | queryStock (productId) {
  method updateStock (line 57) | updateStock (productId, amount) {

FILE: src/plugins/errorhandler-plugin.js
  function isPromise (line 6) | function isPromise (ret) {
  function registerActionHandle (line 22) | function registerActionHandle (actions, errorHandler) {
  method beforeCreate (line 67) | beforeCreate () {

FILE: src/store/constant.js
  constant CART_ADD_PRODUCT_TO_CART (line 2) | const CART_ADD_PRODUCT_TO_CART = 'CART_ADD_PRODUCT_TO_CART' // 添加购物车
  constant CART_DEL_PRODUCT_TO_CART (line 3) | const CART_DEL_PRODUCT_TO_CART = 'CART_DEL_PRODUCT_TO_CART' // 删除购物车
  constant CART_ADD_PRODUCT_QUANTITY (line 4) | const CART_ADD_PRODUCT_QUANTITY = 'CART_ADD_PRODUCT_QUANTITY' // 添加商品数量
  constant CART_DEL_PRODUCT_QUANTITY (line 5) | const CART_DEL_PRODUCT_QUANTITY = 'CART_DEL_PRODUCT_QUANTITY' // 减少商品数量
  constant CART_SET_CHECKOUT_STATUS (line 6) | const CART_SET_CHECKOUT_STATUS = 'CART_SET_CHECKOUT_STATUS' // 改变商品购买状态的
  constant CART_SET_CHECKOUT_STATUS_ALL (line 7) | const CART_SET_CHECKOUT_STATUS_ALL = 'CART_SET_CHECKOUT_STATUS_ALL' // 一...
  constant PRODUCTS_SET_PRODUCT (line 10) | const PRODUCTS_SET_PRODUCT = 'PRODUCTS_SET_PRODUCT' // 获取所有商品的列表
  constant USER_CHANGE_LOGIN (line 13) | const USER_CHANGE_LOGIN = 'USER_CHANGE_LOGIN' // 改变用户的登陆状态
  constant USER_EXIT_STATUS (line 14) | const USER_EXIT_STATUS = 'USER_EXIT_STATUS' // 退出登录状态

FILE: src/store/modules/cart.js
  method adjustCartItems (line 45) | adjustCartItems (state, product) {
  method addCartItem (line 59) | addCartItem (state, product) {
  method removeCartItem (line 73) | removeCartItem (state, id) {
  method setupSettlementBill (line 81) | setupSettlementBill (state, settlement) {
  method receivePayment (line 89) | receivePayment (state, payment) {
  method setupSettlementBillWithDefaultValue (line 99) | setupSettlementBillWithDefaultValue ({state, rootState, commit}, settlem...
  method submitSettlement (line 116) | async submitSettlement ({state, commit}) {

FILE: src/store/modules/notification.js
  method setException (line 17) | setException (state, exception) {
  method clearException (line 26) | clearException (state) {

FILE: src/store/modules/user.js
  method setupSession (line 63) | setupSession (state, session) {
  method clearSession (line 75) | clearSession (state) {
  method updateAccount (line 84) | updateAccount (state, account) {
  method addFavorite (line 91) | addFavorite (state, id) {
  method removeFavorite (line 98) | removeFavorite (state, id) {
  method refreshSessionTrigger (line 107) | refreshSessionTrigger ({dispatch, commit, state}) {
  method refreshSession (line 123) | async refreshSession ({commit, state}) {
Condensed preview — 74 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (200K chars).
[
  {
    "path": ".babelrc",
    "chars": 230,
    "preview": "{\n  \"presets\": [\n    [\"env\", {\n      \"modules\": false,\n      \"targets\": {\n        \"browsers\": [\"> 1%\", \"last 2 versions\""
  },
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".eslintignore",
    "chars": 30,
    "preview": "/build/\n/config/\n/dist/\n/*.js\n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 791,
    "preview": "// https://eslint.org/docs/user-guide/configuring\n\nmodule.exports = {\n  root: true,\n  parserOptions: {\n    parser: 'babe"
  },
  {
    "path": ".gitignore",
    "chars": 154,
    "preview": ".DS_Store\nnode_modules/\n/dist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vsc"
  },
  {
    "path": ".postcssrc.js",
    "chars": 246,
    "preview": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  \"plugins\": {\n    \"postcss-import\": {},"
  },
  {
    "path": ".travis.yml",
    "chars": 641,
    "preview": "language: node_js\nnode_js:\n  - lts/*\nbefore_install:\n  - export TZ='Asia/Shanghai'\ninstall:\n  - npm install\nscript:\n  - "
  },
  {
    "path": "Dockerfile",
    "chars": 155,
    "preview": "FROM nginx:alpine\n\nMAINTAINER icyfenix\n\nWORKDIR /usr/share/nginx/html\nCOPY dist /usr/share/nginx/html\n\nEXPOSE 80\nENTRYPO"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 5970,
    "preview": "# Fenix's BookStore前端工程\n\n<p align=\"center\">\n  <a href=\"https://icyfenix.cn\" target=\"_blank\">\n    <img width=\"180\" src=\"h"
  },
  {
    "path": "build/build.js",
    "chars": 1289,
    "preview": "'use strict'\nrequire('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nconst ora = require('ora')\nconst rm = r"
  },
  {
    "path": "build/check-versions.js",
    "chars": 1290,
    "preview": "'use strict'\nconst chalk = require('chalk')\nconst semver = require('semver')\nconst packageConfig = require('../package.j"
  },
  {
    "path": "build/qcloud.cdn.refresh.js",
    "chars": 308,
    "preview": "const qcloudSDK = require('qcloud-cdn-node-sdk')\n\nconst userConfig = {\n  secretId: process.env.CDN_ID,\n  secretKey: proc"
  },
  {
    "path": "build/utils.js",
    "chars": 2587,
    "preview": "'use strict'\nconst path = require('path')\nconst config = require('../config')\nconst ExtractTextPlugin = require('extract"
  },
  {
    "path": "build/vue-loader.conf.js",
    "chars": 553,
    "preview": "'use strict'\nconst utils = require('./utils')\nconst config = require('../config')\nconst isProduction = process.env.NODE_"
  },
  {
    "path": "build/webpack.base.conf.js",
    "chars": 2385,
    "preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst config = require('../config')\nconst vue"
  },
  {
    "path": "build/webpack.dev.conf.js",
    "chars": 3043,
    "preview": "'use strict'\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require('../config')\ncon"
  },
  {
    "path": "build/webpack.prod.conf.js",
    "chars": 5095,
    "preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst conf"
  },
  {
    "path": "config/dev.env.js",
    "chars": 170,
    "preview": "'use strict'\nconst merge = require('webpack-merge')\nconst prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEn"
  },
  {
    "path": "config/index.js",
    "chars": 2375,
    "preview": "'use strict'\n// Template version: 1.3.1\n// see http://vuejs-templates.github.io/webpack for documentation.\n\nconst path ="
  },
  {
    "path": "config/prod.env.js",
    "chars": 136,
    "preview": "'use strict'\nmodule.exports = {\n  NODE_ENV: '\"production\"',\n  // 传入了参数--mock的话,生产模式中也仍然采用Mock.JS\n  MOCK: process.argv[2]"
  },
  {
    "path": "index.html",
    "chars": 279,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width,initial"
  },
  {
    "path": "package.json",
    "chars": 2657,
    "preview": "{\n  \"name\": \"bookstore\",\n  \"version\": \"1.0.0\",\n  \"description\": \"The Fenix Project Client Demo\",\n  \"author\": \"icyfenix <"
  },
  {
    "path": "src/App.vue",
    "chars": 846,
    "preview": "<template>\n  <div id=\"app\">\n    <transition name=\"slide-fade\">\n      <el-alert title=\"接收到未处理的异常:\" type=\"error\" :descript"
  },
  {
    "path": "src/api/index.js",
    "chars": 2270,
    "preview": "import axios from 'axios'\nimport constants from './remote/constants'\nimport warehouse from './remote/warehouse-api'\nimpo"
  },
  {
    "path": "src/api/local/encrypt-api.js",
    "chars": 645,
    "preview": "const crypto = require('crypto')\nconst bcrypt = require('bcryptjs')\n\nconst CLIENT_SALT = '$2a$10$o5L.dWYEjZjaejOmN3x4Qu'"
  },
  {
    "path": "src/api/local/option-api.js",
    "chars": 1250,
    "preview": "const LOCAL_SESSION_KEY = 'Client-Session'\n\n/**\n * 本地选项\n * 可以持久存在在客户端本地或者Session的设置信息\n * 通过统一的API来对外部屏蔽掉localStorage和ses"
  },
  {
    "path": "src/api/local/string-api.js",
    "chars": 332,
    "preview": "export default {\n  /**\n   * 去除HTML标签\n   */\n  pureText: text => text.replace(/<\\/?[^>]*>/g, ''),\n\n  /**\n   * 将HTML中的<p></"
  },
  {
    "path": "src/api/mock/index.js",
    "chars": 1839,
    "preview": "const MockJS = require('mockjs')\n\n/**\n * Mock的请求不会真正发送,在Network面板看不到,输出日志以便调试使用\n */\nconst loadJSON = (options, file) => "
  },
  {
    "path": "src/api/mock/json/accounts.json",
    "chars": 177,
    "preview": "{\n  \"id\": 1,\n  \"username\": \"icyfenix\",\n  \"name\": \"周志明\",\n  \"avatar\": \"\",\n  \"telephone\": \"18888888888\",\n  \"email\": \"icyfen"
  },
  {
    "path": "src/api/mock/json/advertisements.json",
    "chars": 265,
    "preview": "[\n  {\n    \"id\": \"fenix\",\n    \"image\": \"/static/carousel/fenix2.png\",\n    \"productId\": 8\n  },\n  {\n    \"id\": \"ai\",\n    \"im"
  },
  {
    "path": "src/api/mock/json/authorization.json",
    "chars": 998,
    "preview": "{\n  \"access_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJpY3lmZW5peCIsInNjb3BlIjpbIkFMTCJdLCJleHAiOj"
  },
  {
    "path": "src/api/mock/json/products.json",
    "chars": 11857,
    "preview": "[\n  {\n    \"id\": 8,\n    \"title\": \"凤凰架构:构建可靠的大型分布式系统\",\n    \"price\": 0.0,\n    \"rate\": 0.0,\n    \"description\": \"<p>这是一部以“如何构"
  },
  {
    "path": "src/api/mock/json/settlements.json",
    "chars": 285,
    "preview": "{\n  \"id\": 0,\n  \"createTime\": \"2020-03-13T16:04:53.388+0000\",\n  \"payId\": \"0904ff25-819b-42d1-9651-dffd79a7893e\",\n  \"total"
  },
  {
    "path": "src/api/mock/json/stockpile.json",
    "chars": 65,
    "preview": "{\n  \"id\": 1,\n  \"product_id\": 1,\n  \"amount\": 10,\n  \"frozen\": 10\n}\n"
  },
  {
    "path": "src/api/remote/account-api.js",
    "chars": 494,
    "preview": "import axios from 'axios'\nimport api from '@/api'\n\nexport default {\n\n  /**\n   * 根据用户名查询用户信息\n   */\n  getAccountByUsername"
  },
  {
    "path": "src/api/remote/authorization-api.js",
    "chars": 1156,
    "preview": "import axios from 'axios'\nimport api from '@/api'\nimport constants from '@/api/remote/constants'\n\nexport default {\n\n  /*"
  },
  {
    "path": "src/api/remote/constants.js",
    "chars": 506,
    "preview": "export default {\n  // 远程服务约定成功操作代码\n  REMOTE_OPERATION_SUCCESS: 0,\n\n  // HTTP 请求超时时间(毫秒)\n  REMOTE_TIMEOUT: 30000,\n  // 资源"
  },
  {
    "path": "src/api/remote/payment-api.js",
    "chars": 475,
    "preview": "import axios from 'axios'\n\nexport default {\n  /**\n   * 提交要购买的商品和配送信息到服务端\n   * 服务端会进行库存检查、配种地址检查等校验,如果结果满足的话,会执行该结算单,返回订单"
  },
  {
    "path": "src/api/remote/warehouse-api.js",
    "chars": 915,
    "preview": "import axios from 'axios'\n\nexport default {\n\n  /**\n   * 无过滤条件,获取全部的产品\n   */\n  getAllProducts () {\n    return axios.get('"
  },
  {
    "path": "src/assets/css/global.css",
    "chars": 1577,
    "preview": "html {\n  height: 100%\n}\n\nbody {\n  margin: 0;\n  background-color: #ededed;\n  overflow-x: hidden;\n  /* 解决el-images为了预览模式设置"
  },
  {
    "path": "src/components/home/Copyright.vue",
    "chars": 3948,
    "preview": "<template>\n  <div style=\"display: block\">\n    <el-row :gutter=\"20\">\n      <el-col :span=\"6\">\n        <div>\n          <h1"
  },
  {
    "path": "src/components/home/NavigationBar.vue",
    "chars": 3829,
    "preview": "<template>\n  <div>\n    <div class=\"nav-bar-container\">\n      <div class=\"left-action-bar\">\n        <img src=\"@/assets/lo"
  },
  {
    "path": "src/components/home/UserInformation.vue",
    "chars": 3833,
    "preview": "<template>\n  <el-popover placement=\"top\" width=\"250\" v-model=\"visible\" trigger=\"click\">\n    <div class=\"container\">\n    "
  },
  {
    "path": "src/components/home/cart/PayStepIndicator.vue",
    "chars": 632,
    "preview": "<template>\n  <el-card class=\"box-card\" style=\"margin-top: 20px\">\n    <div slot=\"header\" class=\"header\">\n      <span>购买流程"
  },
  {
    "path": "src/components/home/detail/Checkstand.vue",
    "chars": 3534,
    "preview": "<template>\n  <div class=\"sale\">\n    <ul class=\"sale_ul\">\n      <li>\n        <label>零&nbsp;&nbsp;售&nbsp;&nbsp;价:</label>\n"
  },
  {
    "path": "src/components/home/main/Cabinet.vue",
    "chars": 3834,
    "preview": "<template>\n  <el-card class=\"box-card\">\n    <div slot=\"header\" class=\"header\">\n      <span>热销书籍</span>\n    </div>\n    <e"
  },
  {
    "path": "src/components/home/main/Carousel.vue",
    "chars": 846,
    "preview": "<template>\n  <el-carousel :interval=\"5000\" type=\"card\" height=\"400px\">\n    <el-carousel-item v-for=\"item in advertisemen"
  },
  {
    "path": "src/components/home/warehouse/ProductManage.vue",
    "chars": 3973,
    "preview": "<template>\n  <el-form :model=\"product\" label-position=\"left\">\n    <el-form-item label=\"商品名称\" label-width=\"100px\">\n      "
  },
  {
    "path": "src/components/home/warehouse/StockManage.vue",
    "chars": 1360,
    "preview": "<template>\n  <div>\n    <span class=\"title\">{{this.product.title}}</span>\n    <el-form :model=\"product\" label-position=\"r"
  },
  {
    "path": "src/components/login/LoginForm.vue",
    "chars": 3677,
    "preview": "<template>\n  <div>\n    <img src=\"../../assets/logo-color.png\" class=\"logo\">\n    <span class=\"title\">Fenix's Bookstore</s"
  },
  {
    "path": "src/components/login/RegistrationForm.vue",
    "chars": 4067,
    "preview": "<template>\n  <div>\n    <img src=\"../../assets/logo-color.png\" class=\"logo\">\n    <span class=\"title\">新用户注册</span>\n    <el"
  },
  {
    "path": "src/main.js",
    "chars": 790,
    "preview": "import Vue from 'vue'\nimport 'default-passive-events'\nimport ElementUI from 'element-ui'\nimport 'element-ui/lib/theme-ch"
  },
  {
    "path": "src/pages/Login.vue",
    "chars": 2396,
    "preview": "<template>\n  <div class=\"bg\">\n    <div class=\"dialog dialog-shadow\">\n      <LoginForm v-if=\"!registrationMode\" v-on:chan"
  },
  {
    "path": "src/pages/home/CartPage.vue",
    "chars": 4321,
    "preview": "<template>\n  <div>\n    <el-card class=\"box-card\">\n      <div slot=\"header\" class=\"header\">\n        <span>我的购物车</span>\n  "
  },
  {
    "path": "src/pages/home/CommentPage.vue",
    "chars": 507,
    "preview": "<template>\n  <el-card class=\"box-card\" :body-style=\"{ padding: '0 10px 0 10px' }\">\n    <div slot=\"header\" class=\"header\""
  },
  {
    "path": "src/pages/home/DetailPage.vue",
    "chars": 2463,
    "preview": "<template>\n  <div id=\"information\">\n    <el-card class=\"box-card\">\n      <div slot=\"header\" class=\"header\">\n        <spa"
  },
  {
    "path": "src/pages/home/MainPage.vue",
    "chars": 311,
    "preview": "<template>\n  <div>\n    <Carousel/>\n    <Cabinet/>\n  </div>\n</template>\n\n<script>\nimport Carousel from '@/components/home"
  },
  {
    "path": "src/pages/home/PaymentPage.vue",
    "chars": 3420,
    "preview": "<template>\n  <div>\n    <el-card class=\"box-card\" style=\"margin-top: 20px\">\n      <div slot=\"header\" class=\"header\">\n    "
  },
  {
    "path": "src/pages/home/SettlementPage.vue",
    "chars": 6712,
    "preview": "<template>\n  <div>\n    <el-card class=\"box-card\">\n      <div slot=\"header\" class=\"header\">\n        <span>我的结算单</span>\n  "
  },
  {
    "path": "src/pages/home/WarehousePage.vue",
    "chars": 4518,
    "preview": "<template>\n  <el-card class=\"box-card\">\n    <div slot=\"header\" class=\"header\">\n      <span>商品及库存管理</span>\n      <span st"
  },
  {
    "path": "src/pages/home/index.vue",
    "chars": 739,
    "preview": "<template>\n<!--  <div class=\"home-box\">-->\n    <el-container>\n      <el-header direction-=\"vertical\">\n        <Navigatio"
  },
  {
    "path": "src/plugins/errorhandler-plugin.js",
    "chars": 2317,
    "preview": "/**\n * 针对vue\\vuex\\vue-router中的同步、异步异常信息做全局处理\n */\nimport Router from 'vue-router'\n\nfunction isPromise (ret) {\n  return (r"
  },
  {
    "path": "src/router/index.js",
    "chars": 1960,
    "preview": "import Vue from 'vue'\nimport Router from 'vue-router'\nimport store from '@/store'\n\nVue.use(Router)\n\nconst router = new R"
  },
  {
    "path": "src/store/constant.js",
    "chars": 713,
    "preview": "// cart\nexport const CART_ADD_PRODUCT_TO_CART = 'CART_ADD_PRODUCT_TO_CART' // 添加购物车\nexport const CART_DEL_PRODUCT_TO_CAR"
  },
  {
    "path": "src/store/index.js",
    "chars": 325,
    "preview": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport cart from './modules/cart'\nimport products from './modules/products"
  },
  {
    "path": "src/store/modules/cart.js",
    "chars": 3280,
    "preview": "import api from '@/api'\n\n/**\n * 购物车状态数据\n * 一共存有三类状态:\n *  - 购物车元素(items)\n *  - 结算单(settlement),即本次打算购买的内容,以及派送信息\n *  - 支付"
  },
  {
    "path": "src/store/modules/notification.js",
    "chars": 506,
    "preview": "const state = {\n  // 没有被捕获,抛到全局的异常\n  exception: null,\n  // 服务端发来的通知消息\n  notification: null\n}\n\nconst getters = {}\n\nconst "
  },
  {
    "path": "src/store/modules/products.js",
    "chars": 455,
    "preview": "const state = {\n  favorite: []\n}\n\nconst getters = {}\n\nconst mutations = {}\n\nconst actions = {\n  // vuex 给actions 的 commi"
  },
  {
    "path": "src/store/modules/user.js",
    "chars": 3107,
    "preview": "import api from '@/api'\n\nconst EMPTY_SESSION = () => ({\n  username: null,\n  scope: '',\n  expires: 0,\n  access_token: '',"
  },
  {
    "path": "static/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "static/board/gitalk.css",
    "chars": 24919,
    "preview": "@font-face {\n  font-family: octicons-link;\n  src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAA"
  },
  {
    "path": "static/board/gitalk.html",
    "chars": 497,
    "preview": "<link rel=\"stylesheet\" href=\"gitalk.css\">\n<script src=\"gitalk.min.js\"></script>\n<html>\n\t<body>\n\t\t<div id=\"container\"></d"
  },
  {
    "path": "travis_docker_push.sh",
    "chars": 255,
    "preview": "#!/bin/bash\necho \"$DOCKER_PASSWORD\" | docker login -u \"$DOCKER_USERNAME\" --password-stdin\ndocker build -t bookstore:fron"
  }
]

About this extraction

This page contains the full source code of the fenixsoft/fenix-bookstore-frontend GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 74 files (162.0 KB), approximately 53.2k tokens, and a symbol index with 61 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!