├── .browserslistrc
├── .editorconfig
├── .env
├── .env.development
├── .env.production
├── .eslintrc.js
├── .github
└── workflows
│ └── codesee-arch-diagram.yml
├── .gitignore
├── README.md
├── babel.config.js
├── mock
├── article.js
├── index.js
├── mock-server.js
├── remote-search.js
├── role
│ ├── index.js
│ └── routes.js
└── user.js
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
└── index.html
├── src
├── App.vue
├── api
│ ├── article.js
│ └── user.js
├── assets
│ ├── logo.png
│ └── webpack.png
├── components
│ ├── FooterTabbar.vue
│ ├── SvgIcon.vue
│ └── VerifyCodeBtn.vue
├── icons
│ ├── index.js
│ ├── svg
│ │ ├── 404.svg
│ │ ├── dashboard.svg
│ │ ├── example.svg
│ │ ├── eye-open.svg
│ │ ├── eye.svg
│ │ ├── form.svg
│ │ ├── link.svg
│ │ ├── nested.svg
│ │ ├── password.svg
│ │ ├── table.svg
│ │ ├── tree.svg
│ │ └── user.svg
│ └── svgo.yml
├── main.js
├── router
│ ├── article.js
│ ├── index.js
│ └── user.js
├── settings.js
├── store
│ ├── index.js
│ └── modules
│ │ ├── test.js
│ │ └── user.js
├── style
│ ├── _mixin.scss
│ ├── _variables.scss
│ └── common.scss
├── utils
│ ├── auth.js
│ ├── get-page-title.js
│ ├── index.js
│ ├── permission.js
│ ├── request.js
│ ├── validate.js
│ └── vuex-loading.js
└── views
│ ├── 404.vue
│ ├── Article.vue
│ ├── Home.vue
│ └── user
│ ├── Login.vue
│ └── Register.vue
├── vue.config.js
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NODE_ENV = 'development'
2 | BASE_URL = '/'
3 | VUE_APP_BASE_API = '/dev-api'
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | VUE_CLI_BABEL_TRANSPILE_MODULES = true
2 | NODE_ENV = 'development'
3 | BASE_URL = '/'
4 | VUE_APP_BASE_API = '/dev-api'
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | NODE_ENV = 'production'
2 | BASE_URL = './'
3 | VUE_APP_BASE_API = '/prod-api'
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | 'extends': [
7 | 'plugin:vue/essential',
8 | '@vue/standard'
9 | ],
10 | rules: {
11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
13 | },
14 | parserOptions: {
15 | parser: 'babel-eslint'
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/codesee-arch-diagram.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - master
5 | pull_request_target:
6 | types: [opened, synchronize, reopened]
7 |
8 | name: CodeSee Map
9 |
10 | jobs:
11 | test_map_action:
12 | runs-on: ubuntu-latest
13 | continue-on-error: true
14 | name: Run CodeSee Map Analysis
15 | steps:
16 | - name: checkout
17 | id: checkout
18 | uses: actions/checkout@v2
19 | with:
20 | repository: ${{ github.event.pull_request.head.repo.full_name }}
21 | ref: ${{ github.event.pull_request.head.ref }}
22 | fetch-depth: 0
23 |
24 | # codesee-detect-languages has an output with id languages.
25 | - name: Detect Languages
26 | id: detect-languages
27 | uses: Codesee-io/codesee-detect-languages-action@latest
28 |
29 | - name: Configure JDK 16
30 | uses: actions/setup-java@v2
31 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).java }}
32 | with:
33 | java-version: '16'
34 | distribution: 'zulu'
35 |
36 | # CodeSee Maps Go support uses a static binary so there's no setup step required.
37 |
38 | - name: Configure Node.js 14
39 | uses: actions/setup-node@v2
40 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).javascript }}
41 | with:
42 | node-version: '14'
43 |
44 | - name: Configure Python 3.x
45 | uses: actions/setup-python@v2
46 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).python }}
47 | with:
48 | python-version: '3.x'
49 | architecture: 'x64'
50 |
51 | - name: Configure Ruby '3.x'
52 | uses: ruby/setup-ruby@v1
53 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).ruby }}
54 | with:
55 | ruby-version: '3.0'
56 |
57 | # CodeSee Maps Rust support uses a static binary so there's no setup step required.
58 |
59 | - name: Generate Map
60 | id: generate-map
61 | uses: Codesee-io/codesee-map-action@latest
62 | with:
63 | step: map
64 | github_ref: ${{ github.ref }}
65 | languages: ${{ steps.detect-languages.outputs.languages }}
66 |
67 | - name: Upload Map
68 | id: upload-map
69 | uses: Codesee-io/codesee-map-action@latest
70 | with:
71 | step: mapUpload
72 | api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}
73 | github_ref: ${{ github.ref }}
74 |
75 | - name: Insights
76 | id: insights
77 | uses: Codesee-io/codesee-map-action@latest
78 | with:
79 | step: insights
80 | api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}
81 | github_ref: ${{ github.ref }}
82 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 基于vue+vant搭建H5通用架子
2 |
3 | ### 项目初衷
4 |
5 | 开发一个H5的通用架子,让前端同学开箱即用,迅速投入战斗。
6 |
7 | ----
8 |
9 | 项目源码跟线上预览地址在文章结尾处,记得查收哦~**
10 |
11 | ### 主要功能
12 | 1. 常用目录别名
13 | 2. Vant/Rem适配
14 | 3. scss支持、_mixin.scss、_variables.scss
15 | 4. 页面切换动画+keepAlive
16 | 5. 页面标题
17 | 6. 自动注册:自动注册路由表/自动注册Vuex/svg图标引入
18 | 7. mock server
19 | 8. axios封装、api管理
20 | 9. 用户鉴权
21 | 10. vuex-loading
22 | 11. vo-pages/dayjs/vconsole
23 | 12. 生产环境优化
24 |
25 | ### 常用目录别名
26 | 
27 | ### Vant/Rem适配
28 | 按照Vant官网推荐自动按需引入组件,同样,Vant官网中也有对Rem适配的推荐配置,按照官网说明的使用。需要注意的是postcss的配置中,autoprefixer下的`browsers`需要替换成`overrideBrowserslist`,否则会有报错信息。具体如图
29 | 
30 | ### scss支持、_mixin.scss、_variables.scss
31 | 选择scss作为css预处理,并对mixin、variables、common.scss作全局引入。
32 | ```js
33 | css: {
34 | // 是否使用css分离插件 ExtractTextPlugin
35 | extract: !!IS_PRODUCTION,
36 | // 开启 CSS source maps?
37 | sourceMap: false,
38 | // css预设器配置项
39 | // 启用 CSS modules for all css / pre-processor files.
40 | modules: false,
41 | loaderOptions: {
42 | sass: {
43 | data: '@import "style/_mixin.scss";@import "style/_variables.scss";@import "style/common.scss";' // 全局引入
44 | }
45 | }
46 | }
47 | ```
48 | ### 页面切换动画+keepAlive
49 | 利用vuex存取/更新页面切换方向,配合vue的transition做页面切换动画,router设置keepAlive判断页面是否需要缓冲。
50 | ```js
51 | // vuex中
52 | state: {
53 | direction: 'forward' // 页面切换方向
54 | },
55 | mutations: {
56 | // 更新页面切换方向
57 | updateDirection (state, direction) {
58 | state.direction = direction
59 | }
60 | },
61 | // App.vue
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | ```
73 | ### 页面标题
74 | 在vue-router页面配置中添加meta的title信息,配合`vue-router`的`beforeEach`注册一个前置守卫用户获取到页面配置的title
75 | ```js
76 | // get-page-title.js
77 | import defaultSettings from '@/settings'
78 |
79 | const title = defaultSettings.title || 'H5Vue'
80 |
81 | export default function getPageTitle (pageTitle) {
82 | if (pageTitle) {
83 | return `${pageTitle} - ${title}`
84 | }
85 | return `${title}`
86 | }
87 | // permission.js
88 | router.beforeEach((to, from, next) => {
89 | // set page title
90 | document.title = getPageTitle(to.meta.title)
91 | }
92 | ```
93 | ### 自动注册
94 |
95 | 先来了解一下`require.context()`:
96 |
97 | > 你可以通过 `require.context()` 函数来创建自己的 context。
98 | >
99 | > 可以给这个函数传入三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。
100 | >
101 | > webpack 会在构建中解析代码中的 `require.context()` 。
102 |
103 | 上面的是官网原话,可能你跟我一样没太看懂,说白了,他可以用来导入模块。
104 |
105 | 来看一下如何使用,我的router下的文件结构是这样的:
106 |
107 | 
108 | ```js
109 | // 利用require.context()自动引入article.js和user.js
110 | const routerContext = require.context('./', true, /\.js$/)
111 | routerContext.keys().forEach(route => {
112 | // 如果是根目录的 index.js 、不处理
113 | if (route.startsWith('./index')) {
114 | return
115 | }
116 | const routerModule = routerContext(route)
117 | /**
118 | * 兼容 import export 和 require module.export 两种规范
119 | */
120 | routes = routes.concat(routerModule.default || routerModule)
121 | })
122 | ```
123 | 需要额外注意的是,404页面需要在自动引入后向路由数组concat上去,否则会提前匹配到404页面。
124 |
125 | 对于vuex也同样引入,记得把引入的vuex按照文件名注册为对应的模块中。
126 |
127 | ### mock server
128 |
129 | Mock server部分可直接参看[vue-element-admin](https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/essentials/mock-api.html)的mock方案
130 |
131 | ### axios封装
132 |
133 | axios部分,配置了baseUrl、超时时间,利用拦截器对header添加了用户的Token,方便下一步的用户鉴权,并对错误做了Toast提示。具体错误的code需要视各业务而定,本项目只做为示例参考。
134 |
135 | ### 用户鉴权
136 | 在`vue-router`的`beforeEach`中,添加用户鉴权功能。当用户登录后使用cookie持续化保存用户token,并赋值到vuex,后续可利用token获取用户信息。具体代码如下图:
137 | 
138 | ### vuex-loading
139 | 在vuex3.1.0中对[vuex.subscribeAction](https://vuex.vuejs.org/zh/api/#subscribe)做了改动,使其拥有了before/after钩子函数。
140 | ```js
141 | // subscribeAction官网示例
142 | store.subscribeAction({
143 | before: (action, state) => {
144 | console.log(`before action ${action.type}`)
145 | },
146 | after: (action, state) => {
147 | console.log(`after action ${action.type}`)
148 | }
149 | })
150 | ```
151 | 有了它,配合`vuex`的[插件](https://vuex.vuejs.org/zh/guide/plugins.html)功能,实现对应action的状态监听也不再是难题。
152 |
153 | [点击查看具体实现代码](https://github.com/Ljhhhhhh/h5vue/blob/master/src/utils/vuex-loading.js)
154 |
155 | > 参照自[vue 在移动端体验上的优化解决方案](https://juejin.im/post/5cdd2457f265da034e7eb2f9#heading-2)
156 |
157 | ```vue
158 | // 使用方法
159 | computed: {
160 | ...mapState({
161 | loading: state => state['@@loading'].effects['test/onePlusAction']
162 | })
163 | }
164 | // 其中 test对应的是vuex中的模块名,onePlusAction对应模块内的actions
165 | ```
166 | 具体效果:

167 | ### 列表页(vo-pages的使用)
168 |
169 | 列表页这里,使用了本人自己写的组件`vo-pages`,详细使用可查看[一款易用、高可定制的vue翻页组件](https://juejin.im/post/5d81da4551882556ba55e50e)
170 |
171 | 实现效果:
172 | 
173 |
174 | ### 生产环境优化
175 | 上线前,得优化一下资源了,该项目做了如下几步操作
176 | 1. 通用库改用CDN
177 | 2. 关闭sourcemap防止源码泄露
178 | 3. 丑化html/css/js
179 | 4. 生成gzip
180 | 5. 移除掉debugger/console
181 | 6. 利用webpack-bundle-analyzer做资源分析,提供进一步优化的数据分析
182 | 想对性能、资源了解更多的,推荐[Vue SPA 项目webpack打包优化指南](https://juejin.im/post/5bd2b60e6fb9a05d27794c5e)这篇文章。
183 |
184 | ### 更多
185 | 花了不少时间开发了这个项目,希望能提高您的H5开发效率。也欢迎大家跟我一起交流学习。
186 |
187 | ### 相关链接
188 | * [源码地址](https://github.com/Ljhhhhhh/h5vue)
189 | * [在线预览](http://h5vue.cixi518.com)
190 |
191 | ### 文章参考
192 | * [基于vue-cli3.0构建功能完善的移动端架子](https://juejin.im/post/5cbf32bc6fb9a03236393379)
193 | * [Vue SPA 项目webpack打包优化指南](https://juejin.im/post/5bd2b60e6fb9a05d27794c5e)
194 | * [vue 在移动端体验上的优化解决方案](https://juejin.im/post/5cdd2457f265da034e7eb2f9)
195 | * [vue-element-admin](https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/)
196 | * [手摸手,带你优雅的使用 icon](https://juejin.im/post/59bb864b5188257e7a427c09)
197 | * [一款易用、高可定制的vue翻页组件](https://juejin.im/post/5d81da4551882556ba55e50e)
198 |
199 |
200 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ],
5 | plugins: [
6 | ['import', {
7 | libraryName: 'vant',
8 | libraryDirectory: 'es',
9 | style: true
10 | }, 'vant']
11 | ]
12 | }
13 | /*
14 | 'postcss-pxtorem': {
15 | rootValue: 37.5,
16 | propList: ['*']
17 | } */
18 |
--------------------------------------------------------------------------------
/mock/article.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 |
3 | const List = []
4 | const count = 20
5 |
6 | const baseContent = '
I am testing data, I am testing data.

'
7 | const imageUri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
8 |
9 | for (let i = 0; i < count; i++) {
10 | List.push(Mock.mock({
11 | id: '@increment',
12 | timestamp: +Mock.Random.date('T'),
13 | author: '@first',
14 | reviewer: '@first',
15 | title: '@title(5, 10)',
16 | content_short: 'mock data',
17 | content: baseContent,
18 | forecast: '@float(0, 100, 2, 2)',
19 | importance: '@integer(1, 3)',
20 | 'type|1': ['CN', 'US', 'JP', 'EU'],
21 | 'status|1': ['published', 'draft', 'deleted'],
22 | display_time: '@datetime',
23 | comment_disabled: true,
24 | pageviews: '@integer(300, 5000)',
25 | imageUri,
26 | platforms: ['a-platform']
27 | }))
28 | }
29 |
30 | export default [
31 | {
32 | url: '/article/list',
33 | type: 'post',
34 | response: config => {
35 | const { importance, type, title, page = 1, pageSize = 20, sort } = config.body
36 |
37 | let mockList = List.filter(item => {
38 | if (importance && item.importance !== +importance) return false
39 | if (type && item.type !== type) return false
40 | if (title && item.title.indexOf(title) < 0) return false
41 | return true
42 | })
43 |
44 | if (sort === '-id') {
45 | mockList = mockList.reverse()
46 | }
47 |
48 | const pageList = mockList.filter((item, index) => index < pageSize * page && index >= pageSize * (page - 1))
49 |
50 | return {
51 | code: 200,
52 | data: {
53 | total: mockList.length,
54 | items: pageList
55 | }
56 | }
57 | }
58 | },
59 |
60 | {
61 | url: '/article/detail',
62 | type: 'get',
63 | response: config => {
64 | const { id } = config.query
65 | for (const article of List) {
66 | if (article.id === +id) {
67 | return {
68 | code: 200,
69 | data: article
70 | }
71 | }
72 | }
73 | }
74 | },
75 |
76 | {
77 | url: '/article/pv',
78 | type: 'get',
79 | response: _ => {
80 | return {
81 | code: 200,
82 | data: {
83 | pvData: [
84 | { key: 'PC', pv: 1024 },
85 | { key: 'mobile', pv: 1024 },
86 | { key: 'ios', pv: 1024 },
87 | { key: 'android', pv: 1024 }
88 | ]
89 | }
90 | }
91 | }
92 | },
93 |
94 | {
95 | url: '/article/create',
96 | type: 'post',
97 | response: _ => {
98 | return {
99 | code: 200,
100 | data: 'success'
101 | }
102 | }
103 | },
104 |
105 | {
106 | url: '/article/update',
107 | type: 'post',
108 | response: _ => {
109 | return {
110 | code: 200,
111 | data: 'success'
112 | }
113 | }
114 | }
115 | ]
116 |
--------------------------------------------------------------------------------
/mock/index.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 | import { param2Obj } from '../src/utils'
3 |
4 | import user from './user'
5 | import role from './role'
6 | import article from './article'
7 | import search from './remote-search'
8 |
9 | const mocks = [
10 | ...user,
11 | ...role,
12 | ...article,
13 | ...search
14 | ]
15 |
16 | // for front mock
17 | // please use it cautiously, it will redefine XMLHttpRequest,
18 | // which will cause many of your third-party libraries to be invalidated(like progress event).
19 | export function mockXHR () {
20 | // mock patch
21 | // https://github.com/nuysoft/Mock/issues/300
22 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
23 | Mock.XHR.prototype.send = function () {
24 | if (this.custom.xhr) {
25 | this.custom.xhr.withCredentials = this.withCredentials || false
26 |
27 | if (this.responseType) {
28 | this.custom.xhr.responseType = this.responseType
29 | }
30 | }
31 | this.proxy_send(...arguments)
32 | }
33 |
34 | function XHR2ExpressReqWrap (respond) {
35 | return function (options) {
36 | let result = null
37 | if (respond instanceof Function) {
38 | const { body, type, url } = options
39 | // https://expressjs.com/en/4x/api.html#req
40 | result = respond({
41 | method: type,
42 | body: JSON.parse(body),
43 | query: param2Obj(url)
44 | })
45 | } else {
46 | result = respond
47 | }
48 | return Mock.mock(result)
49 | }
50 | }
51 |
52 | for (const i of mocks) {
53 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
54 | }
55 | }
56 |
57 | // for mock server
58 | const responseFake = (url, type, respond) => {
59 | return {
60 | url: new RegExp(`/mock${url}`),
61 | type: type || 'get',
62 | response (req, res) {
63 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
64 | }
65 | }
66 | }
67 |
68 | const d = mocks.map(route => {
69 | return responseFake(route.url, route.type, route.response)
70 | })
71 | export default d
72 |
--------------------------------------------------------------------------------
/mock/mock-server.js:
--------------------------------------------------------------------------------
1 | const chokidar = require('chokidar')
2 | const bodyParser = require('body-parser')
3 | const chalk = require('chalk')
4 | const path = require('path')
5 |
6 | const mockDir = path.join(process.cwd(), 'mock')
7 |
8 | function registerRoutes (app) {
9 | let mockLastIndex
10 | const { default: mocks } = require('./index.js')
11 | for (const mock of mocks) {
12 | app[mock.type](mock.url, mock.response)
13 | mockLastIndex = app._router.stack.length
14 | }
15 | const mockRoutesLength = Object.keys(mocks).length
16 | return {
17 | mockRoutesLength: mockRoutesLength,
18 | mockStartIndex: mockLastIndex - mockRoutesLength
19 | }
20 | }
21 |
22 | function unregisterRoutes () {
23 | Object.keys(require.cache).forEach(i => {
24 | if (i.includes(mockDir)) {
25 | delete require.cache[require.resolve(i)]
26 | }
27 | })
28 | }
29 |
30 | module.exports = app => {
31 | // es6 polyfill
32 | require('@babel/register')
33 |
34 | // parse app.body
35 | // https://expressjs.com/en/4x/api.html#req.body
36 | app.use(bodyParser.json())
37 | app.use(bodyParser.urlencoded({
38 | extended: true
39 | }))
40 |
41 | const mockRoutes = registerRoutes(app)
42 | var mockRoutesLength = mockRoutes.mockRoutesLength
43 | var mockStartIndex = mockRoutes.mockStartIndex
44 |
45 | // watch files, hot reload mock server
46 | chokidar.watch(mockDir, {
47 | ignored: /mock-server/,
48 | ignoreInitial: true
49 | }).on('all', (event, path) => {
50 | if (event === 'change' || event === 'add') {
51 | try {
52 | // remove mock routes stack
53 | app._router.stack.splice(mockStartIndex, mockRoutesLength)
54 |
55 | // clear routes cache
56 | unregisterRoutes()
57 |
58 | const mockRoutes = registerRoutes(app)
59 | mockRoutesLength = mockRoutes.mockRoutesLength
60 | mockStartIndex = mockRoutes.mockStartIndex
61 |
62 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
63 | } catch (error) {
64 | console.log(chalk.redBright(error))
65 | }
66 | }
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/mock/remote-search.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 |
3 | const NameList = []
4 | const count = 100
5 |
6 | for (let i = 0; i < count; i++) {
7 | NameList.push(Mock.mock({
8 | name: '@first'
9 | }))
10 | }
11 | NameList.push({ name: 'mock-Pan' })
12 |
13 | export default [
14 | // username search
15 | {
16 | url: '/search/user',
17 | type: 'get',
18 | response: config => {
19 | const { name } = config.query
20 | const mockNameList = NameList.filter(item => {
21 | const lowerCaseName = item.name.toLowerCase()
22 | return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0)
23 | })
24 | return {
25 | code: 200,
26 | data: { items: mockNameList }
27 | }
28 | }
29 | },
30 |
31 | // transaction list
32 | {
33 | url: '/transaction/list',
34 | type: 'get',
35 | response: _ => {
36 | return {
37 | code: 200,
38 | data: {
39 | total: 20,
40 | 'items|20': [{
41 | order_no: '@guid()',
42 | timestamp: +Mock.Random.date('T'),
43 | username: '@name()',
44 | price: '@float(1000, 15000, 0, 2)',
45 | 'status|1': ['success', 'pending']
46 | }]
47 | }
48 | }
49 | }
50 | }
51 | ]
52 |
--------------------------------------------------------------------------------
/mock/role/index.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 | import { deepClone } from '../../src/utils/index.js'
3 | import { asyncRoutes, constantRoutes } from './routes.js'
4 |
5 | const routes = deepClone([...constantRoutes, ...asyncRoutes])
6 |
7 | const roles = [
8 | {
9 | key: 'admin',
10 | name: 'admin',
11 | description: 'Super Administrator. Have access to view all pages.',
12 | routes: routes
13 | },
14 | {
15 | key: 'editor',
16 | name: 'editor',
17 | description: 'Normal Editor. Can see all pages except permission page',
18 | routes: routes.filter(i => i.path !== '/permission')// just a mock
19 | },
20 | {
21 | key: 'visitor',
22 | name: 'visitor',
23 | description: 'Just a visitor. Can only see the home page and the document page',
24 | routes: [{
25 | path: '',
26 | redirect: 'dashboard',
27 | children: [
28 | {
29 | path: 'dashboard',
30 | name: 'Dashboard',
31 | meta: { title: 'dashboard', icon: 'dashboard' }
32 | }
33 | ]
34 | }]
35 | }
36 | ]
37 |
38 | export default [
39 | // mock get all routes form server
40 | {
41 | url: '/routes',
42 | type: 'get',
43 | response: _ => {
44 | return {
45 | code: 200,
46 | data: routes
47 | }
48 | }
49 | },
50 |
51 | // mock get all roles form server
52 | {
53 | url: '/roles',
54 | type: 'get',
55 | response: _ => {
56 | return {
57 | code: 200,
58 | data: roles
59 | }
60 | }
61 | },
62 |
63 | // add role
64 | {
65 | url: '/role',
66 | type: 'post',
67 | response: {
68 | code: 200,
69 | data: {
70 | key: Mock.mock('@integer(300, 5000)')
71 | }
72 | }
73 | },
74 |
75 | // update role
76 | {
77 | url: '/role/[A-Za-z0-9]',
78 | type: 'put',
79 | response: {
80 | code: 200,
81 | data: {
82 | status: 'success'
83 | }
84 | }
85 | },
86 |
87 | // delete role
88 | {
89 | url: '/role/[A-Za-z0-9]',
90 | type: 'delete',
91 | response: {
92 | code: 200,
93 | data: {
94 | status: 'success'
95 | }
96 | }
97 | }
98 | ]
99 |
--------------------------------------------------------------------------------
/mock/role/routes.js:
--------------------------------------------------------------------------------
1 | // Just a mock data
2 |
3 | export const constantRoutes = [
4 | {
5 | path: '/redirect',
6 | component: 'layout/Layout',
7 | hidden: true,
8 | children: [
9 | {
10 | path: '/redirect/:path*',
11 | component: 'views/redirect/index'
12 | }
13 | ]
14 | },
15 | {
16 | path: '/login',
17 | component: 'views/login/index',
18 | hidden: true
19 | },
20 | {
21 | path: '/auth-redirect',
22 | component: 'views/login/auth-redirect',
23 | hidden: true
24 | },
25 | {
26 | path: '/404',
27 | component: 'views/error-page/404',
28 | hidden: true
29 | },
30 | {
31 | path: '/401',
32 | component: 'views/error-page/401',
33 | hidden: true
34 | },
35 | {
36 | path: '',
37 | component: 'layout/Layout',
38 | redirect: 'dashboard',
39 | children: [
40 | {
41 | path: 'dashboard',
42 | component: 'views/dashboard/index',
43 | name: 'Dashboard',
44 | meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
45 | }
46 | ]
47 | },
48 | {
49 | path: '/documentation',
50 | component: 'layout/Layout',
51 | children: [
52 | {
53 | path: 'index',
54 | component: 'views/documentation/index',
55 | name: 'Documentation',
56 | meta: { title: 'Documentation', icon: 'documentation', affix: true }
57 | }
58 | ]
59 | },
60 | {
61 | path: '/guide',
62 | component: 'layout/Layout',
63 | redirect: '/guide/index',
64 | children: [
65 | {
66 | path: 'index',
67 | component: 'views/guide/index',
68 | name: 'Guide',
69 | meta: { title: 'Guide', icon: 'guide', noCache: true }
70 | }
71 | ]
72 | }
73 | ]
74 |
75 | export const asyncRoutes = [
76 | {
77 | path: '/permission',
78 | component: 'layout/Layout',
79 | redirect: '/permission/index',
80 | alwaysShow: true,
81 | meta: {
82 | title: 'Permission',
83 | icon: 'lock',
84 | roles: ['admin', 'editor']
85 | },
86 | children: [
87 | {
88 | path: 'page',
89 | component: 'views/permission/page',
90 | name: 'PagePermission',
91 | meta: {
92 | title: 'Page Permission',
93 | roles: ['admin']
94 | }
95 | },
96 | {
97 | path: 'directive',
98 | component: 'views/permission/directive',
99 | name: 'DirectivePermission',
100 | meta: {
101 | title: 'Directive Permission'
102 | }
103 | },
104 | {
105 | path: 'role',
106 | component: 'views/permission/role',
107 | name: 'RolePermission',
108 | meta: {
109 | title: 'Role Permission',
110 | roles: ['admin']
111 | }
112 | }
113 | ]
114 | },
115 |
116 | {
117 | path: '/icon',
118 | component: 'layout/Layout',
119 | children: [
120 | {
121 | path: 'index',
122 | component: 'views/icons/index',
123 | name: 'Icons',
124 | meta: { title: 'Icons', icon: 'icon', noCache: true }
125 | }
126 | ]
127 | },
128 |
129 | {
130 | path: '/components',
131 | component: 'layout/Layout',
132 | redirect: 'noRedirect',
133 | name: 'ComponentDemo',
134 | meta: {
135 | title: 'Components',
136 | icon: 'component'
137 | },
138 | children: [
139 | {
140 | path: 'tinymce',
141 | component: 'views/components-demo/tinymce',
142 | name: 'TinymceDemo',
143 | meta: { title: 'Tinymce' }
144 | },
145 | {
146 | path: 'markdown',
147 | component: 'views/components-demo/markdown',
148 | name: 'MarkdownDemo',
149 | meta: { title: 'Markdown' }
150 | },
151 | {
152 | path: 'json-editor',
153 | component: 'views/components-demo/json-editor',
154 | name: 'JsonEditorDemo',
155 | meta: { title: 'Json Editor' }
156 | },
157 | {
158 | path: 'split-pane',
159 | component: 'views/components-demo/split-pane',
160 | name: 'SplitpaneDemo',
161 | meta: { title: 'SplitPane' }
162 | },
163 | {
164 | path: 'avatar-upload',
165 | component: 'views/components-demo/avatar-upload',
166 | name: 'AvatarUploadDemo',
167 | meta: { title: 'Avatar Upload' }
168 | },
169 | {
170 | path: 'dropzone',
171 | component: 'views/components-demo/dropzone',
172 | name: 'DropzoneDemo',
173 | meta: { title: 'Dropzone' }
174 | },
175 | {
176 | path: 'sticky',
177 | component: 'views/components-demo/sticky',
178 | name: 'StickyDemo',
179 | meta: { title: 'Sticky' }
180 | },
181 | {
182 | path: 'count-to',
183 | component: 'views/components-demo/count-to',
184 | name: 'CountToDemo',
185 | meta: { title: 'Count To' }
186 | },
187 | {
188 | path: 'mixin',
189 | component: 'views/components-demo/mixin',
190 | name: 'ComponentMixinDemo',
191 | meta: { title: 'componentMixin' }
192 | },
193 | {
194 | path: 'back-to-top',
195 | component: 'views/components-demo/back-to-top',
196 | name: 'BackToTopDemo',
197 | meta: { title: 'Back To Top' }
198 | },
199 | {
200 | path: 'drag-dialog',
201 | component: 'views/components-demo/drag-dialog',
202 | name: 'DragDialogDemo',
203 | meta: { title: 'Drag Dialog' }
204 | },
205 | {
206 | path: 'drag-select',
207 | component: 'views/components-demo/drag-select',
208 | name: 'DragSelectDemo',
209 | meta: { title: 'Drag Select' }
210 | },
211 | {
212 | path: 'dnd-list',
213 | component: 'views/components-demo/dnd-list',
214 | name: 'DndListDemo',
215 | meta: { title: 'Dnd List' }
216 | },
217 | {
218 | path: 'drag-kanban',
219 | component: 'views/components-demo/drag-kanban',
220 | name: 'DragKanbanDemo',
221 | meta: { title: 'Drag Kanban' }
222 | }
223 | ]
224 | },
225 | {
226 | path: '/charts',
227 | component: 'layout/Layout',
228 | redirect: 'noRedirect',
229 | name: 'Charts',
230 | meta: {
231 | title: 'Charts',
232 | icon: 'chart'
233 | },
234 | children: [
235 | {
236 | path: 'keyboard',
237 | component: 'views/charts/keyboard',
238 | name: 'KeyboardChart',
239 | meta: { title: 'Keyboard Chart', noCache: true }
240 | },
241 | {
242 | path: 'line',
243 | component: 'views/charts/line',
244 | name: 'LineChart',
245 | meta: { title: 'Line Chart', noCache: true }
246 | },
247 | {
248 | path: 'mixchart',
249 | component: 'views/charts/mixChart',
250 | name: 'MixChart',
251 | meta: { title: 'Mix Chart', noCache: true }
252 | }
253 | ]
254 | },
255 | {
256 | path: '/nested',
257 | component: 'layout/Layout',
258 | redirect: '/nested/menu1/menu1-1',
259 | name: 'Nested',
260 | meta: {
261 | title: 'Nested',
262 | icon: 'nested'
263 | },
264 | children: [
265 | {
266 | path: 'menu1',
267 | component: 'views/nested/menu1/index',
268 | name: 'Menu1',
269 | meta: { title: 'Menu1' },
270 | redirect: '/nested/menu1/menu1-1',
271 | children: [
272 | {
273 | path: 'menu1-1',
274 | component: 'views/nested/menu1/menu1-1',
275 | name: 'Menu1-1',
276 | meta: { title: 'Menu1-1' }
277 | },
278 | {
279 | path: 'menu1-2',
280 | component: 'views/nested/menu1/menu1-2',
281 | name: 'Menu1-2',
282 | redirect: '/nested/menu1/menu1-2/menu1-2-1',
283 | meta: { title: 'Menu1-2' },
284 | children: [
285 | {
286 | path: 'menu1-2-1',
287 | component: 'views/nested/menu1/menu1-2/menu1-2-1',
288 | name: 'Menu1-2-1',
289 | meta: { title: 'Menu1-2-1' }
290 | },
291 | {
292 | path: 'menu1-2-2',
293 | component: 'views/nested/menu1/menu1-2/menu1-2-2',
294 | name: 'Menu1-2-2',
295 | meta: { title: 'Menu1-2-2' }
296 | }
297 | ]
298 | },
299 | {
300 | path: 'menu1-3',
301 | component: 'views/nested/menu1/menu1-3',
302 | name: 'Menu1-3',
303 | meta: { title: 'Menu1-3' }
304 | }
305 | ]
306 | },
307 | {
308 | path: 'menu2',
309 | name: 'Menu2',
310 | component: 'views/nested/menu2/index',
311 | meta: { title: 'Menu2' }
312 | }
313 | ]
314 | },
315 |
316 | {
317 | path: '/example',
318 | component: 'layout/Layout',
319 | redirect: '/example/list',
320 | name: 'Example',
321 | meta: {
322 | title: 'Example',
323 | icon: 'example'
324 | },
325 | children: [
326 | {
327 | path: 'create',
328 | component: 'views/example/create',
329 | name: 'CreateArticle',
330 | meta: { title: 'Create Article', icon: 'edit' }
331 | },
332 | {
333 | path: 'edit/:id(\\d+)',
334 | component: 'views/example/edit',
335 | name: 'EditArticle',
336 | meta: { title: 'Edit Article', noCache: true },
337 | hidden: true
338 | },
339 | {
340 | path: 'list',
341 | component: 'views/example/list',
342 | name: 'ArticleList',
343 | meta: { title: 'Article List', icon: 'list' }
344 | }
345 | ]
346 | },
347 |
348 | {
349 | path: '/tab',
350 | component: 'layout/Layout',
351 | children: [
352 | {
353 | path: 'index',
354 | component: 'views/tab/index',
355 | name: 'Tab',
356 | meta: { title: 'Tab', icon: 'tab' }
357 | }
358 | ]
359 | },
360 |
361 | {
362 | path: '/error',
363 | component: 'layout/Layout',
364 | redirect: 'noRedirect',
365 | name: 'ErrorPages',
366 | meta: {
367 | title: 'Error Pages',
368 | icon: '404'
369 | },
370 | children: [
371 | {
372 | path: '401',
373 | component: 'views/error-page/401',
374 | name: 'Page401',
375 | meta: { title: 'Page 401', noCache: true }
376 | },
377 | {
378 | path: '404',
379 | component: 'views/error-page/404',
380 | name: 'Page404',
381 | meta: { title: 'Page 404', noCache: true }
382 | }
383 | ]
384 | },
385 |
386 | {
387 | path: '/error-log',
388 | component: 'layout/Layout',
389 | redirect: 'noRedirect',
390 | children: [
391 | {
392 | path: 'log',
393 | component: 'views/error-log/index',
394 | name: 'ErrorLog',
395 | meta: { title: 'Error Log', icon: 'bug' }
396 | }
397 | ]
398 | },
399 |
400 | {
401 | path: '/excel',
402 | component: 'layout/Layout',
403 | redirect: '/excel/export-excel',
404 | name: 'Excel',
405 | meta: {
406 | title: 'Excel',
407 | icon: 'excel'
408 | },
409 | children: [
410 | {
411 | path: 'export-excel',
412 | component: 'views/excel/export-excel',
413 | name: 'ExportExcel',
414 | meta: { title: 'Export Excel' }
415 | },
416 | {
417 | path: 'export-selected-excel',
418 | component: 'views/excel/select-excel',
419 | name: 'SelectExcel',
420 | meta: { title: 'Select Excel' }
421 | },
422 | {
423 | path: 'export-merge-header',
424 | component: 'views/excel/merge-header',
425 | name: 'MergeHeader',
426 | meta: { title: 'Merge Header' }
427 | },
428 | {
429 | path: 'upload-excel',
430 | component: 'views/excel/upload-excel',
431 | name: 'UploadExcel',
432 | meta: { title: 'Upload Excel' }
433 | }
434 | ]
435 | },
436 |
437 | {
438 | path: '/zip',
439 | component: 'layout/Layout',
440 | redirect: '/zip/download',
441 | alwaysShow: true,
442 | meta: { title: 'Zip', icon: 'zip' },
443 | children: [
444 | {
445 | path: 'download',
446 | component: 'views/zip/index',
447 | name: 'ExportZip',
448 | meta: { title: 'Export Zip' }
449 | }
450 | ]
451 | },
452 |
453 | {
454 | path: '/pdf',
455 | component: 'layout/Layout',
456 | redirect: '/pdf/index',
457 | children: [
458 | {
459 | path: 'index',
460 | component: 'views/pdf/index',
461 | name: 'PDF',
462 | meta: { title: 'PDF', icon: 'pdf' }
463 | }
464 | ]
465 | },
466 | {
467 | path: '/pdf/download',
468 | component: 'views/pdf/download',
469 | hidden: true
470 | },
471 |
472 | {
473 | path: '/theme',
474 | component: 'layout/Layout',
475 | redirect: 'noRedirect',
476 | children: [
477 | {
478 | path: 'index',
479 | component: 'views/theme/index',
480 | name: 'Theme',
481 | meta: { title: 'Theme', icon: 'theme' }
482 | }
483 | ]
484 | },
485 |
486 | {
487 | path: '/clipboard',
488 | component: 'layout/Layout',
489 | redirect: 'noRedirect',
490 | children: [
491 | {
492 | path: 'index',
493 | component: 'views/clipboard/index',
494 | name: 'ClipboardDemo',
495 | meta: { title: 'Clipboard Demo', icon: 'clipboard' }
496 | }
497 | ]
498 | },
499 |
500 | {
501 | path: '/i18n',
502 | component: 'layout/Layout',
503 | children: [
504 | {
505 | path: 'index',
506 | component: 'views/i18n-demo/index',
507 | name: 'I18n',
508 | meta: { title: 'I18n', icon: 'international' }
509 | }
510 | ]
511 | },
512 |
513 | {
514 | path: 'external-link',
515 | component: 'layout/Layout',
516 | children: [
517 | {
518 | path: 'https://github.com/PanJiaChen/vue-element-admin',
519 | meta: { title: 'External Link', icon: 'link' }
520 | }
521 | ]
522 | },
523 |
524 | { path: '*', redirect: '/404', hidden: true }
525 | ]
526 |
--------------------------------------------------------------------------------
/mock/user.js:
--------------------------------------------------------------------------------
1 |
2 | const tokens = {
3 | '13216698987': 'admin-token',
4 | '123456': 'editor-token'
5 | }
6 |
7 | const users = {
8 | 'admin-token': {
9 | roles: ['admin'],
10 | introduction: 'I am a super administrator',
11 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
12 | name: 'Super Admin'
13 | },
14 | 'editor-token': {
15 | roles: ['editor'],
16 | introduction: 'I am an editor',
17 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
18 | name: 'Normal Editor'
19 | }
20 | }
21 |
22 | export default [
23 | // user login
24 | {
25 | url: '/user/login',
26 | type: 'post',
27 | response: config => {
28 | const { phoneNumber } = config.body
29 | const token = tokens[phoneNumber]
30 |
31 | // mock error
32 | if (!token) {
33 | return {
34 | code: 60204,
35 | message: 'Account and password are incorrect.'
36 | }
37 | }
38 |
39 | return {
40 | code: 200,
41 | data: token
42 | }
43 | }
44 | },
45 |
46 | // get user info
47 | {
48 | // eslint-disable-next-line
49 | url: '/user/info\.*',
50 | type: 'get',
51 | response: config => {
52 | const { token } = config.query
53 | const info = users[token]
54 |
55 | // mock error
56 | if (!info) {
57 | return {
58 | code: 50008,
59 | message: 'Login failed, unable to get user details.'
60 | }
61 | }
62 |
63 | return {
64 | code: 200,
65 | data: info
66 | }
67 | }
68 | },
69 |
70 | // user logout
71 | {
72 | url: '/user/logout',
73 | type: 'post',
74 | response: _ => {
75 | return {
76 | code: 200,
77 | data: 'success'
78 | }
79 | }
80 | }
81 | ]
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-cli-h5",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
10 | },
11 | "dependencies": {
12 | "axios": "^0.19.0",
13 | "core-js": "2.6.5",
14 | "dayjs": "^1.8.16",
15 | "js-cookie": "^2.2.1",
16 | "lib-flexible": "^0.3.2",
17 | "vant": "^2.2.5",
18 | "vo-pages": "^1.0.8",
19 | "vue": "^2.6.10",
20 | "vue-router": "^3.0.3",
21 | "vuex": "3.1.0"
22 | },
23 | "devDependencies": {
24 | "@babel/register": "^7.6.2",
25 | "@vue/cli-plugin-babel": "^3.11.0",
26 | "@vue/cli-plugin-eslint": "^3.11.0",
27 | "@vue/cli-service": "^3.11.0",
28 | "@vue/eslint-config-standard": "^4.0.0",
29 | "amfe-flexible": "^2.2.1",
30 | "babel-eslint": "^10.0.1",
31 | "babel-plugin-import": "^1.12.2",
32 | "body-parser": "^1.19.0",
33 | "chalk": "^2.4.2",
34 | "chokidar": "^3.2.1",
35 | "compression-webpack-plugin": "^3.0.0",
36 | "eslint": "^5.16.0",
37 | "eslint-plugin-vue": "^5.0.0",
38 | "glob": "^7.1.6",
39 | "happypack": "^5.0.1",
40 | "image-webpack-loader": "^6.0.0",
41 | "mini-css-extract-plugin": "^0.9.0",
42 | "mockjs": "^1.0.1-beta3",
43 | "postcss-pxtorem": "^4.0.1",
44 | "purgecss-webpack-plugin": "^2.1.2",
45 | "sass": "^1.18.0",
46 | "sass-loader": "^7.1.0",
47 | "script-ext-html-webpack-plugin": "^2.1.4",
48 | "speed-measure-webpack-plugin": "^1.3.3",
49 | "svg-sprite-loader": "^4.1.6",
50 | "svgo": "^1.3.0",
51 | "uglifyjs-webpack-plugin": "^2.2.0",
52 | "vconsole": "^3.3.4",
53 | "vue-template-compiler": "^2.6.10",
54 | "webpack-bundle-analyzer": "^3.5.2"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {
4 | overrideBrowserslist: ['Android >= 4.0', 'iOS >= 7']
5 | },
6 | 'postcss-pxtorem': {
7 | rootValue: 37.5,
8 | propList: ['*']
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ljhhhhhh/h5vue/93e7fc9aabb8cf8302688da237bfabc04dab1cda/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <% if (process.env.NODE_ENV === 'production') { %> <% for(var css of
9 | htmlWebpackPlugin.options.cdn.css) { %>
10 |
11 |
12 | <% } %> <% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
13 |
14 |
15 | <% } %> <% } %>
16 | H5Vue
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
25 |
26 |
57 |
--------------------------------------------------------------------------------
/src/api/article.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function fetchList (query) {
4 | const { pageSize = 10, page = 1, ...rest } = query || {}
5 | return request({
6 | url: '/article/list',
7 | method: 'post',
8 | data: {
9 | pageSize,
10 | page,
11 | ...rest
12 | },
13 | showLoading: false
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function login (data) {
4 | return request({
5 | url: '/user/login',
6 | method: 'post',
7 | data
8 | })
9 | }
10 |
11 | export function getInfo (token) {
12 | return request({
13 | url: '/user/info',
14 | method: 'get',
15 | params: { token }
16 | })
17 | }
18 |
19 | export function logout () {
20 | return request({
21 | url: '/user/logout',
22 | method: 'post'
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ljhhhhhh/h5vue/93e7fc9aabb8cf8302688da237bfabc04dab1cda/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/webpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ljhhhhhh/h5vue/93e7fc9aabb8cf8302688da237bfabc04dab1cda/src/assets/webpack.png
--------------------------------------------------------------------------------
/src/components/FooterTabbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 | 首页
7 | 文章
10 |
11 |
12 |
13 |
23 |
--------------------------------------------------------------------------------
/src/components/SvgIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
47 |
48 |
63 |
--------------------------------------------------------------------------------
/src/components/VerifyCodeBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 | {{codeRestTime ? `${codeRestTime}S` : '发送验证码'}}
8 |
9 |
10 |
47 |
53 |
--------------------------------------------------------------------------------
/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import SvgIcon from '@/components/SvgIcon'// svg component
3 |
4 | // register globally
5 | Vue.component('svg-icon', SvgIcon)
6 |
7 | const req = require.context('./svg', false, /\.svg$/)
8 | const requireAll = requireContext => requireContext.keys().map(requireContext)
9 | requireAll(req)
10 |
--------------------------------------------------------------------------------
/src/icons/svg/404.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svgo.yml:
--------------------------------------------------------------------------------
1 | # replace default config
2 |
3 | # multipass: true
4 | # full: true
5 |
6 | plugins:
7 |
8 | # - name
9 | #
10 | # or:
11 | # - name: false
12 | # - name: true
13 | #
14 | # or:
15 | # - name:
16 | # param1: 1
17 | # param2: 2
18 |
19 | - removeAttrs:
20 | attrs:
21 | - 'fill'
22 | - 'fill-rule'
23 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import 'lib-flexible'
3 | import App from './App.vue'
4 | import router from '@/router'
5 | import store from '@/store'
6 | import 'utils/permission'
7 | import SvgIcon from 'components/SvgIcon'
8 | import '@/icons' // icon
9 | import '@/style/common.scss'
10 | import { Lazyload } from 'vant'
11 | import defaultSettings from '@/settings'
12 |
13 | /**
14 | * If you don't want to use mock-server
15 | * you want to use MockJs for mock api
16 | * you can execute: mockXHR()
17 | *
18 | * Currently MockJs will be used in the production environment,
19 | * please remove it before going online! ! !
20 | */
21 | import { mockXHR } from '../mock'
22 |
23 | if (process.env.NODE_ENV === 'production') {
24 | mockXHR()
25 | }
26 |
27 | // options 为可选参数,无则不传
28 | Vue.use(Lazyload)
29 |
30 | Vue.component('svg-icon', SvgIcon)
31 |
32 | if (process.env.NODE_ENV === 'development' && defaultSettings.vconsole) {
33 | const VConsole = require('vconsole')
34 | // eslint-disable-next-line
35 | const my_console = new VConsole()
36 | }
37 | // var vConsole = new VConsole(option)
38 |
39 | Vue.config.productionTip = false
40 |
41 | new Vue({
42 | router,
43 | store,
44 | render: h => h(App)
45 | }).$mount('#app')
46 |
--------------------------------------------------------------------------------
/src/router/article.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | path: '/article',
4 | name: 'article',
5 | component: () => import(/* webpackChunkName: "article" */ 'views/Article.vue'),
6 | meta: {
7 | auth: true,
8 | title: '文章',
9 | keepAlive: true
10 | }
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Home from 'views/Home.vue'
4 | import store from '@/store'
5 |
6 | Vue.use(Router)
7 |
8 | let routes = [
9 | {
10 | path: '/',
11 | name: 'home',
12 | component: Home,
13 | meta: {
14 | title: '首页',
15 | keepAlive: true
16 | }
17 | },
18 | {
19 | path: '/404',
20 | name: '404',
21 | component: () => import(/* webpackChunkName: "404" */ 'views/404.vue'),
22 | meta: {
23 | title: '404',
24 | keepAlive: true
25 | }
26 | }
27 | ]
28 |
29 | const routerContext = require.context('./', true, /\.js$/)
30 | routerContext.keys().forEach(route => {
31 | // 如果是根目录的 index.js 、不处理
32 | if (route.startsWith('./index')) {
33 | return
34 | }
35 | const routerModule = routerContext(route)
36 | /**
37 | * 兼容 import export 和 require module.export 两种规范
38 | */
39 | routes = routes.concat(routerModule.default || routerModule)
40 | })
41 |
42 | routes = routes.concat({
43 | path: '*',
44 | redirect: '/404'
45 | })
46 |
47 | const createRouter = () => new Router({
48 | mode: 'history', // require service support
49 | base: process.env.BASE_URL,
50 | scrollBehavior: () => ({ y: 0 }),
51 | routes
52 | })
53 |
54 | const myRouter = createRouter()
55 |
56 | // const myRouter = new Router({
57 | // mode: 'history',
58 | // base: process.env.BASE_URL,
59 | // routes
60 | // })
61 |
62 | const history = window.sessionStorage
63 | history.clear()
64 | let historyCount = history.getItem('count') * 1 || 0
65 | history.setItem('/', 0)
66 |
67 | myRouter.beforeEach((to, from, next) => {
68 | if (to.params.direction) {
69 | store.commit('updateDirection', to.params.direction)
70 | } else {
71 | const toIndex = history.getItem(to.path)
72 | const fromIndex = history.getItem(from.path)
73 | // 判断并记录跳转页面是否访问过,以此判断跳转过渡方式
74 | if (toIndex) {
75 | if (!fromIndex || parseInt(toIndex, 10) > parseInt(fromIndex, 10) || (toIndex === '0' && fromIndex === '0')) {
76 | store.commit('updateDirection', 'forward')
77 | } else {
78 | store.commit('updateDirection', 'back')
79 | }
80 | } else {
81 | ++historyCount
82 | history.setItem('count', historyCount)
83 | to.path !== '/' && history.setItem(to.path, historyCount)
84 | store.commit('updateDirection', 'forward')
85 | }
86 | }
87 | next()
88 | })
89 |
90 | export function resetRouter () {
91 | myRouter.replace('/login')
92 | }
93 |
94 | export default myRouter
95 |
--------------------------------------------------------------------------------
/src/router/user.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | path: '/login',
4 | name: 'login',
5 | component: () => import(/* webpackChunkName: "login" */ 'views/user/Login.vue'),
6 | meta: {
7 | title: '登录'
8 | // auth: true,
9 | // keepAlive: true
10 | }
11 | },
12 | {
13 | path: '/register',
14 | name: 'register',
15 | component: () => import(/* webpackChunkName: "register" */ 'views/user/Register.vue'),
16 | meta: {
17 | title: '注册'
18 | // auth: true,
19 | // keepAlive: true
20 | }
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | title: 'H5Vue',
4 |
5 | /**
6 | * @type {boolean} true | false
7 | * @description Whether fix the header
8 | */
9 | fixedHeader: false,
10 | vconsole: true,
11 | needPageTrans: true,
12 |
13 | /**
14 | * @type {boolean} true | false
15 | * @description Whether show the logo in sidebar
16 | */
17 | sidebarLogo: false
18 | }
19 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import createLoadingPlugin from 'utils/vuex-loading'
4 |
5 | Vue.use(Vuex)
6 |
7 | const files = require.context('./modules', false, /\.js$/)
8 | const modules = {}
9 |
10 | files.keys().forEach(key => {
11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default
12 | })
13 |
14 | export default new Vuex.Store({
15 | plugins: [createLoadingPlugin()],
16 | state: {
17 | direction: 'forward' // 页面切换方向
18 | },
19 | getters: {
20 | userData (state, getters) {
21 | return state.user.user
22 | // return getters['user/user']
23 | }
24 | // vuex 全局getters引入局部
25 | // token () {
26 | // return store.getters['user/token']
27 | // }
28 | },
29 | mutations: {
30 | // 更新页面切换方向
31 | updateDirection (state, direction) {
32 | state.direction = direction
33 | }
34 | },
35 | actions: {
36 |
37 | },
38 | modules
39 | })
40 |
--------------------------------------------------------------------------------
/src/store/modules/test.js:
--------------------------------------------------------------------------------
1 | const state = {
2 | number: 1
3 | }
4 | const actions = {
5 | onePlusAsync: ({ commit }, { val }) => {
6 | // commit('setLoading', true, { root: true }) // 调用全局vuex的setLoading方法
7 | // 需要使用promise用来配合loading
8 | return new Promise((resolve, reject) => {
9 | setTimeout(() => {
10 | commit('onePlus', val)
11 | resolve()
12 | // commit('setLoading', false, { root: true })
13 | }, 1500)
14 | })
15 | }
16 | }
17 | const mutations = {
18 | onePlus (state, val = 1) {
19 | state.number = state.number + val
20 | }
21 | }
22 | const getters = {
23 |
24 | }
25 | export default {
26 | namespaced: true,
27 | state,
28 | actions,
29 | mutations,
30 | getters
31 | }
32 |
--------------------------------------------------------------------------------
/src/store/modules/user.js:
--------------------------------------------------------------------------------
1 | import { login, getInfo } from 'api/user'
2 | import { Toast } from 'vant'
3 | import { getToken, setToken, removeToken } from '@/utils/auth'
4 | import { resetRouter } from '@/router'
5 | // import router from '@/router'
6 |
7 | const LOGIN = 'LOGIN'// 获取用户信息
8 | const SetUserData = 'SetUserData'// 获取用户信息
9 | const LOGOUT = 'LOGOUT'// 退出登录、清除用户数据
10 | const USER_DATA = 'userDate'// 用户数据
11 |
12 | export default {
13 | namespaced: true,
14 | state: {
15 | token: getToken() || '',
16 | user: JSON.parse(localStorage.getItem(USER_DATA) || null)
17 | },
18 | mutations: {
19 |
20 | [LOGIN] (state, data) {
21 | let userToken = data.data
22 | state.token = userToken
23 | setToken(userToken)
24 | },
25 |
26 | [SetUserData] (state, userData = {}) {
27 | state.user = userData
28 | localStorage.setItem(USER_DATA, JSON.stringify(userData))
29 | },
30 | [LOGOUT] (state) {
31 | state.user = null
32 | state.token = null
33 | removeToken()
34 | localStorage.removeItem(USER_DATA)
35 | resetRouter()
36 | }
37 |
38 | },
39 | actions: {
40 | async login (state, data) {
41 | try {
42 | let res = await login({
43 | phoneNumber: data.phoneNumber,
44 | password: data.password
45 | })
46 | state.commit(LOGIN, res)
47 | Toast({
48 | message: '登录成功',
49 | position: 'middle',
50 | duration: 1500
51 | })
52 | setTimeout(() => {
53 | const redirect = data.$route.query.redirect || '/'
54 | data.$router.replace({
55 | path: redirect
56 | })
57 | }, 1500)
58 | } catch (error) {
59 | }
60 | },
61 | // get user info
62 | getInfo ({ commit, state }) {
63 | return new Promise((resolve, reject) => {
64 | getInfo(state.token).then(response => {
65 | const { data } = response
66 |
67 | if (!data) {
68 | // eslint-disable-next-line
69 | reject('Verification failed, please Login again.')
70 | }
71 | commit(SetUserData, data)
72 | resolve(data)
73 | }).catch(error => {
74 | reject(error)
75 | })
76 | })
77 | }
78 | },
79 | getters: {
80 | token (state) {
81 | return state.token
82 | },
83 | user (state) {
84 | return state.user
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/style/_mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin center($width: null, $height: null) {
2 | position: absolute;
3 | top: 50%;
4 | left: 50%;
5 |
6 | @if not $width and not $height {
7 | transform: translate(-50%, -50%);
8 | }
9 |
10 | @else if $width and $height {
11 | width: $width;
12 | height: $height;
13 | margin: -($height / 2) #{0 0} -($width / 2);
14 | }
15 |
16 | @else if not $height {
17 | width: $width;
18 | margin-left: -($width / 2);
19 | transform: translateY(-50%);
20 | }
21 |
22 | @else {
23 | height: $height;
24 | margin-top: -($height / 2);
25 | transform: translateX(-50%);
26 | }
27 | }
28 |
29 |
30 |
31 | @mixin opacity($opacity) {
32 | opacity: $opacity;
33 | $opacity-ie: $opacity * 100;
34 | filter: alpha(opacity=$opacity-ie);
35 | }
36 |
37 | @mixin ell() {
38 | //
39 | overflow: hidden;
40 | -ms-text-overflow: ellipsis;
41 | text-overflow: ellipsis;
42 | white-space: nowrap;
43 | }
44 |
45 | //多行超出省略号
46 | @mixin ell2() {
47 | word-break: break-all;
48 | text-overflow: ellipsis;
49 | display: -webkit-box;
50 | -webkit-box-orient: vertical;
51 | -webkit-line-clamp: 2;
52 | overflow: hidden;
53 | }
54 |
55 | //.arrow{
56 | // @include arrow(bottom,10px,#F00);
57 | //
58 | @mixin arrow($direction, $size, $color) {
59 | width: 0;
60 | height: 0;
61 | line-height: 0;
62 | font-size: 0;
63 | overflow: hidden;
64 | border-width: $size;
65 | cursor: pointer;
66 |
67 | @if $direction==top {
68 | border-style: dashed dashed solid dashed;
69 | border-color: transparent transparent $color transparent;
70 | border-top: none;
71 | }
72 |
73 | @else if $direction==bottom {
74 | border-style: solid dashed dashed dashed;
75 | border-color: $color transparent transparent transparent;
76 | border-bottom: none;
77 | }
78 |
79 | @else if $direction==right {
80 | border-style: dashed dashed dashed solid;
81 | border-color: transparent transparent transparent $color;
82 | border-right: none;
83 | }
84 |
85 | @else if $direction==left {
86 | border-style: dashed solid dashed dashed;
87 | border-color: transparent $color transparent transparent;
88 | border-left: none;
89 | }
90 | }
91 |
92 | // clearfix
93 | @mixin clr {
94 | &:after {
95 | clear: both;
96 | content: '.';
97 | display: block;
98 | height: 0;
99 | line-height: 0;
100 | overflow: hidden;
101 | }
102 |
103 | *height: 1%;
104 | }
105 |
106 | /*渐变(从上到下)*/
107 | @mixin linear-gradient($direction:bottom, $color1:transparent, $color2:#306eff, $color3:transparent) {
108 | //background: -webkit-linear-gradient($direction,$colorTop, $colorCenter, $colorBottom); /* Safari 5.1 - 6.0 */
109 | background: -o-linear-gradient($direction, $color1, $color2, $color3);
110 | /* Opera 11.1 - 12.0 */
111 | background: -moz-linear-gradient($direction, $color1, $color2, $color3);
112 | /* Firefox 3.6 - 15 */
113 | background: linear-gradient(to $direction, $color1, $color2, $color3);
114 | /* 标准的语法 */
115 |
116 | }
117 |
118 | /* 行高 */
119 | @mixin line-height($height:30px, $line-height:30px) {
120 | @if ($height !=null) {
121 | height: $height;
122 | }
123 |
124 | @if ($line-height !=null) {
125 | line-height: $line-height;
126 | }
127 | }
128 |
129 | /* 定义滚动条样式 圆角和阴影不需要则传入null */
130 | @mixin scrollBar($width:10px, $height:10px, $outColor:$bgColor, $innerColor:$bgGrey, $radius:5px, $shadow:null) {
131 |
132 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
133 | &::-webkit-scrollbar {
134 | width: $width;
135 | height: $height;
136 | background-color: $outColor;
137 | }
138 |
139 | /*定义滚动条轨道 内阴影+圆角*/
140 | &::-webkit-scrollbar-track {
141 | @if ($shadow !=null) {
142 | -webkit-box-shadow: $shadow;
143 | }
144 |
145 | @if ($radius !=null) {
146 | border-radius: $radius;
147 | }
148 |
149 | background-color: $outColor;
150 | }
151 |
152 | /*定义滑块 内阴影+圆角*/
153 | &::-webkit-scrollbar-thumb {
154 | @if ($shadow !=null) {
155 | -webkit-box-shadow: $shadow;
156 | }
157 |
158 | @if ($radius !=null) {
159 | border-radius: $radius;
160 | }
161 |
162 | background-color: $innerColor;
163 | border: 1px solid $innerColor;
164 | }
165 | }
166 |
167 | /* css3动画 默认3s宽度到200px */
168 | @mixin animation($from:(width:0px), $to:(width:200px), $name:mymove, $animate:mymove 2s 1 linear infinite) {
169 | -webkit-animation: $animate;
170 | -o-animation: $animate;
171 | animation: $animate;
172 |
173 | @keyframes #{$name} {
174 | from {
175 |
176 | @each $key,
177 | $value in $from {
178 | #{$key}: #{$value};
179 | }
180 | }
181 |
182 | to {
183 |
184 | @each $key,
185 | $value in $to {
186 | #{$key}: #{$value};
187 | }
188 | }
189 | }
190 |
191 | @-webkit-keyframes #{$name} {
192 | from {
193 |
194 | @each $key,
195 | $value in $from {
196 | $key: $value;
197 | }
198 | }
199 |
200 | to {
201 |
202 | @each $key,
203 | $value in $to {
204 | $key: $value;
205 | }
206 | }
207 | }
208 | }
--------------------------------------------------------------------------------
/src/style/_variables.scss:
--------------------------------------------------------------------------------
1 | $tabbarHeight: 50px;
2 |
--------------------------------------------------------------------------------
/src/style/common.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | width: 100%;
4 | height: 100%;
5 | overflow-x: hidden;
6 | overflow-y: auto;
7 | -webkit-overflow-scrolling: touch;
8 | }
9 |
10 | .clearfix:after {
11 | display: block;
12 | clear: both;
13 | content: '';
14 | visibility: hidden;
15 | height: 0;
16 | }
17 | input,
18 | button,
19 | select,
20 | textarea {
21 | outline: none;
22 | }
23 |
24 | /* page change */
25 | $--transition-time: 300ms;
26 | .back-enter-active,
27 | .back-leave-active,
28 | .forward-enter-active,
29 | .forward-leave-active {
30 | will-change: transform;
31 | transition: transform $--transition-time;
32 | position: absolute;
33 | height: 100%;
34 | backface-visibility: hidden;
35 | perspective: 1000;
36 | }
37 | .back-enter {
38 | opacity: 0.75;
39 | transform: translate3d(-50%, 0, 0) !important;
40 | }
41 | .back-enter-active {
42 | z-index: -1 !important;
43 | transition: transform $--transition-time;
44 | }
45 | .back-leave-active {
46 | transform: translate3d(100%, 0, 0) !important;
47 | transition: transform $--transition-time;
48 | }
49 | .forward-enter {
50 | transform: translate3d(100%, 0, 0) !important;
51 | }
52 | .forward-enter-active {
53 | transition: transform $--transition-time;
54 | }
55 | .forward-leave-active {
56 | z-index: -1;
57 | opacity: 0.65;
58 | transform: translate3d(-50%, 0, 0) !important;
59 | transition: transform $--transition-time;
60 | }
61 |
62 | .aabbccddee {
63 | display: flex;
64 | width: 200px;
65 | height: 200px;
66 | background: red;
67 | }
--------------------------------------------------------------------------------
/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const TokenKey = 'h5-vue-cli_token'
4 |
5 | export function getToken () {
6 | return Cookies.get(TokenKey)
7 | }
8 |
9 | export function setToken (token) {
10 | return Cookies.set(TokenKey, token)
11 | }
12 |
13 | export function removeToken () {
14 | return Cookies.remove(TokenKey)
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const title = defaultSettings.title || 'H5Vue'
4 |
5 | export default function getPageTitle (pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`
8 | }
9 | return `${title}`
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string} url
3 | * @returns {Object}
4 | */
5 | export function param2Obj (url) {
6 | const search = url.split('?')[1]
7 | if (!search) {
8 | return {}
9 | }
10 | return JSON.parse(
11 | '{"' +
12 | decodeURIComponent(search)
13 | .replace(/"/g, '\\"')
14 | .replace(/&/g, '","')
15 | .replace(/=/g, '":"')
16 | .replace(/\+/g, ' ') +
17 | '"}'
18 | )
19 | }
20 |
21 | export function deepClone (source) {
22 | if (!source && typeof source !== 'object') {
23 | throw new Error('error arguments', 'deepClone')
24 | }
25 | const targetObj = source.constructor === Array ? [] : {}
26 | Object.keys(source).forEach(keys => {
27 | if (source[keys] && typeof source[keys] === 'object') {
28 | targetObj[keys] = deepClone(source[keys])
29 | } else {
30 | targetObj[keys] = source[keys]
31 | }
32 | })
33 | return targetObj
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/permission.js:
--------------------------------------------------------------------------------
1 | import router from '@/router'
2 | import store from '@/store'
3 | import { Notify } from 'vant'
4 | import { getToken } from '@/utils/auth' // get token from cookie
5 | import getPageTitle from '@/utils/get-page-title'
6 |
7 | const whiteList = ['/login', '/register'] // 白名单列表
8 |
9 | router.beforeEach(async (to, from, next) => {
10 | // 设置页面标题
11 | document.title = getPageTitle(to.meta.title)
12 |
13 | // determine whether the user has logged in
14 | const hasToken = getToken()
15 |
16 | if (hasToken) {
17 | if (to.path === '/login') {
18 | // 已经登录,跳转到首页
19 | next({ path: '/' })
20 | } else {
21 | // 获取用户信息
22 | const hasGetUserInfo = store.getters.userData && store.getters.userData.name
23 | if (hasGetUserInfo) {
24 | next()
25 | } else {
26 | try {
27 | // get user info
28 | await store.dispatch('user/getInfo')
29 | next()
30 | } catch (error) {
31 | // 清除用户信息,退出登录,跳转登录页
32 | store.commit('user/LOGOUT')
33 | Notify.error(error || 'Has Error')
34 | next(`/login?redirect=${to.path}`)
35 | }
36 | }
37 | }
38 | } else {
39 | /* has no token */
40 | if (whiteList.indexOf(to.path) !== -1) {
41 | // 白名单中,无需验证
42 | next()
43 | } else {
44 | // other pages that do not have permission to access are redirected to the login page.
45 | next(`/login?redirect=${to.path}`)
46 | }
47 | }
48 | })
49 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { Toast } from 'vant'
3 | import store from '@/store'
4 | import { getToken } from '@/utils/auth'
5 |
6 | // create an axios instance
7 | const service = axios.create({
8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
9 | // withCredentials: true, // send cookies when cross-domain requests
10 | timeout: 5000 // request timeout
11 | })
12 |
13 | // request interceptor
14 | service.interceptors.request.use(
15 | config => {
16 | // do something before request is sent
17 | if (store.getters.token) {
18 | config.headers['X-Token'] = getToken()
19 | }
20 | return config
21 | },
22 | error => {
23 | // do something with request error
24 | console.log(error, 'err') // for debug
25 | return Promise.reject(error)
26 | }
27 | )
28 |
29 | // response interceptor
30 | service.interceptors.response.use(
31 | /**
32 | * If you want to get http information such as headers or status
33 | * Please return response => response
34 | */
35 |
36 | /**
37 | * Determine the request status by custom code
38 | * Here is just an example
39 | * You can also judge the status by HTTP Status Code
40 | */
41 | response => {
42 | const res = response.data
43 |
44 | // if the custom code is not 20000, it is judged as an error.
45 | if (res.code !== 200) {
46 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
47 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
48 | // to re-login
49 | Toast.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
50 | confirmButtonText: 'Re-Login',
51 | cancelButtonText: 'Cancel',
52 | type: 'warning'
53 | }).then(() => {
54 | store.dispatch('user/resetToken').then(() => {
55 | location.reload()
56 | })
57 | })
58 | }
59 | return Promise.reject(new Error(res.message || 'Error'))
60 | } else {
61 | return res
62 | }
63 | },
64 | error => {
65 | console.log('err' + error) // for debug
66 | Toast.fail({
67 | message: error.message,
68 | duration: 1.5 * 1000
69 | })
70 | return Promise.reject(error)
71 | }
72 | )
73 |
74 | export default service
75 |
--------------------------------------------------------------------------------
/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * @param {string} path
7 | * @returns {Boolean}
8 | */
9 | export function isExternal (path) {
10 | return /^(https?:|mailto:|tel:)/.test(path)
11 | }
12 |
13 | /**
14 | * @param {string} str
15 | * @returns {Boolean}
16 | */
17 | export function validUsername (str) {
18 | const validMap = ['admin', 'editor']
19 | return validMap.indexOf(str.trim()) >= 0
20 | }
21 |
22 | /**
23 | * @param {string} url
24 | * @returns {Boolean}
25 | */
26 | export function validURL (url) {
27 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
28 | return reg.test(url)
29 | }
30 |
31 | /**
32 | * @param {string} str
33 | * @returns {Boolean}
34 | */
35 | export function validLowerCase (str) {
36 | const reg = /^[a-z]+$/
37 | return reg.test(str)
38 | }
39 |
40 | /**
41 | * @param {string} str
42 | * @returns {Boolean}
43 | */
44 | export function validUpperCase (str) {
45 | const reg = /^[A-Z]+$/
46 | return reg.test(str)
47 | }
48 |
49 | /**
50 | * @param {string} str
51 | * @returns {Boolean}
52 | */
53 | export function validAlphabets (str) {
54 | const reg = /^[A-Za-z]+$/
55 | return reg.test(str)
56 | }
57 |
58 | /**
59 | * @param {string} email
60 | * @returns {Boolean}
61 | */
62 | export function validEmail (email) {
63 | // eslint-disable-next-line
64 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
65 | return reg.test(email)
66 | }
67 |
68 | /**
69 | * @param {string} str
70 | * @returns {Boolean}
71 | */
72 | export function isString (str) {
73 | if (typeof str === 'string' || str instanceof String) {
74 | return true
75 | }
76 | return false
77 | }
78 |
79 | /**
80 | * @param {Array} arg
81 | * @returns {Boolean}
82 | */
83 | export function isArray (arg) {
84 | if (typeof Array.isArray === 'undefined') {
85 | return Object.prototype.toString.call(arg) === '[object Array]'
86 | }
87 | return Array.isArray(arg)
88 | }
89 |
--------------------------------------------------------------------------------
/src/utils/vuex-loading.js:
--------------------------------------------------------------------------------
1 | // 相关文档 https://vuex.vuejs.org/zh/api/#subscribeaction
2 | const NAMESPACE = '@@loading'
3 |
4 | const createLoadingPlugin = ({
5 | namespace = NAMESPACE,
6 | includes = [],
7 | excludes = []
8 | } = {}) => {
9 | return store => {
10 | if (store.state[namespace]) {
11 | throw new Error(
12 | `createLoadingPlugin: ${namespace} exited in current store`
13 | )
14 | }
15 |
16 | store.registerModule(namespace, {
17 | namespaced: true,
18 | state: {
19 | global: false,
20 | effects: {
21 |
22 | }
23 | },
24 | mutations: {
25 | SHOW (state, { payload }) {
26 | state.global = true
27 | state.effects = {
28 | ...state.effects,
29 | [payload]: true
30 | }
31 | },
32 | HIDE (state, { payload }) {
33 | state.global = false
34 | state.effects = {
35 | ...state.effects,
36 | [payload]: false
37 | }
38 | }
39 | }
40 | })
41 |
42 | store.subscribeAction({
43 | before: action => {
44 | console.log(`before action ${action.type}`)
45 | if (shouldEffect(action, includes, excludes)) {
46 | store.commit({ type: namespace + '/SHOW', payload: action.type })
47 | }
48 | },
49 | after: action => {
50 | console.log(`after action ${action.type}`)
51 | if (shouldEffect(action, includes, excludes)) {
52 | store.commit({ type: namespace + '/HIDE', payload: action.type })
53 | }
54 | }
55 | })
56 | }
57 | }
58 |
59 | function shouldEffect ({ type }, includes, excludes) {
60 | if (includes.length === 0 && excludes.length === 0) {
61 | return true
62 | }
63 |
64 | if (includes.length > 0) {
65 | return includes.indexOf(type) > -1
66 | }
67 |
68 | return excludes.length > 0 && excludes.indexOf(type) === -1
69 | }
70 |
71 | export default createLoadingPlugin
72 | // 需要在vuex中引入并注册成插件:
73 | /**
74 | import createLoadingPlugin from 'utils/vuex-loading'
75 | export default new Vuex.Store({
76 | plugins: [createLoadingPlugin()],
77 | state: {
78 | // loading: false,
79 | direction: 'forward'
80 | }
81 | })
82 | **/
83 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 404
4 | 返回首页
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/views/Article.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 | -
12 |
13 |
![thumb]()
15 |
16 |
17 |
{{ article.title }}
18 |
19 | 作者:{{ article.author }}
20 | 发布时间:{{article.displayTimeFormart}}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
90 |
160 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
当前数值{{$store.state.test.number}}
13 |
14 | 异步+1
18 | +1
21 |
22 |
23 |
24 |
28 |
29 |
30 | 退出登录
32 |
33 | 前往404页面
34 |
35 |
![]()
36 |
37 |
38 |
39 |
40 |
104 |
148 |
--------------------------------------------------------------------------------
/src/views/user/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 登录
19 |
20 |
21 |
没有账号?去注册
22 |
{{loginWayObj.toggleMsg}}
23 |
24 |
25 |
26 |
27 |
95 |
138 |
--------------------------------------------------------------------------------
/src/views/user/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | this is register page
4 |
5 |
6 |
11 |
14 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const glob = require('glob')
3 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
4 | const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
5 | const HappyPack = require('happypack')
6 | const PurgecssPlugin = require('purgecss-webpack-plugin')
7 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
8 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
9 | const port = process.env.port || process.env.npm_config_port || 8888
10 | const cdnDomian = '/' // cdn域名,如果有cdn修改成对应的cdn
11 | const name = 'H5Vue' // page title
12 | const IS_PRODUCTION = process.env.NODE_ENV === 'production'
13 | const cdn = {
14 | css: [],
15 | js: [
16 | 'https://cdn.bootcss.com/vue/2.6.10/vue.min.js',
17 | 'https://cdn.bootcss.com/vue-router/3.0.3/vue-router.min.js',
18 | 'https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js',
19 | 'https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js',
20 | 'https://cdn.bootcss.com/js-cookie/2.2.1/js.cookie.min.js'
21 | ]
22 | }
23 |
24 | const externals = {
25 | vue: 'Vue',
26 | 'vue-router': 'VueRouter',
27 | vuex: 'Vuex',
28 | axios: 'axios',
29 | 'js-cookie': 'Cookies'
30 | }
31 |
32 | const PATHS = {
33 | src: path.join(__dirname, 'src')
34 | }
35 |
36 | // 记录打包速度
37 | const smp = new SpeedMeasurePlugin()
38 |
39 | function resolve (dir) {
40 | return path.join(__dirname, dir)
41 | }
42 |
43 | module.exports = {
44 | publicPath: IS_PRODUCTION ? cdnDomian : './',
45 | outputDir: 'dist',
46 | assetsDir: 'static',
47 | lintOnSave: process.env.NODE_ENV === 'development',
48 | productionSourceMap: false,
49 | devServer: {
50 | port: port,
51 | open: true,
52 | overlay: {
53 | warnings: false,
54 | errors: true
55 | },
56 | proxy: {
57 | // change xxx-api/login => mock/login
58 | // detail: https://cli.vuejs.org/config/#devserver-proxy
59 | [process.env.VUE_APP_BASE_API]: {
60 | target: `http://127.0.0.1:${port}/mock`,
61 | changeOrigin: true,
62 | pathRewrite: {
63 | ['^' + process.env.VUE_APP_BASE_API]: ''
64 | }
65 | }
66 | },
67 | after: require('./mock/mock-server.js')
68 | },
69 | configureWebpack: smp.wrap({
70 | // provide the app's title in webpack's name field, so that
71 | // it can be accessed in index.html to inject the correct title.
72 | name: name,
73 | resolve: {
74 | alias: {
75 | '@': resolve('src'), // 主目录
76 | views: resolve('src/views'), // 页面
77 | components: resolve('src/components'), // 组件
78 | api: resolve('src/api'), // 接口
79 | utils: resolve('src/utils'), // 通用功能
80 | assets: resolve('src/assets'), // 静态资源
81 | style: resolve('src/style') // 通用样式
82 | }
83 | }
84 | }),
85 | chainWebpack (config) {
86 | config.plugins.delete('preload') // TODO: need test
87 | config.plugins.delete('prefetch') // TODO: need test
88 |
89 | // set svg-sprite-loader
90 | config.module
91 | .rule('svg')
92 | .exclude.add(resolve('src/icons'))
93 | .end()
94 | config.module
95 | .rule('icons')
96 | .test(/\.svg$/)
97 | .include.add(resolve('src/icons'))
98 | .end()
99 | .use('svg-sprite-loader')
100 | .loader('svg-sprite-loader')
101 | .options({
102 | symbolId: 'icon-[name]'
103 | })
104 | .end()
105 |
106 | // set preserveWhitespace
107 | config.module
108 | .rule('vue')
109 | .use('vue-loader')
110 | .loader('vue-loader')
111 | .tap(options => {
112 | options.compilerOptions.preserveWhitespace = true
113 | return options
114 | })
115 | .end()
116 |
117 | // 图片压缩
118 | // config.module
119 | // .rule('images')
120 | // .use('image-webpack-loader')
121 | // .loader('image-webpack-loader')
122 | // .options({
123 | // bypassOnDebug: true,
124 | // pngquant: {
125 | // quality: [0.75, 0.90],
126 | // speed: 5
127 | // }
128 | // })
129 | // .end()
130 |
131 | config
132 | // https://webpack.js.org/configuration/devtool/#development
133 | .when(process.env.NODE_ENV === 'development', config =>
134 | config.devtool('cheap-source-map')
135 | )
136 |
137 | config.when(process.env.NODE_ENV !== 'development', config => {
138 | config
139 | .plugin('ScriptExtHtmlWebpackPlugin')
140 | .after('html')
141 | .use('script-ext-html-webpack-plugin', [
142 | {
143 | // `runtime` must same as runtimeChunk name. default is `runtime`
144 | inline: /runtime\..*\.js$/
145 | }
146 | ])
147 | .end()
148 | config.optimization.splitChunks({
149 | chunks: 'all',
150 | cacheGroups: {
151 | libs: {
152 | name: 'chunk-libs',
153 | test: /[\\/]node_modules[\\/]/,
154 | priority: 10,
155 | chunks: 'initial' // only package third parties that are initially dependent
156 | },
157 | commons: {
158 | name: 'chunk-commons',
159 | test: resolve('src/components'), // can customize your rules
160 | minChunks: 3, // minimum common number
161 | priority: 5,
162 | reuseExistingChunk: true
163 | }
164 | }
165 | })
166 | config.optimization.runtimeChunk('single')
167 | })
168 | if (IS_PRODUCTION) {
169 | config.plugin('analyzer').use(BundleAnalyzerPlugin)
170 | config.plugin('html').tap(args => {
171 | args[0].cdn = cdn
172 | return args
173 | })
174 | config.externals(externals)
175 | config.plugin('html').tap(args => {
176 | args[0].minify.minifyCSS = true // 压缩html中的css
177 | return args
178 | })
179 |
180 | // 多线程
181 | config.plugin('HappyPack').use(HappyPack, [
182 | {
183 | loaders: [
184 | {
185 | loader: 'babel-loader?cacheDirectory=true'
186 | }
187 | ]
188 | }
189 | ])
190 | // gzip需要nginx进行配合
191 | config
192 | .plugin('compression')
193 | .use(CompressionWebpackPlugin)
194 | .tap(() => [
195 | {
196 | test: /\.js$|\.html$|\.css/, // 匹配文件名
197 | threshold: 10240, // 超过10k进行压缩
198 | deleteOriginalAssets: false // 是否删除源文件
199 | }
200 | ])
201 |
202 | // css Tree Thaking TODO:: 发现build之后导致样式消失
203 | // config.plugin('purecss').use(
204 | // new PurgecssPlugin({
205 | // paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true })
206 | // })
207 | // )
208 |
209 | config.optimization.minimizer([
210 | new UglifyjsWebpackPlugin({
211 | // 生产环境推荐关闭 sourcemap 防止源码泄漏
212 | // 服务端通过前端发送的行列,根据 sourcemap 转为源文件位置
213 | // sourceMap: true,
214 | uglifyOptions: {
215 | warnings: false,
216 | compress: {
217 | drop_console: true,
218 | drop_debugger: true
219 | }
220 | }
221 | })
222 | ])
223 | }
224 | },
225 | css: {
226 | // 是否使用css分离插件 ExtractTextPlugin
227 | extract: !!IS_PRODUCTION,
228 | // 开启 CSS source maps?
229 | sourceMap: false,
230 | // css预设器配置项
231 | // 启用 CSS modules for all css / pre-processor files.
232 | modules: false,
233 | loaderOptions: {
234 | sass: {
235 | data:
236 | '@import "style/_mixin.scss";@import "style/_variables.scss";@import "style/common.scss";' // 全局引入
237 | }
238 | }
239 | }
240 | }
241 |
--------------------------------------------------------------------------------