diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..d6471a3 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,2 @@ +> 1% +last 2 versions diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7053c49 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..8f5856d --- /dev/null +++ b/.env.development @@ -0,0 +1,14 @@ +# just a flag +ENV = 'development' + +# base api +VUE_APP_BASE_API = '/dev-api' + +# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, +# to control whether the babel-plugin-dynamic-import-node plugin is enabled. +# It only does one thing by converting all import() to require(). +# This configuration can significantly increase the speed of hot updates, +# when you have a large number of pages. +# Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js + +VUE_CLI_BABEL_TRANSPILE_MODULES = true diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..80c8103 --- /dev/null +++ b/.env.production @@ -0,0 +1,6 @@ +# just a flag +ENV = 'production' + +# base api +VUE_APP_BASE_API = '/prod-api' + diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e6529fc --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +build/*.js +src/assets +public +dist diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..c18ca28 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { + node: true + }, + extends: [ + 'plugin:vue/essential', + '@vue/standard' + ], + parserOptions: { + parser: 'babel-eslint' + }, + rules: { + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + "space-before-function-paren": 0, + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..997c6eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules +/dist +package-lock.json +yarn.lock + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..232db83 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Ewall&熊猫 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..6c909a4 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,15 @@ +module.exports = { + presets: ['@vue/cli-plugin-babel/preset'], + // vant引入:https://youzan.github.io/vant/#/zh-CN/quickstart#yin-ru-zu-jian + plugins: [ + [ + 'import', + { + libraryName: 'vant', + libraryDirectory: 'es', + style: name => `${name}/style/less` + }, + 'vant' + ] + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d27b5e0 --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "name": "panda-vue-template", + "version": "0.1.0", + "author": "Ewall&熊猫", + "license": "MIT", + "keywords": [ + "vue", + "vue-template", + "panda-vue-template" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/Ewall1106/panda-vue-template" + }, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint", + "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" + }, + "dependencies": { + "axios": "^0.19.2", + "core-js": "^3.6.4", + "js-cookie": "^2.2.1", + "normalize.css": "^8.0.1", + "vant": "^2.5.5", + "vue": "^2.6.11", + "vue-router": "^3.1.5", + "vue-scrollto": "^2.17.1", + "vuex": "^3.1.2" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "~4.2.0", + "@vue/cli-plugin-eslint": "~4.2.0", + "@vue/cli-plugin-router": "~4.2.0", + "@vue/cli-plugin-vuex": "~4.2.0", + "@vue/cli-service": "~4.2.0", + "@vue/eslint-config-standard": "^5.1.0", + "babel-eslint": "^10.0.3", + "babel-plugin-import": "^1.13.0", + "eslint": "^6.7.2", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-node": "^11.0.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.0", + "eslint-plugin-vue": "^6.1.2", + "less": "^3.11.1", + "less-loader": "^5.0.0", + "node-sass": "^4.13.1", + "postcss-preset-env": "^6.7.0", + "postcss-px-to-viewport": "^1.1.1", + "sass-loader": "^8.0.2", + "svg-sprite-loader": "^4.2.1", + "svgo": "^1.3.2", + "vue-template-compiler": "^2.6.11" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..ed64934 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,23 @@ +module.exports = { + plugins: { + // https://github.com/csstools/postcss-preset-env + 'postcss-preset-env': {}, + // https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md + 'postcss-px-to-viewport': { + unitToConvert: 'px', + viewportWidth: 750, + unitPrecision: 3, + propList: ['*'], + viewportUnit: 'vw', + fontViewportUnit: 'vw', + selectorBlackList: ['.ignore', 'van'], + minPixelValue: 1, + mediaQuery: false, + replace: true, + exclude: [], + landscape: false, + landscapeUnit: 'vw', + landscapeWidth: 568 + } + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..bb172ba --- /dev/null +++ b/public/index.html @@ -0,0 +1,22 @@ + + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + +
+ + + + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..d3e2cd8 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,5 @@ + diff --git a/src/api/user.js b/src/api/user.js new file mode 100644 index 0000000..d155005 --- /dev/null +++ b/src/api/user.js @@ -0,0 +1,16 @@ +import request from '@/utils/request' + +export function getInfo() { + return request({ + url: '/user/info', + method: 'get' + }) +} + +export function login(data) { + return request({ + url: '/user/login', + method: 'post', + data + }) +} diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..89ffdd0 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/components/BackTop/index.vue b/src/components/BackTop/index.vue new file mode 100644 index 0000000..74f857f --- /dev/null +++ b/src/components/BackTop/index.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/src/components/SvgIcon/index.vue b/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..a4f0252 --- /dev/null +++ b/src/components/SvgIcon/index.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/icons/index.js b/src/icons/index.js new file mode 100644 index 0000000..28ae463 --- /dev/null +++ b/src/icons/index.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import SvgIcon from '@/components/SvgIcon' + +// register globally +Vue.component('svg-icon', SvgIcon) + +const req = require.context('./svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys().map(requireContext) +requireAll(req) diff --git a/src/icons/svg/backtop.svg b/src/icons/svg/backtop.svg new file mode 100644 index 0000000..f142331 --- /dev/null +++ b/src/icons/svg/backtop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/qq.svg b/src/icons/svg/qq.svg new file mode 100644 index 0000000..ee13d4e --- /dev/null +++ b/src/icons/svg/qq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/search.svg b/src/icons/svg/search.svg new file mode 100644 index 0000000..3110f2a --- /dev/null +++ b/src/icons/svg/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/star.svg b/src/icons/svg/star.svg new file mode 100644 index 0000000..63a68b3 --- /dev/null +++ b/src/icons/svg/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/user.svg b/src/icons/svg/user.svg new file mode 100644 index 0000000..0ba0716 --- /dev/null +++ b/src/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/wechat.svg b/src/icons/svg/wechat.svg new file mode 100644 index 0000000..c586e55 --- /dev/null +++ b/src/icons/svg/wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svgo.yml b/src/icons/svgo.yml new file mode 100644 index 0000000..d11906a --- /dev/null +++ b/src/icons/svgo.yml @@ -0,0 +1,22 @@ +# replace default config + +# multipass: true +# full: true + +plugins: + + # - name + # + # or: + # - name: false + # - name: true + # + # or: + # - name: + # param1: 1 + # param2: 2 + +- removeAttrs: + attrs: + - 'fill' + - 'fill-rule' diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..a9ef71e --- /dev/null +++ b/src/main.js @@ -0,0 +1,42 @@ +import Vue from 'vue' +// A modern alternative to CSS resets +import './styles/reset.css' +// https://github.com/necolas/normalize.css +import 'normalize.css/normalize.css' + +import App from './App.vue' +import router from './router' +import store from './store' + +import './icons' +import './permission' + +// vant +import { + Button, + Toast, + Dialog, + Field, + Form, + Notify, + Cell, + CellGroup, + NavBar +} from 'vant' +Vue.use(Toast) + .use(Dialog) + .use(Notify) + .use(Field) + .use(Form) + .use(Button) + .use(Cell) + .use(CellGroup) + .use(NavBar) + +Vue.config.productionTip = false + +new Vue({ + router, + store, + render: h => h(App) +}).$mount('#app') diff --git a/src/permission.js b/src/permission.js new file mode 100644 index 0000000..57f6d3d --- /dev/null +++ b/src/permission.js @@ -0,0 +1,53 @@ +import router from './router' +import store from './store' +import { Toast } from 'vant' +import { getToken } from '@/utils/auth' // get token from cookie + +const whiteList = ['/login'] // 白名单 + +router.beforeEach(async (to, from, next) => { + // 设置标题 + document.title = '熊猫商城' + + // 根据token判断用户是否登录 + const hasToken = getToken() + + console.log('adsfa', hasToken) + + if (hasToken) { + if (to.path === '/login') { + // 如果已经登录了,而去的又是login页就重定向到首页 + next({ path: '/' }) + } else { + const hasUserInfo = store.getters.name + console.log('asdfas', hasUserInfo) + if (hasUserInfo) { + next() + } else { + // 如果用户刷新了浏览器,那么需要重新请求基本信息塞到vuex中进行状态管理 + try { + await store.dispatch('user/getInfo') + next() + } catch (error) { + if (whiteList.indexOf(to.path) !== -1) { + next() + } else { + // 清空token重新去登录 + await store.dispatch('user/resetToken') + Toast.fail('出错了') + next(`/login?redirect=${encodeURIComponent(location.href)}`) + } + } + } + } + } else { + if (whiteList.indexOf(to.path) !== -1) { + // 白名单没有token也直接放行 + next() + } else { + // 反之则去登录页面 + next(`/login?redirect=${encodeURIComponent(location.href)}`) + // NProgress.done() + } + } +}) diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..86dab02 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,29 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' + +Vue.use(VueRouter) + +const routes = [ + { + path: '*', + name: 'Home', + component: () => import('@/views/home') + }, + { + path: '/login', + name: 'Login', + component: () => import('@/views/auth/login') + }, + { + path: '/iconfont', + name: 'IconFont', + component: () => import('@/views/about/iconfont') + } +] + +const router = new VueRouter({ + scrollBehavior: () => ({ y: 0 }), + routes +}) + +export default router diff --git a/src/store/getters.js b/src/store/getters.js new file mode 100644 index 0000000..1d8bf04 --- /dev/null +++ b/src/store/getters.js @@ -0,0 +1,7 @@ +const getters = { + avatar: state => state.user.avatar, + name: state => state.user.name, + token: state => state.user.token +} + +export default getters diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..d12f06b --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,22 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import getters from './getters' + +Vue.use(Vuex) + +// https://webpack.js.org/guides/dependency-management/#requirecontext +const modulesFiles = require.context('./modules', true, /\.js$/) + +const modules = modulesFiles.keys().reduce((modules, modulePath) => { + const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') + const value = modulesFiles(modulePath) + modules[moduleName] = value.default + return modules +}, {}) + +const store = new Vuex.Store({ + modules, + getters +}) + +export default store diff --git a/src/store/modules/user.js b/src/store/modules/user.js new file mode 100644 index 0000000..20b9456 --- /dev/null +++ b/src/store/modules/user.js @@ -0,0 +1,90 @@ +import { login, logout, getInfo } from '@/api/user' +import { getToken, setToken, removeToken } from '@/utils/auth' + +const state = { + token: getToken(), + name: '', + avatar: '' +} + +const mutations = { + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar + } +} + +const actions = { + // 登录 + login({ commit }, userInfo) { + const { phonenum, password } = userInfo + return new Promise((resolve, reject) => { + login({ phonenum, password }) + .then(res => { + const { token } = res.entry + commit('SET_TOKEN', token) + setToken(token) + resolve() + }) + .catch(error => { + reject(error) + }) + }) + }, + + // 登出 + logout({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token) + .then(() => { + commit('SET_TOKEN', '') + removeToken() + + resolve() + }) + .catch(error => { + reject(error) + }) + }) + }, + + // 获取基本用户信息 + getInfo({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo(state.token) + .then(res => { + const data = res.entry + if (!data) { + reject(new Error('获取基本信息失败,请重新登录')) + } + commit('SET_NAME', data.name) + commit('SET_AVATAR', data.avatar) + resolve(data) + }) + .catch(error => { + reject(error) + }) + }) + }, + + // 重置token + resetToken({ commit }) { + return new Promise(resolve => { + commit('SET_TOKEN', '') + removeToken() + resolve() + }) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/src/styles/reset.css b/src/styles/reset.css new file mode 100644 index 0000000..45a05ec --- /dev/null +++ b/src/styles/reset.css @@ -0,0 +1,129 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/src/utils/auth.js b/src/utils/auth.js new file mode 100644 index 0000000..b04f3de --- /dev/null +++ b/src/utils/auth.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +const key = 'MallToken' + +export function getToken() { + return Cookies.get(key) +} + +export function setToken(token) { + return Cookies.set(key, token) +} + +export function removeToken() { + return Cookies.remove(key) +} diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 0000000..4ea801e --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,60 @@ +import axios from 'axios' +import { Toast, Dialog } from 'vant' +import store from '@/store' +import { getToken } from '@/utils/auth' + +// 创建一个axios实例 +const service = axios.create({ + baseURL: process.env.VUE_APP_BASE_API, + // withCredentials: true, + timeout: 5000 +}) + +// 请求拦截器 +service.interceptors.request.use( + config => { + if (store.getters.token) { + // ['X-Token']是我这里自定义测试而塞到请求头中 + config.headers['X-TOKEN'] = getToken() + } + return config + }, + error => { + console.log(error) + return Promise.reject(error) + } +) + +// 响应拦截器 +service.interceptors.response.use( + response => { + const res = response.data + + // 与后端约定的错误码 + if (res.code !== 200) { + Toast.fail(res.message) + // 现约定 50001:无效token 50002:token过期 + if (res.code === 50001 || res.code === 50002) { + Dialog.alert({ + title: '提示', + message: '您还未登录或登录已过期,请重新登录' + }).then(() => { + store.dispatch('user/resetToken').then(() => { + location.reload() + }) + }) + } + + return Promise.reject(new Error(res.message || 'Error')) + } else { + return res + } + }, + error => { + console.log('err' + error) + Toast.fail(error.message) + return Promise.reject(error) + } +) + +export default service diff --git a/src/views/about/iconfont.vue b/src/views/about/iconfont.vue new file mode 100644 index 0000000..61f45c7 --- /dev/null +++ b/src/views/about/iconfont.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/views/auth/login.vue b/src/views/auth/login.vue new file mode 100644 index 0000000..f9bf375 --- /dev/null +++ b/src/views/auth/login.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/src/views/home/index.vue b/src/views/home/index.vue new file mode 100644 index 0000000..15c67a6 --- /dev/null +++ b/src/views/home/index.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/vue.config.js b/vue.config.js new file mode 100644 index 0000000..3f8e120 --- /dev/null +++ b/vue.config.js @@ -0,0 +1,103 @@ +'use strict' +const path = require('path') + +const mockUrl = 'http://yapi.demo.qunar.com/mock/17982' + +function resolve (dir) { + return path.join(__dirname, dir) +} + +// All configuration item explanations can be find in https://cli.vuejs.org/config/ +module.exports = { + publicPath: '/', + outputDir: 'dist', + assetsDir: 'static', + lintOnSave: process.env.NODE_ENV === 'development', + productionSourceMap: false, + devServer: { + port: 8080, + open: true, + overlay: { + warnings: false, + errors: true + }, + proxy: { + '/dev-api': { + target: mockUrl, + pathRewrite: { '^/dev-api': '' }, + secure: false, + changeOrigin: true + } + } + }, + configureWebpack: { + // provide the app's title in webpack's name field, so that + // it can be accessed in index.html to inject the correct title. + name: 'panda vue template', + resolve: { + alias: { + '@': resolve('src') + } + } + }, + chainWebpack (config) { + config.plugins.delete('preload') // TODO: need test + config.plugins.delete('prefetch') // TODO: need test + + // set svg-sprite-loader + config.module + .rule('svg') + .exclude.add(resolve('src/icons')) + .end() + config.module + .rule('icons') + .test(/\.svg$/) + .include.add(resolve('src/icons')) + .end() + .use('svg-sprite-loader') + .loader('svg-sprite-loader') + .options({ + symbolId: 'icon-[name]' + }) + .end() + + // set preserveWhitespace + config.module + .rule('vue') + .use('vue-loader') + .loader('vue-loader') + .tap(options => { + options.compilerOptions.preserveWhitespace = true + return options + }) + .end() + + config + // https://webpack.js.org/configuration/devtool/#development + .when(process.env.NODE_ENV === 'development', config => + config.devtool('cheap-source-map') + ) + + config.when(process.env.NODE_ENV !== 'development', config => { + config.optimization.splitChunks({ + chunks: 'all', + cacheGroups: { + libs: { + name: 'chunk-libs', + test: /[\\/]node_modules[\\/]/, + priority: 10, + chunks: 'initial' // only package third parties that are initially dependent + }, + commons: { + name: 'chunk-commons', + test: resolve('src/components'), // can customize your rules + minChunks: 3, // minimum common number + priority: 5, + reuseExistingChunk: true + } + } + }) + config.optimization.runtimeChunk('single') + }) + } +}