Merge branch 'master' into deploy

This commit is contained in:
Pan 2019-04-08 14:13:53 +08:00
commit caffe6f470
31 changed files with 248 additions and 141 deletions

View File

@ -1,11 +1,23 @@
import Mock from 'mockjs'
import mocks from './mocks'
import { param2Obj } from '../src/utils'
const MOCK_API_BASE = '/mock'
import user from './user'
import role from './role'
import article from './article'
import search from './remoteSearch'
const mocks = [
...user,
...role,
...article,
...search
]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,
// which will cause many of your third-party libraries to be invalidated(like progress event).
export function mockXHR() {
// 修复在使用 MockJS 情况下,设置 withCredentials = true且未被拦截的跨域请求丢失 Cookies 的问题
// mock patch
// https://github.com/nuysoft/Mock/issues/300
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
Mock.XHR.prototype.send = function() {
@ -42,9 +54,10 @@ export function mockXHR() {
}
}
// for mock server
const responseFake = (url, type, respond) => {
return {
url: new RegExp(`${MOCK_API_BASE}${url}`),
url: new RegExp(`/mock${url}`),
type: type || 'get',
response(req, res) {
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))

79
mock/mock-server.js Normal file
View File

@ -0,0 +1,79 @@
const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
function registerRoutes(app) {
const { default: mocks } = require('./index.js')
for (const mock of mocks) {
app[mock.type](mock.url, mock.response)
}
return {
mockRoutesLength: Object.keys(mocks).length
}
}
function unregisterRoutes() {
Object.keys(require.cache).forEach(i => {
if (i.includes('/mock')) {
delete require.cache[require.resolve(i)]
}
})
}
function getPath(path) {
var match = path.toString()
.replace('\\/?', '')
.replace('(?=\\/|$)', '$')
.match(/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//)
return match
? match[1].replace(/\\(.)/g, '$1').split('/')
: path.toString()
}
function getMockRoutesIndex(app) {
for (let index = 0; index <= app._router.stack.length; index++) {
const r = app._router.stack[index]
if (r.route && r.route.path) {
const path = getPath(r.route.path)
if (path.includes('mock')) {
return index
}
}
}
}
module.exports = app => {
// es6 polyfill
require('@babel/register')
// parse app.body
// http://expressjs.com/en/4x/api.html#req.body
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: true
}))
const { mockRoutesLength } = registerRoutes(app)
// watch files, hot reload mock server
chokidar.watch(('./mock'), {
ignored: 'mock/mock-server.js',
persistent: true,
ignoreInitial: true
}).on('all', (event, path) => {
if (event === 'change' || event === 'add') {
// find mock routes stack index
const index = getMockRoutesIndex(app)
// remove mock routes stack
app._router.stack.splice(index, mockRoutesLength)
// clear routes cache
unregisterRoutes()
registerRoutes(app)
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
}
})
}

View File

@ -1,12 +0,0 @@
import user from './user'
import role from './role'
import article from './article'
import search from './remoteSearch'
export default [
...user,
...role,
...article,
...search
]

View File

@ -19,7 +19,7 @@ export const constantRoutes = [
},
{
path: '/auth-redirect',
component: 'views/login/authredirect',
component: 'views/login/authRedirect',
hidden: true
},
{

View File

@ -84,6 +84,7 @@
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"chokidar": "2.1.5",
"connect": "3.6.6",
"eslint": "5.15.3",
"eslint-plugin-vue": "5.2.2",

View File

@ -2,7 +2,7 @@
<div class="dndList">
<div :style="{width:width1}" class="dndList-list">
<h3>{{ list1Title }}</h3>
<draggable :list="list1" group="article" class="dragArea">
<draggable :set-data="setData" :list="list1" group="article" class="dragArea">
<div v-for="element in list1" :key="element.id" class="list-complete-item">
<div class="list-complete-item-handle">
{{ element.id }}[{{ element.author }}] {{ element.title }}
@ -94,6 +94,11 @@ export default {
if (this.isNotInList1(ele)) {
this.list1.push(ele)
}
},
setData(dataTransfer) {
// to avoid Firefox bug
// Detail see : https://github.com/RubaXa/Sortable/issues/1012
dataTransfer.setData('Text', '')
}
}
}

View File

@ -6,7 +6,7 @@
</el-button>
</el-badge>
<el-dialog :visible.sync="dialogTableVisible" title="Error Log" width="80%">
<el-dialog :visible.sync="dialogTableVisible" title="Error Log" width="80%" append-to-body>
<el-table :data="errorLogs" border>
<el-table-column label="Message">
<template slot-scope="{row}">

View File

@ -7,6 +7,7 @@
:list="list"
v-bind="$attrs"
class="board-column-content"
:set-data="setData"
>
<div v-for="element in list" :key="element.id" class="board-item">
{{ element.name }} {{ element.id }}
@ -39,6 +40,13 @@ export default {
return []
}
}
},
methods: {
setData(dataTransfer) {
// to avoid Firefox bug
// Detail see : https://github.com/RubaXa/Sortable/issues/1012
dataTransfer.setData('Text', '')
}
}
}
</script>

View File

@ -1,7 +1,6 @@
export default {
route: {
dashboard: 'Dashboard',
introduction: 'Introduction',
documentation: 'Documentation',
guide: 'Guide',
permission: 'Permission',
@ -10,7 +9,6 @@ export default {
directivePermission: 'Directive Permission',
icons: 'Icons',
components: 'Components',
componentIndex: 'Introduction',
tinymce: 'Tinymce',
markdown: 'Markdown',
jsonEditor: 'JSON Editor',
@ -19,9 +17,9 @@ export default {
avatarUpload: 'Avatar Upload',
dropzone: 'Dropzone',
sticky: 'Sticky',
countTo: 'CountTo',
countTo: 'Count To',
componentMixin: 'Mixin',
backToTop: 'BackToTop',
backToTop: 'Back To Top',
dragDialog: 'Drag Dialog',
dragSelect: 'Drag Select',
dragKanban: 'Drag Kanban',
@ -75,7 +73,7 @@ export default {
},
login: {
title: 'Login Form',
logIn: 'Log in',
logIn: 'Login',
username: 'Username',
password: 'Password',
any: 'any',
@ -88,10 +86,10 @@ export default {
},
permission: {
addRole: 'New Role',
editPermission: 'Edit Permission',
editPermission: 'Edit',
roles: 'Your roles',
switchRoles: 'Switch roles',
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.',
tips: 'In some cases, using v-permission will have no effect. For example: Element-UI el-tab or el-table-column and other scenes that dynamically render dom. You can only do this with v-if.',
delete: 'Delete',
confirm: 'Confirm',
cancel: 'Cancel'
@ -102,7 +100,7 @@ export default {
},
components: {
documentation: 'Documentation',
tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
tinymceTips: 'Rich text is a core feature of the management backend, but at the same time it is a place with lots of pits. In the process of selecting rich texts, I also took a lot of detours. The common rich texts on the market have been basically used, and I finally chose Tinymce. See the more detailed rich text comparison and introduction.',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
@ -143,14 +141,14 @@ export default {
excel: {
export: 'Export',
selectedExport: 'Export Selected Items',
placeholder: 'Please enter the file name(default excel-list)'
placeholder: 'Please enter the file name (default excel-list)'
},
zip: {
export: 'Export',
placeholder: 'Please enter the file name(default file)'
placeholder: 'Please enter the file name (default file)'
},
pdf: {
tips: 'Here we use window.print() to implement the feature of downloading pdf.'
tips: 'Here we use window.print() to implement the feature of downloading PDF.'
},
theme: {
change: 'Change Theme',

View File

@ -1,7 +1,6 @@
export default {
route: {
dashboard: 'Panel de control',
introduction: 'Introducción',
documentation: 'Documentación',
guide: 'Guía',
permission: 'Permisos',
@ -10,7 +9,6 @@ export default {
directivePermission: 'Permisos de la directiva',
icons: 'Iconos',
components: 'Componentes',
componentIndex: 'Introducción',
tinymce: 'Tinymce',
markdown: 'Markdown',
jsonEditor: 'Editor JSON',

View File

@ -24,11 +24,24 @@ const messages = {
...elementEsLocale
}
}
export function getLanguage() {
const chooseLanguage = Cookies.get('language')
if (chooseLanguage) return chooseLanguage
// if has not choose language
const language = (navigator.language || navigator.browserLanguage).toLowerCase()
const locales = Object.keys(messages)
for (const locale of locales) {
if (language.indexOf(locale) > -1) {
return locale
}
}
return 'en'
}
const i18n = new VueI18n({
// set locale
// options: en | zh | es
locale: Cookies.get('language') || 'en',
locale: getLanguage(),
// set locale messages
messages
})

View File

@ -1,7 +1,6 @@
export default {
route: {
dashboard: '首页',
introduction: '简述',
documentation: '文档',
guide: '引导页',
permission: '权限测试页',
@ -10,16 +9,15 @@ export default {
directivePermission: '指令权限',
icons: '图标',
components: '组件',
componentIndex: '介绍',
tinymce: '富文本编辑器',
markdown: 'Markdown',
jsonEditor: 'JSON编辑器',
jsonEditor: 'JSON 编辑器',
dndList: '列表拖拽',
splitPane: 'Splitpane',
avatarUpload: '头像上传',
dropzone: 'Dropzone',
sticky: 'Sticky',
countTo: 'CountTo',
countTo: 'Count To',
componentMixin: '小组件',
backToTop: '返回顶部',
dragDialog: '拖拽 Dialog',
@ -32,17 +30,17 @@ export default {
example: '综合实例',
nested: '路由嵌套',
menu1: '菜单1',
'menu1-1': '菜单1-1',
'menu1-2': '菜单1-2',
'menu1-2-1': '菜单1-2-1',
'menu1-2-2': '菜单1-2-2',
'menu1-3': '菜单1-3',
menu2: '菜单2',
'menu1-1': '菜单 1-1',
'menu1-2': '菜单 1-2',
'menu1-2-1': '菜单 1-2-1',
'menu1-2-2': '菜单 1-2-2',
'menu1-3': '菜单 1-3',
menu2: '菜单 2',
Table: 'Table',
dynamicTable: '动态Table',
dragTable: '拖拽Table',
inlineEditTable: 'Table内编辑',
complexTable: '综合Table',
dynamicTable: '动态 Table',
dragTable: '拖拽 Table',
inlineEditTable: 'Table 内编辑',
complexTable: '综合 Table',
tab: 'Tab',
form: '表单',
createArticle: '创建文章',
@ -91,7 +89,7 @@ export default {
editPermission: '编辑权限',
roles: '你的权限',
switchRoles: '切换权限',
tips: '在某些情况下,不适合使用 v-permission。例如Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。',
tips: '在某些情况下,不适合使用 v-permission。例如Element-UI 的 el-tab 或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。',
delete: '删除',
confirm: '确定',
cancel: '取消'

View File

@ -3,7 +3,7 @@
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="$route.path"
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
@ -30,6 +30,15 @@ export default {
'permission_routes',
'sidebar'
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},

View File

@ -243,7 +243,7 @@ export default {
.contextmenu {
margin: 0;
background: #fff;
z-index: 100;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;

View File

@ -12,32 +12,32 @@ import chartsRouter from './modules/charts'
import tableRouter from './modules/table'
import nestedRouter from './modules/nested'
/** note: sub-menu only appear when children.length>=1
* detail see https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
**/
/**
* hidden: true if `hidden:true` will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu, whatever its child routes length
* if not set alwaysShow, only more than one route under the children
* it will becomes nested mode, otherwise not show the root menu
* redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin','editor'] will control the page roles (you can set multiple roles)
title: 'title' the name show in sub-menu and breadcrumb (recommend set)
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true if set true, item will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu
* if not set alwaysShow, when item has more than one children route,
* it will becomes nested mode, otherwise not show the root menu
* redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin','editor'] control the page roles (you can set multiple roles)
title: 'title' the name show in sidebar and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar
noCache: true if true, the page will no be cached(default is false)
breadcrumb: false if false, the item will hidden in breadcrumb(default is true)
affix: true if true, the tag will affix in the tags-view
noCache: true if set true, the page will no be cached(default is false)
affix: true if set true, the tag will affix in the tags-view
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
}
**/
*/
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
* */
*/
export const constantRoutes = [
{
path: '/redirect',
@ -57,7 +57,7 @@ export const constantRoutes = [
},
{
path: '/auth-redirect',
component: () => import('@/views/login/authredirect'),
component: () => import('@/views/login/authRedirect'),
hidden: true
},
{
@ -113,7 +113,7 @@ export const constantRoutes = [
/**
* asyncRoutes
* the routes that need to be dynamically loaded based on user roles
*/
*/
export const asyncRoutes = [
{
path: '/permission',
@ -195,7 +195,7 @@ export const asyncRoutes = [
path: 'edit/:id(\\d+)',
component: () => import('@/views/example/edit'),
name: 'EditArticle',
meta: { title: 'editArticle', noCache: true },
meta: { title: 'editArticle', noCache: true, activeMenu: '/example/list' },
hidden: true
},
{

View File

@ -28,10 +28,10 @@ export default {
sidebarLogo: false,
/**
* @type {string | array} 'production' | ['production','development']
* @type {string | array} 'production' | ['production', 'development']
* @description Need show err logs component.
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production','development']
* If you want to also use it in dev, you can pass ['production', 'development']
*/
errorLog: 'production'
}

View File

@ -1,24 +1,24 @@
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import errorLog from './modules/errorLog'
import permission from './modules/permission'
import tagsView from './modules/tagsView'
import settings from './modules/settings'
import user from './modules/user'
import getters from './getters'
Vue.use(Vuex)
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', false, /\.js$/)
// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
// set './app.js' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
const store = new Vuex.Store({
modules: {
app,
errorLog,
permission,
tagsView,
settings,
user
},
modules,
getters
})

View File

@ -1,4 +1,5 @@
import Cookies from 'js-cookie'
import { getLanguage } from '@/lang/index'
const state = {
sidebar: {
@ -6,7 +7,7 @@ const state = {
withoutAnimation: false
},
device: 'desktop',
language: Cookies.get('language') || 'en',
language: getLanguage(),
size: Cookies.get('size') || 'medium'
}

View File

@ -1,4 +1,3 @@
const state = {
logs: []
}

View File

@ -1,4 +1,3 @@
const state = {
visitedViews: [],
cachedViews: []

View File

@ -28,10 +28,6 @@
.scrollbar-wrapper {
overflow-x: hidden !important;
.el-scrollbar__view {
height: 100%;
}
}
.el-scrollbar__bar.is-vertical {

View File

@ -23,7 +23,7 @@ service.interceptors.request.use(
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
return Promise.reject(error)
}
)
@ -43,7 +43,7 @@ service.interceptors.response.use(
const res = response.data
if (res.code !== 20000) {
Message({
message: res.message,
message: res.message || 'error',
type: 'error',
duration: 5 * 1000
})
@ -61,7 +61,7 @@ service.interceptors.response.use(
})
})
}
return Promise.reject('error')
return Promise.reject(res.message || 'error')
} else {
return res
}

View File

@ -2,7 +2,7 @@
<div style="display:inline-block;">
<!-- $t is vue-i18n global function to translate lang -->
<label class="radio-label" style="padding-left:0;">Filename: </label>
<el-input v-model="filename" :placeholder="$t('excel.placeholder')" style="width:340px;" prefix-icon="el-icon-document" />
<el-input v-model="filename" :placeholder="$t('excel.placeholder')" style="width:350px;" prefix-icon="el-icon-document" />
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="app-container">
<!-- $t is vue-i18n global function to translate lang -->
<el-input v-model="filename" :placeholder="$t('excel.placeholder')" style="width:340px;" prefix-icon="el-icon-document" />
<el-input v-model="filename" :placeholder="$t('excel.placeholder')" style="width:350px;" prefix-icon="el-icon-document" />
<el-button :loading="downloadLoading" style="margin-bottom:20px" type="primary" icon="document" @click="handleDownload">
{{ $t('excel.selectedExport') }}
</el-button>

View File

@ -1,6 +1,6 @@
<script>
export default {
name: 'Authredirect',
name: 'AuthRedirect',
created() {
const hash = window.location.search.slice(1)
if (window.localStorage) {

View File

@ -23,24 +23,28 @@
/>
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
:placeholder="$t('login.password')"
name="password"
auto-complete="on"
@keyup.enter.native="handleLogin"
/>
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
</el-form-item>
<el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
:placeholder="$t('login.password')"
name="password"
auto-complete="on"
@keyup.native="checkCapslock"
@blur="capsTooltip = false"
@keyup.enter.native="handleLogin"
/>
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
</el-form-item>
</el-tooltip>
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">
{{ $t('login.logIn') }}
@ -77,7 +81,7 @@
<script>
import { validUsername } from '@/utils/validate'
import LangSelect from '@/components/LangSelect'
import SocialSign from './socialsignin'
import SocialSign from './socialSignin'
export default {
name: 'Login',
@ -107,6 +111,7 @@ export default {
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
},
passwordType: 'password',
capsTooltip: false,
loading: false,
showDialog: false,
redirect: undefined
@ -134,6 +139,18 @@ export default {
// window.removeEventListener('storage', this.afterQRScan)
},
methods: {
checkCapslock({ shiftKey, key } = {}) {
if (key && key.length === 1) {
if (shiftKey && (key >= 'a' && key <= 'z') || !shiftKey && (key >= 'A' && key <= 'Z')) {
this.capsTooltip = true
} else {
this.capsTooltip = false
}
}
if (key === 'CapsLock' && this.capsTooltip === true) {
this.capsTooltip = false
}
},
showPwd() {
if (this.passwordType === 'password') {
this.passwordType = ''

View File

@ -1,9 +1,9 @@
<template>
<div>
<div style="margin-bottom:15px;">
{{ $t('permission.roles') }} {{ roles }}
{{ $t('permission.roles') }}: {{ roles }}
</div>
{{ $t('permission.switchRoles') }}
{{ $t('permission.switchRoles') }}:
<el-radio-group v-model="switchRoles">
<el-radio-button label="editor" />
<el-radio-button label="admin" />

View File

@ -54,10 +54,10 @@
</el-table>
<!-- $t is vue-i18n global function to translate lang (lang in @/lang) -->
<div class="show-d">
{{ $t('table.dragTips1') }} : &nbsp; {{ oldList }}
<el-tag style="margin-right:12px;">{{ $t('table.dragTips1') }} :</el-tag> {{ oldList }}
</div>
<div class="show-d">
{{ $t('table.dragTips2') }} : {{ newList }}
<el-tag>{{ $t('table.dragTips2') }} :</el-tag> {{ newList }}
</div>
</div>
</template>
@ -113,9 +113,9 @@ export default {
this.sortable = Sortable.create(el, {
ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
setData: function(dataTransfer) {
dataTransfer.setData('Text', '')
// to avoid Firefox bug
// Detail see : https://github.com/RubaXa/Sortable/issues/1012
dataTransfer.setData('Text', '')
},
onEnd: evt => {
const targetRow = this.list.splice(evt.oldIndex, 1)[0]

View File

@ -3,7 +3,7 @@
<!-- $t is vue-i18n global function to translate lang -->
<el-input v-model="filename" :placeholder="$t('zip.placeholder')" style="width:300px;" prefix-icon="el-icon-document" />
<el-button :loading="downloadLoading" style="margin-bottom:20px;" type="primary" icon="document" @click="handleDownload">
{{ $t('zip.export') }} zip
{{ $t('zip.export') }} Zip
</el-button>
<el-table v-loading="listLoading" :data="list" element-loading-text="拼命加载中" border fit highlight-current-row>
<el-table-column align="center" label="ID" width="95">

View File

@ -41,22 +41,7 @@ module.exports = {
}
}
},
after(app) {
require('@babel/register')
const bodyParser = require('body-parser')
// parse app.body
// http://expressjs.com/en/4x/api.html#req.body
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: true
}))
const { default: mocks } = require('./mock')
for (const mock of mocks) {
app[mock.type](mock.url, mock.response)
}
}
after: require('./mock/mock-server.js')
},
configureWebpack: {
// provide the app's title in webpack's name field, so that