Browse Source

初始化提交

old
wangqing 6 years ago
commit
09780d3e0b
  1. 12
      .editorconfig
  2. 2
      .env.development
  3. 2
      .env.production
  4. 2
      .eslintignore
  5. 87
      .eslintrc.js
  6. 23
      .gitignore
  7. 2
      .stylelintignore
  8. 14
      .stylelintrc
  9. 65
      README.md
  10. 5
      babel.config.js
  11. 55
      package.json
  12. BIN
      public/favicon.ico
  13. 17
      public/index.html
  14. 65
      scss.template.handlebars
  15. 41
      src/App.vue
  16. 85
      src/api/index.js
  17. 1
      src/assets/icons/example.color.svg
  18. 1
      src/assets/icons/example.svg
  19. BIN
      src/assets/images/example.png
  20. BIN
      src/assets/sprites/example/address.png
  21. BIN
      src/assets/sprites/example/feedback.png
  22. BIN
      src/assets/sprites/example/payment.png
  23. 8
      src/assets/styles/example.scss
  24. 52
      src/assets/styles/resources/utils.scss
  25. 0
      src/assets/styles/resources/variables.scss
  26. 16
      src/components/ExampleList/index.vue
  27. 21
      src/components/autoRegister.js
  28. 18
      src/components/global/ExampleNotice/index.js
  29. 42
      src/components/global/ExampleNotice/main.vue
  30. 36
      src/components/global/SvgIcon/index.vue
  31. 44
      src/layout/example.vue
  32. 36
      src/main.js
  33. 50
      src/router/index.js
  34. 84
      src/router/modules/example.js
  35. 10
      src/router/modules/root.js
  36. 15
      src/store/index.js
  37. 44
      src/store/modules/example.js
  38. 19
      src/store/modules/global.js
  39. 60
      src/store/modules/token.js
  40. 3
      src/util/constants.js
  41. 12
      src/util/index.js
  42. 84
      src/util/signMd5Utils.js
  43. 52
      src/views/example/axios.vue
  44. 21
      src/views/example/component.vue
  45. 31
      src/views/example/cookie.vue
  46. 25
      src/views/example/global.component.vue
  47. 18
      src/views/example/meta.vue
  48. 5
      src/views/example/params.vue
  49. 20
      src/views/example/permission.js.vue
  50. 3
      src/views/example/permission.router.vue
  51. 5
      src/views/example/query.vue
  52. 26
      src/views/example/reload.vue
  53. 50
      src/views/example/sprite.vue
  54. 18
      src/views/example/svgicon.vue
  55. 94
      src/views/example/user.vue
  56. 42
      src/views/example/vuex.vue
  57. 5
      src/views/index.vue
  58. 28
      src/views/login.vue
  59. 95
      vue.config.js
  60. 10220
      yarn.lock

12
.editorconfig

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

2
.env.development

@ -0,0 +1,2 @@
VUE_APP_TITLE = 填鸭测试环境
VUE_APP_API_ROOT = /

2
.env.production

@ -0,0 +1,2 @@
VUE_APP_TITLE = 网站标题
VUE_APP_API_ROOT = https://yigou.ketao.com/api/

2
.eslintignore

@ -0,0 +1,2 @@
dist/
node_modules/

87
.eslintrc.js

@ -0,0 +1,87 @@
module.exports = {
root: true,
env: {
browser: true,
es6: true
},
globals: {
process: true,
require: true
},
extends: [
// 'plugin:vue/strongly-recommended',
// 'eslint:recommended'
],
parserOptions: {
ecmaVersion: 2015,
parser: 'babel-eslint',
sourceType: 'module'
},
rules: {
// 代码风格
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-dangle': [2, 'never'],
'comma-style': [2, 'last'],
'computed-property-spacing': [2, 'never'],
'indent': [2, 4, {
'SwitchCase': 1
}],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'linebreak-style': 0,
'multiline-ternary': [2, 'always-multiline'],
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'quotes': [2, 'single'],
'semi': [2, 'never'],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'switch-colon-spacing': [2, {
'after': true,
'before': false
}],
// ES6
'arrow-parens': [2, 'as-needed'],
'arrow-spacing': [2, {
'before': true,
'after': true
}],
// Vue - https://github.com/vuejs/eslint-plugin-vue
'vue/html-indent': [2, 4],
'vue/max-attributes-per-line': 0,
'vue/require-default-prop': 0,
'vue/singleline-html-element-content-newline': 0,
'vue/attributes-order': 2,
'vue/order-in-components': 2,
'vue/this-in-template': 2,
'vue/script-indent': [2, 4, {
'switchCase': 1
}]
}
};

23
.gitignore

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
/dist-dev
/src/assets/sprites/*.*
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

2
.stylelintignore

@ -0,0 +1,2 @@
dist/
node_modules/

14
.stylelintrc

@ -0,0 +1,14 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-recommended-scss"
],
"plugins": [
"stylelint-scss"
],
"rules": {
"indentation": 4,
"rule-empty-line-before": "never",
"at-rule-empty-line-before": "never"
}
}

65
README.md

@ -0,0 +1,65 @@
# tduck-front
## 说明
该仓库是为统一 Vue 项目初期配置而设立,方便快速进行业务开发,基于 Vue CLI 3 。
## 使用
### 方法 1
> 适用于初学者快速上手,项目里包含演示文件,方便学习
拉取该项目到本地,安装依赖包后运行即可。
运行后,可以看到功能演示,同时项目目录里带有 `example` 的目录均为演示代码。
## 依赖
- vue-router
- vuex
- axios
- lodash
- dayjs
- vue-cookies
- vue-meta
- node-sass
- sass-loader
- sass-resources-loader
- svg-sprite-loader
- webpack-spritesmith
## 全局 SCSS 资源
> 全局 SCSS 资源通过 [sass-resources-loader](https://www.npmjs.com/package/sass-resources-loader) 实现
>
> 注意!并不是全局样式,而是 SCSS 资源,是变量、@mixin 、@function 这些东西
`assets/styles/resources/` 目录下存放全局的 scss 资源,也就是说在这个目录里的文件,无需在页面上引用即可生效并使用。
例子中默认存放了 `utils.scss` 文件,里面有几个 `@mixin``%` ,你可以尝试在页面中使用它们看看效果。
同样, `assets/sprites/` 目录下生成的 scss 文件也是默认全局。
## 全局组件
> 全局组件会自动注册,可直接使用。
`components/global/` 目录下存放全局组件,需要注意各个组件按文件夹区分,文件夹名称与组件名无关联。
每个组件的文件夹内至少保留一个文件名为 index 的组件入口,例如 index.vue 。
普通组件必须设置 name 并保证其唯一,自动注册会将组件的 name 设为组件名,可参考 SvgIcon 组件写法。
如果组件是通过 js 进行调用,则确保组件入口文件为 index.js,可参考 ExampleNotice 组件。
## 路由
路由也实现自动注册,但因为有优先级的概念,先定义的会先匹配,所以同一模块下的路由必须放在一个路由配置文件里,具体可参考 `router/modules/example.js` 文件。
## Vuex
Vuex 同样实现了自动注册,开发只需关注 `store/modules/` 文件夹里的文件即可,同样也按照模块区分文件。

5
babel.config.js

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

55
package.json

@ -0,0 +1,55 @@
{
"name": "tduck-front",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build-dev": "vue-cli-service build --mode development --dest dist-dev",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"svgo": "svgo -f src/assets/icons"
},
"dependencies": {
"axios": "^0.19.0",
"core-js": "^3.4.1",
"dayjs": "^1.8.17",
"element-ui": "^2.13.0",
"js-md5": "^0.7.3",
"lodash": "^4.17.15",
"nprogress": "^0.2.0",
"vue": "^2.6.0",
"vue-cookies": "^1.5.7",
"vue-meta": "^2.3.1",
"vue-router": "^3.1.3",
"vuex": "^3.1.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.0.0",
"@vue/cli-plugin-eslint": "^4.0.0",
"@vue/cli-service": "^4.0.0",
"babel-eslint": "^10.0.3",
"eslint": "^6.6.0",
"eslint-plugin-vue": "^6.0.1",
"node-sass": "^4.10.0",
"sass-loader": "^8.0.0",
"sass-resources-loader": "^2.0.1",
"stylelint": "^12.0.0",
"stylelint-config-recommended-scss": "^4.0.0",
"stylelint-config-standard": "^19.0.0",
"stylelint-scss": "^3.4.1",
"svg-sprite-loader": "^4.1.6",
"svgo": "^1.3.0",
"vue-template-compiler": "^2.6.0",
"webpack-spritesmith": "^1.0.2"
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

17
public/index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>vue-automation</title>
</head>
<body>
<noscript>
<strong>We're sorry but vue-automation doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

65
scss.template.handlebars

@ -0,0 +1,65 @@
{
// Default options
'functions': true,
'variableNameTransforms': ['dasherize']
}
{{#block "sprites"}}
{{#each sprites}}
${{../spritesheet_info.strings.name}}-sprite-{{strings.name}}: ({{px.x}}, {{px.y}}, {{px.offset_x}}, {{px.offset_y}}, {{px.width}}, {{px.height}}, {{px.total_width}}, {{px.total_height}}, '{{{escaped_image}}}', '{{name}}');
{{/each}}
${{spritesheet_info.strings.name}}-sprites: (
{{#each sprites}}
{{strings.name}}: ${{../spritesheet_info.strings.name}}-sprite-{{strings.name}},
{{/each}}
);
{{/block}}
{{#block "sprite-functions"}}
{{#if options.functions}}
@mixin {{spritesheet_info.strings.name}}-sprite-width($sprite) {
width: nth($sprite, 5);
}
@mixin {{spritesheet_info.strings.name}}-sprite-height($sprite) {
height: nth($sprite, 6);
}
@mixin {{spritesheet_info.strings.name}}-sprite-position($sprite) {
$sprite-offset-x: nth($sprite, 3);
$sprite-offset-y: nth($sprite, 4);
background-position: $sprite-offset-x $sprite-offset-y;
}
@mixin {{spritesheet_info.strings.name}}-sprite-size($sprite) {
background-size: nth($sprite, 7) nth($sprite, 8);
}
@mixin {{spritesheet_info.strings.name}}-sprite-image($sprite) {
$sprite-image: nth($sprite, 9);
background-image: url(#{$sprite-image});
}
@mixin {{spritesheet_info.strings.name}}-sprite($name) {
@include {{spritesheet_info.strings.name}}-sprite-image(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
@include {{spritesheet_info.strings.name}}-sprite-position(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
@include {{spritesheet_info.strings.name}}-sprite-size(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
@include {{spritesheet_info.strings.name}}-sprite-width(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
@include {{spritesheet_info.strings.name}}-sprite-height(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
}
{{/if}}
{{/block}}
{{#block "spritesheet-functions"}}
{{#if options.functions}}
@mixin all-{{spritesheet_info.strings.name}}-sprites() {
@each $key, $val in ${{spritesheet_info.strings.name}}-sprites {
$sprite-name: nth($val, 10);
.{{spritesheet_info.strings.name}}-#{$sprite-name}-sprites {
@include {{spritesheet_info.strings.name}}-sprite($key);
}
}
}
{{/if}}
{{/block}}

41
src/App.vue

@ -0,0 +1,41 @@
<template>
<div id="app">
<router-view v-if="isRouterAlive" />
</div>
</template>
<script>
export default {
provide() {
return {
reload: this.reload
}
},
data() {
return {
isRouterAlive: true
}
},
watch: {
$route: 'routeChange'
},
methods: {
reload() {
this.isRouterAlive = false
this.$nextTick(() => (this.isRouterAlive = true))
},
routeChange(newVal, oldVal) {
if (newVal.name == oldVal.name) {
this.reload()
}
}
},
metaInfo: {
titleTemplate: title => {
return title
? `${title} - ${process.env.VUE_APP_TITLE}`
: process.env.VUE_APP_TITLE
}
}
}
</script>

85
src/api/index.js

@ -0,0 +1,85 @@
import axios from 'axios'
import router from '@/router/index'
import store from '@/store/index'
import signMd5Utils from '@/util/signMd5Utils'
const toLogin = () => {
router.push({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
})
}
const api = axios.create({
baseURL: process.env.VUE_APP_API_ROOT,
timeout: 1000 * 30,
withCredentials: false,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
})
api.interceptors.request.use(
request => {
if (request.method == 'post') {
if (request.data instanceof FormData) {
if (store.getters['token/isLogin']) {
// 如果是 FormData 类型(上传图片)
request.data.append('token', store.state.token.token)
}
} else {
// 带上 token
if (request.data == undefined) {
request.data = {}
}
if (store.getters['token/isLogin']) {
request.data.token = store.state.token.token
}
let timestamp = new Date().getTime()
request.data.timestamp = '' + timestamp
let sign = signMd5Utils.getSign(request.url, request.data)
request.data.sign = sign
request.data = JSON.stringify(request.data)
}
} else {
// 带上 token
if (request.params == undefined) {
request.params = {}
}
if (store.getters['token/isLogin']) {
request.params.token = store.state.token.token
}
let timestamp = new Date().getTime()
console.log(request.params)
request.params.timestamp = '' + timestamp
let sign = signMd5Utils.getSign(request.url, request.params)
request.params.sign = sign
}
return request
}
)
api.interceptors.response.use(
response => {
if (response.data.code != 200) {
// 如果接口请求时发现 token 失效,则立马跳转到登录页
if (response.data.code == 0) {
toLogin()
return false
}
return Promise.reject(response.data)
}
return Promise.resolve(response.data)
},
error => {
return Promise.reject(error)
}
)
export {
axios,
api
}

1
src/assets/icons/example.color.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

1
src/assets/icons/example.svg

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1562379556323" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2175" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M770.56 460.8h250.88C998.4 220.16 803.84 25.6 563.2 2.56v250.88c104.96 20.48 186.88 102.4 207.36 207.36zM460.8 253.44V2.56C220.16 25.6 25.6 220.16 2.56 460.8h250.88c20.48-104.96 102.4-186.88 207.36-207.36z m102.4 517.12v250.88c243.2-23.04 435.2-217.6 460.8-460.8H773.12C750.08 668.16 668.16 750.08 563.2 770.56zM253.44 563.2H2.56c23.04 243.2 217.6 435.2 460.8 460.8V773.12C355.84 750.08 273.92 668.16 253.44 563.2z m0 0" p-id="2176"></path></svg>

After

Width:  |  Height:  |  Size: 823 B

BIN
src/assets/images/example.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/sprites/example/address.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/assets/sprites/example/feedback.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
src/assets/sprites/example/payment.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

8
src/assets/styles/example.scss

@ -0,0 +1,8 @@
// 改目录下可存放第三方样式文件或者公用样式
// 该例子可在 view/example/sprite.vue 里查看
.sprites {
div {
border: 1px solid #000;
}
}

52
src/assets/styles/resources/utils.scss

@ -0,0 +1,52 @@
// @mixin 通过 @include 调用使用
// % 通过 @extend 调用使用
// 文字超出隐藏默认为单行超出隐藏可设置多行
@mixin text-overflow($line: 1, $fixed-width: true) {
@if ($line==1 and $fixed-width==true) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@else {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $line;
overflow: hidden;
}
}
// 定位居中默认水平居中可选择垂直居中或者水平垂直都居中
@mixin position-center($type: x) {
position: absolute;
@if ($type==x) {
left: 50%;
transform: translateX(-50%);
}
@if ($type==y) {
top: 50%;
transform: translateY(-50%);
}
@if ($type==xy) {
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
}
// 文字两端对齐
%justify-align {
text-align: justify;
text-align-last: justify;
}
// 清除浮动
%clearfix {
zoom: 1;
&::before,
&::after {
content: '';
display: block;
clear: both;
}
}

0
src/assets/styles/resources/variables.scss

16
src/components/ExampleList/index.vue

@ -0,0 +1,16 @@
<template>
<div>
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'ExampleList',
props: {
list: Array
}
}
</script>

21
src/components/autoRegister.js

@ -0,0 +1,21 @@
/**
* 全局组件自动注册
*
* 全局组件统一放在 ./global 目录下需要注意各个组件按文件夹区分文件夹名称与组件名无关联
* 文件夹内至少保留一个文件名为 index 的组件入口例如 index.vue
* 普通组件必须设置 name 并保证其唯一自动注册会将组件的 name 设为组件名可参考 SvgIcon 组件写法
* 如果组件是通过 js 进行调用则确保组件入口文件为 index.js可参考 ExampleNotice 组件
*/
import Vue from 'vue'
const componentsContext = require.context('./global', true, /index.(vue|js)$/)
componentsContext.keys().forEach(file_name => {
// 获取文件中的 default 模块
const componentConfig = componentsContext(file_name).default
if (/.vue$/.test(file_name)) {
Vue.component(componentConfig.name, componentConfig)
} else {
Vue.prototype[`$${componentConfig.name}`] = componentConfig
}
})

18
src/components/global/ExampleNotice/index.js

@ -0,0 +1,18 @@
import Vue from 'vue'
const constructor = Vue.extend(require('./main.vue').default)
let instance
const exampleNotice = options => {
options = options || {}
instance = new constructor({
data: options
})
instance.vm = instance.$mount()
instance.dom = instance.vm.$el
document.body.appendChild(instance.dom)
return instance.vm
}
export default exampleNotice

42
src/components/global/ExampleNotice/main.vue

@ -0,0 +1,42 @@
<template>
<transition name="notice">
<div v-if="show" class="notice">
{{ content }}
</div>
</transition>
</template>
<script>
export default {
name: 'ExampleNotice',
data() {
return {
show: false,
content: ''
}
},
mounted() {
this.show = true
setTimeout(() => {
this.show = false
}, 2000)
}
}
</script>
<style lang="scss" scoped>
.notice {
padding: 10px;
background-color: #eee;
border-radius: 10px;
@include position-center(xy);
}
.notice-leave-active,
.notice-enter-active {
transition: all 0.3s;
}
.notice-enter,
.notice-leave-to {
opacity: 0;
}
</style>

36
src/components/global/SvgIcon/index.vue

@ -0,0 +1,36 @@
<template>
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="`#icon-${iconClass}`" />
</svg>
</template>
<script>
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
svgClass() {
return this.className ? ('svg-icon ' + this.className) : 'svg-icon'
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

44
src/layout/example.vue

@ -0,0 +1,44 @@
<template>
<div>
<div id="nav">
<RouterLink to="/example/sprite">sprite精灵图</RouterLink>
<RouterLink to="/example/svgicon">svg icon</RouterLink>
<RouterLink to="/example/globalComponent">全局组件</RouterLink>
<RouterLink to="/example/axios">axios</RouterLink>
<RouterLink to="/example/cookie">cookie</RouterLink>
<RouterLink to="/example/meta">meta</RouterLink>
<RouterLink to="/example/vuex">vuex</RouterLink>
<RouterLink to="/example/component">组件</RouterLink>
<RouterLink :to="{name:'exampleParams',params:{test:'123'}}">路由params</RouterLink>
<RouterLink :to="{path:'/example/query',query:{test:'123'}}">路由query</RouterLink>
<RouterLink to="/example/reload">刷新当前页面</RouterLink>
<RouterLink to="/example/permission/router">router鉴权</RouterLink>
<RouterLink to="/example/permission/js">js鉴权</RouterLink>
<RouterLink to="/example/user">基本操作</RouterLink>
</div>
<RouterView />
</div>
</template>
<style lang="scss" scoped>
#nav {
margin-bottom: 10px;
a {
text-decoration: none;
font-size: 14px;
&::after {
content: '|';
margin: 0 10px;
font-weight: normal;
font-size: 14px;
}
&:last-child::after {
content: none;
}
&.router-link-active {
font-weight: bold;
font-size: 18px;
}
}
}
</style>

36
src/main.js

@ -0,0 +1,36 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
import store from './store/index'
import lodash from 'lodash'
import {api, axios} from './api'
import dayjs from 'dayjs'
import util from './util/index'
import meta from 'vue-meta'
import cookies from 'vue-cookies'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css';
// 全局组件自动注册
import '@/components/autoRegister'
Vue.use(meta)
Vue.use(cookies)
Vue.use(util)
Vue.use(Element, {size: 'small', zIndex: 3000})
Vue.prototype.$api = api
Vue.prototype.$axios = axios
Vue.prototype._ = lodash
Vue.prototype.$dayjs = dayjs
Vue.config.productionTip = false
// 自动加载 svg 图标
const req = require.context('./assets/icons', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

50
src/router/index.js

@ -0,0 +1,50 @@
import Vue from 'vue'
import Router from 'vue-router'
import store from '@/store/index'
import flattenDeep from 'lodash/flattenDeep'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css' // progress bar style
Vue.use(Router)
/**
* 因为路由有优先级的概念先定义的会先匹配而自动注册是依据文件名的排序来遍历的
* 所以下面这种情况如果访问 /news/edit 会指向到 info.vue 页面上
* a.js /news/:id info.vue
* b.js /news/edit edit.vue
* 为避免这种情况发生同一模块下的路由必须放在一个路由配置文件里
* 按上面的例子news 模块的路由应该放到一个类似于 news.js 的文件里
* 至于模块里的路由优先级可以把 /news/edit 放在 /news/:id 前面或者把 /news/:id 改成 /news/info/:id 均可
*/
const routes = []
const require_module = require.context('./modules', false, /.js$/)
require_module.keys().forEach(file_name => {
routes.push(require_module(file_name).default)
})
const router = new Router({
routes: flattenDeep(routes)
})
router.beforeEach((to, from, next) => {
NProgress.start()
if (to.meta.requireLogin) {
if (store.getters['token/isLogin']) {
next()
NProgress.done()
} else {
next({
path: '/login',
query: {
redirect: to.fullPath
}
})
NProgress.done()
}
} else {
next()
NProgress.done()
}
})
export default router

84
src/router/modules/example.js

@ -0,0 +1,84 @@
import ExampleLayout from '@/layout/example'
export default {
path: '/example',
redirect: '/example/sprite',
component: ExampleLayout,
children: [
{
path: 'sprite',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/sprite.vue')
},
{
path: 'svgicon',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/svgicon.vue')
},
{
path: 'globalComponent',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/global.component.vue')
},
{
path: 'axios',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/axios.vue')
},
{
path: 'cookie',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/cookie.vue')
},
{
path: 'meta',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/meta.vue')
},
{
path: 'vuex',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/vuex.vue')
},
{
path: 'component',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/component.vue')
},
{
path: 'params/:test',
name: 'exampleParams', // 设置路由的name时,建议加上模块名,避免name和其他模块重名
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/params.vue')
},
{
path: 'query',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/query.vue')
},
{
path: 'reload',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/reload.vue')
},
{
path: 'permission/router',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/permission.router.vue'),
meta: {
requireLogin: true // 鉴权
}
},
{
path: 'permission/js',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/permission.js.vue')
},
{
path: 'user',
component: () =>
import(/* webpackChunkName: 'example' */ '@/views/example/user.vue')
}
]
}

10
src/router/modules/root.js

@ -0,0 +1,10 @@
export default [
{
path: '/',
component: () => import(/* webpackChunkName: 'root' */ '@/views/index.vue')
},
{
path: '/login',
component: () => import(/* webpackChunkName: 'root' */ '@/views/login.vue')
}
]

15
src/store/index.js

@ -0,0 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const modules = {}
const require_module = require.context('./modules', false, /.js$/)
require_module.keys().forEach(file_name => {
modules[file_name.slice(2, -3)] = require_module(file_name).default
})
export default new Vuex.Store({
modules: modules,
strict: process.env.NODE_ENV !== 'production'
})

44
src/store/modules/example.js

@ -0,0 +1,44 @@
import {
api
} from '@/api'
const state = {
banner: []
}
const getters = {
bannerCount: state => {
return state.banner.length
}
}
const actions = {
getBanner({
commit
}) {
api.get('banner/list', {
params: {
categoryid: 1
}
}).then(res => {
commit('setBanner', res.data.banner)
})
}
}
const mutations = {
setBanner(state, banner) {
state.banner = banner
},
removeLast(state) {
state.banner.splice(state.banner.length - 1, 1)
}
}
export default {
namespaced: true,
state,
actions,
getters,
mutations
}

19
src/store/modules/global.js

@ -0,0 +1,19 @@
/**
* 存放全局公用状态
*/
const state = {}
const getters = {}
const actions = {}
const mutations = {}
export default {
namespaced: true,
state,
actions,
getters,
mutations
}

60
src/store/modules/token.js

@ -0,0 +1,60 @@
import {
api
} from '@/api'
const state = {
token: localStorage.token,
failuretime: localStorage.failuretime
}
const getters = {
isLogin: state => {
let retn = false
if (state.token != null) {
let unix = Date.parse(new Date())
if (unix < state.failuretime * 1000) {
retn = true
}
}
return retn
}
}
const actions = {
login({
commit
}, data) {
return new Promise((resolve, reject) => {
// 模拟登录成功,写入 token 信息
commit('setData', {
token: '1234567890',
failuretime: Date.parse(new Date()) / 1000 + 24 * 60 * 60
})
resolve()
// api.post('member/login', data).then(res => {
// commit('setData', res.data)
// resolve(res)
// }).catch(error => {
// reject(error)
// })
})
}
}
const mutations = {
setData(state, data) {
localStorage.setItem('token', data.token)
localStorage.setItem('failuretime', data.failuretime)
state.token = data.token
state.failuretime = data.failuretime
}
}
export default {
namespaced: true,
state,
actions,
getters,
mutations
}

3
src/util/constants.js

@ -0,0 +1,3 @@
export default {
signSecret: '916lWh2WMcbSWiHv'
}

12
src/util/index.js

@ -0,0 +1,12 @@
export default {
install(Vue) {
Vue.prototype.$toLogin = function() {
this.$router.push({
path: '/login',
query: {
redirect: this.$route.fullPath
}
})
}
}
}

84
src/util/signMd5Utils.js

@ -0,0 +1,84 @@
import md5 from 'js-md5'
export default class signMd5Utils {
/**
* json参数升序
* @param jsonObj 发送参数
*/
static sortAsc(jsonObj) {
let arr = new Array()
let num = 0
for (let i in jsonObj) {
arr[num] = i
num++
}
let sortArr = arr.sort()
let sortObj = {}
for (let i in sortArr) {
sortObj[sortArr[i]] = jsonObj[sortArr[i]]
}
return sortObj
}
/**
* @param url 请求的url,应该包含请求参数(url的?后面的参数)
* @param requestParams 请求参数(POST的JSON参数)
* @returns {string} 获取签名
*/
static getSign(url, requestParams) {
let urlParams = this.parseQueryString(url)
let jsonObj = this.mergeObject(urlParams, requestParams)
let requestBody = this.sortAsc(jsonObj)
return md5('916lWh2WMcbSWiHv' + JSON.stringify(requestBody)).toLowerCase()
}
/**
* @param url 请求的url
* @returns {{}} 将url中请求参数组装成json对象(url的?后面的参数)
*/
static parseQueryString(url) {
let urlReg = /^[^\?]+\?([\w\W]+)$/,
paramReg = /([^&=]+)=([\w\W]*?)(&|$|#)/g,
urlArray = urlReg.exec(url),
result = {}
if (urlArray && urlArray[1]) {
let paramString = urlArray[1], paramResult
while ((paramResult = paramReg.exec(paramString)) != null) {
result[paramResult[1]] = '' + paramResult[2]
}
}
return result
}
/**
* @returns {*} 将两个对象合并成一个
*/
static mergeObject(objectOne, objectTwo) {
if (Object.keys(objectTwo).length > 0) {
for (let key in objectTwo) {
// eslint-disable-next-line no-prototype-builtins
if (objectTwo.hasOwnProperty(key) === true) {
objectOne[key] = '' + objectTwo[key]
}
}
}
return objectOne
}
static urlEncode(param, key, encode) {
if (param == null) return ''
let paramStr = ''
let t = typeof (param)
if (t == 'string' || t == 'number' || t == 'boolean') {
paramStr += '&' + key + '=' + ((encode == null || encode) ? encodeURIComponent(param) : param)
} else {
for (let i in param) {
let k = key == null ? i : key + (param instanceof Array ? '[' + i + ']' : '.' + i)
paramStr += this.urlEncode(param[i], k, encode)
}
}
return paramStr
}
}

52
src/views/example/axios.vue

@ -0,0 +1,52 @@
signMd5Utils.js
<template>
<div>
<button type="button" @click="getInfo">获取数据</button>
<button type="button" @click="getTest">验签</button>
<img v-for="(item, index) in banner" :key="index" :src="item.image">
</div>
</template>
<script>
export default {
data() {
return {
banner: []
}
},
methods: {
getTest() {
this.$api.post('/api/v1/user/add').then(res => {
console.log(res)
})
},
getInfo() {
this.$axios.all([
this.$api.get('banner/list', {
params: {
categoryid: 1
}
}),
this.$api.get('banner/list', {
params: {
categoryid: 2
}
})
]).then(
this.$axios.spread((acct, perms) => {
this.banner = acct.data.banner.concat(
perms.data.banner
)
})
)
}
}
}
</script>
<style lang="scss" scoped>
img {
display: block;
width: 300px;
}
</style>

21
src/views/example/component.vue

@ -0,0 +1,21 @@
<template>
<div>
<p>这是一个非全局组件需要在页面上引用该组件才能使用</p>
<ExampleList :list="list" />
</div>
</template>
<script>
import ExampleList from '@/components/ExampleList'
export default {
components: {
ExampleList
},
data() {
return {
list: ['张三', '李四', '王五']
}
}
}
</script>

31
src/views/example/cookie.vue

@ -0,0 +1,31 @@
<template>
<div>
<button type="button" @click="setCookie">设置cookie</button>
<button type="button" @click="removeCookie">删除cookie</button>
<button type="button" @click="isSetCookie">判断cookie是否设置</button>
<div>a的cookie值是{{ cookie }}</div>
</div>
</template>
<script>
export default {
data() {
return {
cookie: ''
}
},
methods: {
setCookie() {
this.$cookies.set('a', 'abc')
this.cookie = this.$cookies.get('a')
},
removeCookie() {
this.$cookies.remove('a', 'abc')
this.cookie = this.$cookies.get('a')
},
isSetCookie() {
alert(this.$cookies.isKey('a'))
}
}
}
</script>

25
src/views/example/global.component.vue

@ -0,0 +1,25 @@
<template>
<div>
<p>全局组件会自动注册</p>
<p>使用方法</p>
<ol>
<li>全局组件统一放在 ./src/components/global/ 目录下需要注意各个组件按文件夹区分文件夹名称与组件名无关联</li>
<li>文件夹内至少保留一个文件名为 index 的组件入口例如 index.vue</li>
<li>普通组件必须设置 name 并保证其唯一自动注册会将组件的 name 设为组件名可参考 <RouterLink to="/example/svgicon">SvgIcon</RouterLink> 组件写法</li>
<li>如果组件是通过 js 进行调用则确保组件入口文件为 index.js下面演示 ExampleNotice 组件通过 js 调用并展示 Notice</li>
</ol>
<a href="javascript:;" @click="showNotice">显示Notice</a>
</div>
</template>
<script>
export default {
methods: {
showNotice() {
this.$exampleNotice({
content: '我是Notice!'
})
}
}
}
</script>

18
src/views/example/meta.vue

@ -0,0 +1,18 @@
<template>
<div>注意 title 的变化</div>
</template>
<script>
export default {
data() {
return {
title: '我是这个页面的title噢'
}
},
metaInfo() {
return {
title: this.title
}
}
}
</script>

5
src/views/example/params.vue

@ -0,0 +1,5 @@
<template>
<div>
<div>params:{{ $route.params.test }}</div>
</div>
</template>

20
src/views/example/permission.js.vue

@ -0,0 +1,20 @@
<template>
<div>
<p>如果未登录会跳转到登录页如果已登录则弹出用户信息</p>
<button @click="user">点我</button>
</div>
</template>
<script>
export default {
methods: {
user() {
if (this.$store.getters['token/isLogin']) {
alert('token信息:' + this.$store.state.token.token)
} else {
this.$toLogin()
}
}
}
}
</script>

3
src/views/example/permission.router.vue

@ -0,0 +1,3 @@
<template>
<div>token信息{{ $store.state.token.token }}</div>
</template>

5
src/views/example/query.vue

@ -0,0 +1,5 @@
<template>
<div>
<div>query:{{ $route.query.test }}</div>
</div>
</template>

26
src/views/example/reload.vue

@ -0,0 +1,26 @@
<template>
<div>
<p>可以修改一下 input 框内的值然后点击刷新按钮查看效果</p>
<p>
<input v-model="value" type="text">
<button type="button" @click="plus">+1</button>
</p>
<button type="button" @click="reload">刷新</button>
</div>
</template>
<script>
export default {
inject: ['reload'],
data() {
return {
value: 0
}
},
methods: {
plus() {
this.value += 1
}
}
}
</script>

50
src/views/example/sprite.vue

@ -0,0 +1,50 @@
<template>
<div class="sprites">
<div class="address" />
<div class="feedback" />
<div class="payment" />
<div class="info">
vue.config.js 里配置精灵图路径等信息如果要新增一个精灵图目录则先复制一份 new SpritesmithPlugin() 修改目录名和文件名然后重新运行 serve 任务即可
</div>
<img :src="logo" class="logo">
</div>
</template>
<script>
export default {
data() {
return {
logo: ''
}
},
created() {
// js require
this.logo = require('../../assets/images/example.png')
}
}
</script>
<style lang="scss" scoped>
.sprites {
padding: 10px;
.address,
.feedback,
.payment {
display: inline-block;
margin-right: 10px;
}
.address {
@include example-sprite(address);
}
.feedback {
@include example-sprite(feedback);
}
.payment {
@include example-sprite(payment);
}
}
.logo {
width: 200px;
height: 200px;
}
</style>

18
src/views/example/svgicon.vue

@ -0,0 +1,18 @@
<template>
<div>
<p>这是两个 Svg Icon 图标</p>
<svg-icon icon-class="example" class-name="example-icon" />
<svg-icon icon-class="example.color" class-name="example-icon" />
<p>使用方法</p>
<ol>
<li> <a href="https://www.iconfont.cn/" target="_blank">Iconfont</a> 下载需要的 svg 图标</li>
<li> svg 文件放入 assets/icons 目录文件名即为 icon-class</li>
</ol>
</div>
</template>
<style scoped>
.example-icon {
font-size: 48px;
}
</style>

94
src/views/example/user.vue

@ -0,0 +1,94 @@
<template>
<div>
<el-table
:data="tableData">
<el-table-column
prop="id"
label="Id"
width="180">
</el-table-column>
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="gender"
label="性别"
width="180">
</el-table-column>
<el-table-column
label="操作"
width="100">
<template slot-scope="scope">
<el-button @click="handleDeleteClick(scope.row,scope)" type="text" size="small">删除</el-button>
<el-button type="text" size="small">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="current"
:page-sizes="[10, 20, 50, 100]"
:page-size="size"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</template>
<script>
export default {
name: 'user',
data() {
return {
tableData: [],
current: 1,
size: 10,
total: 0
}
},
methods: {
handleSizeChange(size) {
debugger
this.size = size
this.getData()
},
handleCurrentChange(current) {
this.current = current
this.getData()
},
handleDeleteClick(row, scope) {
this.$api.post(`/api/v1/user/delete?id=${row.id}`).then(res => {
this.$message({
message: '删除成功',
type: 'success'
})
})
},
getData() {
let params = {current: this.current, size: this.size}
this.$api.get('/api/v1/user/page', {
params: params
}).then(res => {
this.tableData = res.data.records
this.size = res.data.size
this.total = res.data.total
this.current = res.data.current
})
}
},
mounted() {
this.getData()
}
}
</script>
<style scoped>
</style>

42
src/views/example/vuex.vue

@ -0,0 +1,42 @@
<template>
<div>
<button type="button" @click="getInfo">获取数据</button>
<button type="button" @click="removeLast">删除最后一条数据</button>
<button type="button" @click="getLength">获取数据长度</button>
<img v-for="(item, index) in banner" :key="index" :src="item.image">
<div v-if="bannerCount">现在你可以切换路由你会发现切换回来后数据还在</div>
</div>
</template>
<script>
import { mapGetters, mapActions, mapMutations, mapState } from 'vuex'
export default {
computed: {
...mapState({
banner: state => state.example.banner
}),
...mapGetters({
bannerCount: 'example/bannerCount'
})
},
methods: {
...mapActions({
getInfo: 'example/getBanner'
}),
...mapMutations({
removeLast: 'example/removeLast'
}),
getLength() {
alert(this.bannerCount)
}
}
}
</script>
<style lang="scss" scoped>
img {
display: block;
width: 300px;
}
</style>

5
src/views/index.vue

@ -0,0 +1,5 @@
<template>
<div>
<RouterLink to="/example">演示Demo</RouterLink>
</div>
</template>

28
src/views/login.vue

@ -0,0 +1,28 @@
<template>
<div>
<button @click="login">模拟登录</button>
</div>
</template>
<script>
export default {
methods: {
login() {
this.$store.dispatch('token/login').then(() => {
//
if (this.$route.query.redirect) {
this.$router.replace({
path: this.$route.query.redirect
})
} else {
if (window.history.length <= 1) {
this.$router.push({ path: '/' })
} else {
this.$router.go(-1)
}
}
})
}
}
}
</script>

95
vue.config.js

@ -0,0 +1,95 @@
const fs = require('fs')
const path = require('path')
const spritesmithPlugin = require('webpack-spritesmith')
// 基础路径 注意发布之前要先修改这里
const spritesmithTasks = []
fs.readdirSync('src/assets/sprites').map(dirname => {
if (fs.statSync(`src/assets/sprites/${dirname}`).isDirectory()) {
spritesmithTasks.push(
new spritesmithPlugin({
src: {
cwd: path.resolve(__dirname, `src/assets/sprites/${dirname}`),
glob: '*.png'
},
target: {
image: path.resolve(__dirname, `src/assets/sprites/${dirname}.[hash].png`),
css: [
[path.resolve(__dirname, `src/assets/sprites/_${dirname}.scss`), {
format: 'handlebars_based_template',
spritesheetName: dirname
}]
]
},
customTemplates: {
'handlebars_based_template': path.resolve(__dirname, 'scss.template.handlebars')
},
// 样式文件中调用雪碧图地址写法
apiOptions: {
cssImageRef: `~${dirname}.[hash].png`
},
spritesmithOptions: {
algorithm: 'binary-tree',
padding: 10
}
})
)
}
})
module.exports = {
publicPath: '',
lintOnSave: true,
devServer: {
open: true,
host: 'localhost',
port: '8081',
proxy: {
'/api': {
target: 'http://localhost:8888', // 要请求的地址
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
},
configureWebpack: {
resolve: {
modules: ['node_modules', 'assets/sprites']
},
plugins: [
...spritesmithTasks
]
},
chainWebpack: config => {
const oneOfsMap = config.module.rule('scss').oneOfs.store
oneOfsMap.forEach(item => {
item.use('sass-resources-loader')
.loader('sass-resources-loader')
.options({
resources: [
'./src/assets/styles/resources/*.scss',
'./src/assets/sprites/*.scss'
]
})
.end()
})
config.module
.rule('svg')
.exclude.add(path.join(__dirname, 'src/assets/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(path.join(__dirname, 'src/assets/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
}
}

10220
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save