├── .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 | ![alias配置](http://img.cixi518.com/alias.png) 27 | ### Vant/Rem适配 28 | 按照Vant官网推荐自动按需引入组件,同样,Vant官网中也有对Rem适配的推荐配置,按照官网说明的使用。需要注意的是postcss的配置中,autoprefixer下的`browsers`需要替换成`overrideBrowserslist`,否则会有报错信息。具体如图
29 | ![postcss配置](http://img.cixi518.com/postcss.png) 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 | 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 | ![router-tree](http://img.cixi518.com/router-tree.png)
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 | ![permission](http://img.cixi518.com/permission.png) 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 | 具体效果:
![loading](http://img.cixi518.com/loading.gif) 167 | ### 列表页(vo-pages的使用) 168 | 169 | 列表页这里,使用了本人自己写的组件`vo-pages`,详细使用可查看[一款易用、高可定制的vue翻页组件](https://juejin.im/post/5d81da4551882556ba55e50e) 170 | 171 | 实现效果:
172 | ![vo-pages](http://img.cixi518.com/Kapture%202019-10-27%20at%2013.36.21.gif) 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 | 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 | 13 | 23 | -------------------------------------------------------------------------------- /src/components/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /src/components/VerifyCodeBtn.vue: -------------------------------------------------------------------------------- 1 | 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 | 7 | 12 | -------------------------------------------------------------------------------- /src/views/Article.vue: -------------------------------------------------------------------------------- 1 | 30 | 90 | 160 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 104 | 148 | -------------------------------------------------------------------------------- /src/views/user/Login.vue: -------------------------------------------------------------------------------- 1 | 27 | 95 | 138 | -------------------------------------------------------------------------------- /src/views/user/Register.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------