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前端工程
如果你此时并不曾了解过什么是“The Fenix Project”,建议先阅读这部分内容 。
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)
>
也许你已注意到,以上这些运行方式,均没有涉及到任何的服务端、数据库的部署。现代软件工程里,基于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/)
渐进式JavaScript框架
- [Element](https://element.eleme.cn/#/zh-CN)
一套为开发者、设计师和产品经理准备的基于Vue 2.0的桌面端组件库
- [Axios](https://github.com/axios/axios)
Promise based HTTP client for the browser and node.js
- [Mock.js](http://mockjs.com/)
生成随机数据,拦截 Ajax 请求
- [DesignEvo](https://www.designevo.com/cn)
一款由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
================================================
Fenix's BookStore
================================================
FILE: package.json
================================================
{
"name": "bookstore",
"version": "1.0.0",
"description": "The Fenix Project Client Demo",
"author": "icyfenix ",
"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
================================================
================================================
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中的
转换为回车
*/
transToReturn: text => text.replace(//g, '').replace(/<\/p>/g, '\n'),
/**
* 将回车转换为
*/
transToHTML: text => '
' + text.replace(/\n*$/g, '').replace(/\n/g, '
') + '
'
}
================================================
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": "这是一部以“如何构建一套可靠的分布式大型软件系统”为叙事主线的开源文档,是一幅帮助开发人员整理现代软件架构各条分支中繁多知识点的技能地图。文章《什么是“凤凰架构” 》详细阐述了这部文档的主旨、目标与名字的来由,文章《如何开始 》简述了文档每章讨论的主要话题与内容详略分布
",
"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": "这是一部从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典,繁体版在台湾也颇受欢迎。
自2011年上市以来,前两个版本累计印刷36次,销量超过30万册,两家主要网络书店的评论近90000条,内容上近乎零差评,是原创计算机图书领域不可逾越的丰碑,第3版在第2版的基础上做了重大修订,内容更丰富、实战性更强:根据新版JDK对内容进行了全方位的修订和升级,围绕新技术和生产实践新增逾10万字,包含近50%的全新内容,并对第2版中含糊、瑕疵和错误内容进行了修正。
全书一共13章,分为五大部分:
第一部分(第1章)走近Java
系统介绍了Java的技术体系、发展历程、虚拟机家族,以及动手编译JDK,了解这部分内容能对学习JVM提供良好的指引。
第二部分(第2~5章)自动内存管理
详细讲解了Java的内存区域与内存溢出、垃圾收集器与内存分配策略、虚拟机性能监控与故障排除等与自动内存管理相关的内容,以及10余个经典的性能优化案例和优化方法;
第三部分(第6~9章)虚拟机执行子系统
深入分析了虚拟机执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎,以及多个类加载及其执行子系统的实战案例;
第四部分(第10~11章)程序编译与代码优化
详细讲解了程序的前、后端编译与优化,包括前端的易用性优化措施,如泛型、主动装箱拆箱、条件编译等的内容的深入分析;以及后端的性能优化措施,如虚拟机的热点探测方法、HotSpot 的即时编译器、提前编译器,以及各种常见的编译期优化技术;
第五部分(第12~13章)高效并发
主要讲解了Java实现高并发的原理,包括Java的内存模型、线程与协程,以及线程安全和锁优化。
全书以实战为导向,通过大量与实际生产环境相结合的案例分析和展示了解决各种Java技术难题的方案和技巧。
",
"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": "这是一部对人工智能充满敬畏之心的匠心之作,由《深入理解Java虚拟机》作者耗时一年完成,它将带你从奠基人物、历史事件、学术理论、研究成果、技术应用等5个维度全面读懂人工智能。
本书以时间为主线,用专业的知识、通俗的语言、巧妙的内容组织方式,详细讲解了人工智能这个学科的全貌、能解决什么问题、面临怎样的困难、尝试过哪些努力、取得过多少成绩、未来将向何方发展,尽可能消除人工智能的神秘感,把阳春白雪的人工智能从科学的殿堂推向公众面前。
",
"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": "本书完整而准确地阐释了Java虚拟机各方面的细节,围绕Java虚拟机整体架构、编译器、class文件格式、加载、链接与初始化、指令集等核心主题对Java虚拟机进行全面而深入的分析,深刻揭示Java虚拟机的工作原理。同时,书中不仅完整地讲述了由Java SE 8所引入的新特性,例如对包含默认实现代码的接口方法所做的调用,还讲述了为支持类型注解及方法参数注解而对class文件格式所做的扩展,并阐明了class文件中各属性的含义,以及字节码验证的规则。
",
"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": "《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》内容简介:第1版两年内印刷近10次,4家网上书店的评论近4?000条,98%以上的评论全部为5星级的好评,是整个Java图书领域公认的经典著作和超级畅销书,繁体版在台湾也十分受欢迎。第2版在第1版的基础上做了很大的改进:根据最新的JDK 1.7对全书内容进行了全面的升级和补充;增加了大量处理各种常见JVM问题的技巧和最佳实践;增加了若干与生产环境相结合的实战案例;对第1版中的错误和不足之处的修正;等等。第2版不仅技术更新、内容更丰富,而且实战性更强。
《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》共分为五大部分,围绕内存管理、执行子系统、程序编译与优化、高效并发等核心主题对JVM进行了全面而深入的分析,深刻揭示了JVM的工作原理。
第一部分从宏观的角度介绍了整个Java技术体系、Java和JVM的发展历程、模块化,以及JDK的编译,这对理解书中后面内容有重要帮助。
第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见虚拟机监控与故障处理工具的原理和使用方法。
第三部分分析了虚拟机的执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎。
第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果;
第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。
",
"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": "本书整合了自1999年《Java虚拟机规范(第2版)》发布以来Java世界所出现的技术变化。另外,还修正了第2版中的许多错误,以及对目前主流Java虚拟机实现来说已经过时的内容。最后还处理了一些Java虚拟机和Java语言概念的模糊之处。
2004年发布的Java SE 5.0版为Java语言带来了翻天覆地的变化,但是对Java虚拟机设计的影响则相对较小。在Java SE 7这个版本中,我们扩充了class文件格式以便支持新的Java语言特性,譬如泛型和变长参数方法等。
",
"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": "本书是原创Java技术图书领域继《深入理解Java虚拟机》后的又一实力之作,也是全球首本基于最新OSGi R5.0规范的著作。理论方面,既全面解读了OSGi规范,深刻揭示了OSGi原理,详细讲解了OSGi服务,又系统地介绍了Equinox框架的使用方法,并通过源码分析了该框架的工作机制;实践方面,不仅包含一些典型的案例,还总结了大量的最佳实践,极具实践指导意义。
全书共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环节中进行程序测试。
",
"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": "作为一位Java程序员,你是否也曾经想深入理解Java虚拟机,但是却被它的复杂和深奥拒之门外?没关系,本书极尽化繁为简之妙,能带领你在轻松中领略Java虚拟机的奥秘。本书是近年来国内出版的唯一一本与Java虚拟机相关的专著,也是唯一一本同时从核心理论和实际运用这两个角度去探讨Java虚拟机的著作,不仅理论分析得透彻,而且书中包含的典型案例和最佳实践也极具现实指导意义。
全书共分为五大部分。第一部分从宏观的角度介绍了整个Java技术体系的过去、现在和未来,以及如何独立地编译一个OpenJDK7,这对理解后面的内容很有帮助。第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见的虚拟机的监控与调试工具的原理和使用方法。第三部分分析了虚拟机的执行子系统,包括Class的文件结构以及如何存储和访问Class中的数据;虚拟机的类创建机制以及类加载器的工作原理和它对虚拟机的意义;虚拟机字节码的执行引擎以及它在实行代码时涉及的内存结构。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。
",
"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
================================================
版权信息
本作品采用知识共享署名 4.0 国际许可协议 进行许可。
您可以自由地:
共享 — 在任何媒介上以任何形式复制、发行本作品
演绎 — 修改、转换或以本作品为基础进行二次创作
只要您遵守许可协议条款中署名、非商业性使用、相同方式共享的条件,许可人就无法收回您的这些权利。
程序资源
Fenix's BookStore前端部分基于以下开源组件和免费资源构建:
Vue.js 渐进式JavaScript框架
Element 一套为开发者、设计师和产品经理准备的基于Vue 2.0的桌面端组件库
Axios Promise based HTTP client for the browser and
node.js
Mock.js 生成随机数据,拦截 Ajax 请求
DesignEvo 一款由PearlMountain有限公司设计研发的logo设计软件
支持作者
可扫描以下二维码在微信公众号上关注更新文章:
在微信、微博、GitHub网站上关注、点赞亦是对作者的支持:
Copyright © 2020 网站备案信息:粤ICP备18088957号-1
================================================
FILE: src/components/home/NavigationBar.vue
================================================
================================================
FILE: src/components/home/UserInformation.vue
================================================
{{account.name}}
更新信息
退出登录
================================================
FILE: src/components/home/cart/PayStepIndicator.vue
================================================
================================================
FILE: src/components/home/detail/Checkstand.vue
================================================
================================================
FILE: src/components/home/main/Cabinet.vue
================================================
¥ {{book.price.toFixed(2)}}
{{book.title}}
{{pureText(book.description)}}
================================================
FILE: src/components/home/main/Carousel.vue
================================================
================================================
FILE: src/components/home/warehouse/ProductManage.vue
================================================
{{spec.item+':'+spec.value}}
+ New Spec
取 消
确 定
================================================
FILE: src/components/home/warehouse/StockManage.vue
================================================
{{this.product.title}}
取 消
确 定
================================================
FILE: src/components/login/LoginForm.vue
================================================
Fenix's Bookstore
自动登录
注册新用户
登录
登录代表你已同意
用户协议
和
隐私政策
================================================
FILE: src/components/login/RegistrationForm.vue
================================================
新用户注册
注册
返回
================================================
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: ' '
})
================================================
FILE: src/pages/Login.vue
================================================
================================================
FILE: src/pages/home/CartPage.vue
================================================
{adjustAmount(scope.row,newValue,oldValue)}">
{{scope.row.price * scope.row.amount}} 元
删除
{{`购物车中共计 ${items.length} 件商品,已选择其中 ${multipleSelection.length} 件`}}
总计:
{{this.total}} 元
¥ 选好了,去结算
================================================
FILE: src/pages/home/CommentPage.vue
================================================
================================================
FILE: src/pages/home/DetailPage.vue
================================================
================================================
FILE: src/pages/home/MainPage.vue
================================================
================================================
FILE: src/pages/home/PaymentPage.vue
================================================
购买成功!
{{message}}
支付总额:{{payment.totalPrice.toFixed(2)}}
提示:本程序为演示,扫描以上二维码并不会实际触发支付
点击模拟扫描
或也可以
点击取消购买
购买失败!
失败原因:{{payment.message || '没有收到服务端的支付结算数据'}}
================================================
FILE: src/pages/home/SettlementPage.vue
================================================
收件人信息:
支付方式:
微信支付
支付宝
结算金额:
{{settlement.items.length}} 件商品,总商品金额: {{totalAmount.toFixed(2)}} 元
运费: 12.00 元
折扣: 0.00 元
应付总额: {{(totalAmount+12).toFixed(2)}}
寄送至:{{fullAddress}}, 收件人:{{this.purchase.name}}, 电话:{{this.purchase.telephone}}
提交订单
{{scope.row.price * scope.row.amount}} 元
================================================
FILE: src/pages/home/WarehousePage.vue
================================================
{{ pureText(scope.row.description) }}
修改
库存
删除
================================================
FILE: src/pages/home/index.vue
================================================
================================================
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
================================================
================================================
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