├── .gitignore ├── README.md ├── babel.config.js ├── img_preview ├── home-1.png ├── home-2.png ├── login.png ├── mall.png └── me.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── img │ ├── 01.jpg │ ├── 02.jpg │ ├── 03.jpg │ ├── 04.jpg │ └── 05.jpg └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── BallAnimation.vue │ ├── GoodsList.vue │ └── Header.vue ├── cube-ui.js ├── http-interceptor.js ├── main.js ├── router.js ├── services │ └── create.js ├── store.js ├── theme.styl ├── utils │ └── history.js └── views │ ├── Cart.vue │ ├── Home.vue │ └── Login.vue └── vue.config.js /.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 | ## mini-vue-mall 2 | 3 | 一个基于Vue.js的小型商城项目,包含登录、商品列表、购物车页面,数据为本地mock。 4 | 5 | ### 技术栈 6 | 7 | - Vue.js 8 | - vue-router 9 | - Vuex 10 | - cube-ui 11 | 12 | ### 登录 13 | 14 | - 登录账号:dora 密码:123 15 | - 全局路由守卫 16 | - Vuex管理登录信息 17 | - 页面借助cube-ui的表单组件搭建 18 | - http拦截器,将token加入请求头中,响应拦截器处理登录失败的情况 19 | 20 | ### 首页 21 | 22 | - 轮播图 23 | - 商品列表,含过滤筛选商品功能 24 | - 商品图片可预览 25 | - 购物车数据通过Vuex实现全局共享 26 | 27 | ### 购物车动画 28 | 29 | - 通过src/services/create.js中创建组件实例的方法,动态生成购物车动画组件 30 | - 借助Vue.js transition的三个动画钩子来用js控制动画 31 | - 实现思路 32 | - GoodsLIst.vue组件点击加购按钮时,派发一个事件 33 | - 通过这个事件,可以得到点击位置的坐标,这个坐标就是小球的起始位置,购物车图标为小球的终止位置 34 | - 动画结束需要销毁组件,释放内存 35 | 36 | ### 公共 37 | 38 | #### 通用 Header 组件 39 | 40 | - 自己实现一个Vue插件`src/utils/history.js`,通过堆栈的方式维护页面跳转的历史记录,控制返回跳转 41 | - 点击返回按钮是出栈操作 42 | - 在全局路由router.js 中,实例化路由前,通过原型扩展router的back()方法 43 | - 在全局afterEach中存放历史记录 44 | 45 | #### 底部tab,监听路由变化 46 | 47 | 将tab的value和路由的路径定义成相同的,这样切换tab,拿到当前的tab的value,如`/cart`,使用`this.$router.push(value)`,就可以切换页面了 48 | 49 | #### 页面切换滑动动画 50 | 51 | 动态绑定transition的name,根据当前是正常跳转页面还是返回页面操作,决定页面是左滑还是右滑 52 | 53 | ```html 54 | 55 | 56 | 57 | ``` 58 | 59 | ### 用到的cube-ui组件 60 | - cube-tabs 61 | - cube-slide 62 | - cube-drawer 63 | - cube-form 64 | - cube-image-preview 65 | 66 | ### 项目预览 67 | ![Image text](https://raw.githubusercontent.com/dora-zc/mini-vue-mall/master/img_preview/home-1.png) 68 | 69 | ![Image text](https://github.com/dora-zc/mini-vue-mall/blob/master/img_preview/home-2.png?raw=true) 70 | 71 | ![Image text](https://github.com/dora-zc/mini-vue-mall/blob/master/img_preview/mall.png?raw=true) 72 | 73 | ![Image text](https://github.com/dora-zc/mini-vue-mall/blob/master/img_preview/me.png?raw=true) 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /img_preview/home-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/img_preview/home-1.png -------------------------------------------------------------------------------- /img_preview/home-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/img_preview/home-2.png -------------------------------------------------------------------------------- /img_preview/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/img_preview/login.png -------------------------------------------------------------------------------- /img_preview/mall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/img_preview/mall.png -------------------------------------------------------------------------------- /img_preview/me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/img_preview/me.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vue-mall", 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 | }, 10 | "dependencies": { 11 | "axios": "^0.18.0", 12 | "core-js": "^2.6.5", 13 | "cube-ui": "~1.12.15", 14 | "vue": "^2.6.10", 15 | "vue-router": "^3.0.3", 16 | "vuex": "^3.0.1" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "^3.7.0", 20 | "@vue/cli-plugin-eslint": "^3.7.0", 21 | "@vue/cli-service": "^3.7.0", 22 | "babel-eslint": "^10.0.1", 23 | "eslint": "^5.16.0", 24 | "eslint-plugin-vue": "^5.0.0", 25 | "stylus": "^0.54.5", 26 | "stylus-loader": "^3.0.2", 27 | "vue-cli-plugin-cube-ui": "^0.2.5", 28 | "vue-template-compiler": "^2.5.21" 29 | }, 30 | "eslintConfig": { 31 | "root": true, 32 | "env": { 33 | "node": true 34 | }, 35 | "extends": [ 36 | "plugin:vue/essential", 37 | "eslint:recommended" 38 | ], 39 | "rules": { 40 | "accessor-pairs": 2, 41 | "arrow-spacing": [ 42 | 2, 43 | { 44 | "before": true, 45 | "after": true 46 | } 47 | ], 48 | "block-spacing": [ 49 | 2, 50 | "always" 51 | ], 52 | "brace-style": [ 53 | 2, 54 | "1tbs", 55 | { 56 | "allowSingleLine": true 57 | } 58 | ], 59 | "camelcase": [ 60 | 0, 61 | { 62 | "properties": "always" 63 | } 64 | ], 65 | "comma-dangle": [ 66 | 2, 67 | "never" 68 | ], 69 | "comma-spacing": [ 70 | 2, 71 | { 72 | "before": false, 73 | "after": true 74 | } 75 | ], 76 | "comma-style": [ 77 | 2, 78 | "last" 79 | ], 80 | "constructor-super": 2, 81 | "curly": [ 82 | 2, 83 | "multi-line" 84 | ], 85 | "dot-location": [ 86 | 2, 87 | "property" 88 | ], 89 | "eol-last": 2, 90 | "eqeqeq": [ 91 | 2, 92 | "allow-null" 93 | ], 94 | "generator-star-spacing": [ 95 | 2, 96 | { 97 | "before": true, 98 | "after": true 99 | } 100 | ], 101 | "handle-callback-err": [ 102 | 2, 103 | "^(err|error)$" 104 | ], 105 | "indent": [ 106 | 2, 107 | 2, 108 | { 109 | "SwitchCase": 1 110 | } 111 | ], 112 | "jsx-quotes": [ 113 | 2, 114 | "prefer-single" 115 | ], 116 | "key-spacing": [ 117 | 2, 118 | { 119 | "beforeColon": false, 120 | "afterColon": true 121 | } 122 | ], 123 | "keyword-spacing": [ 124 | 2, 125 | { 126 | "before": true, 127 | "after": true 128 | } 129 | ], 130 | "new-cap": [ 131 | 2, 132 | { 133 | "newIsCap": true, 134 | "capIsNew": false 135 | } 136 | ], 137 | "new-parens": 2, 138 | "no-array-constructor": 2, 139 | "no-caller": 2, 140 | "no-console": "off", 141 | "no-class-assign": 2, 142 | "no-cond-assign": 2, 143 | "no-const-assign": 2, 144 | "no-control-regex": 2, 145 | "no-delete-var": 2, 146 | "no-dupe-args": 2, 147 | "no-dupe-class-members": 2, 148 | "no-dupe-keys": 2, 149 | "no-duplicate-case": 2, 150 | "no-empty-character-class": 2, 151 | "no-empty-pattern": 2, 152 | "no-eval": 2, 153 | "no-ex-assign": 2, 154 | "no-extend-native": 2, 155 | "no-extra-bind": 2, 156 | "no-extra-boolean-cast": 2, 157 | "no-extra-parens": [ 158 | 2, 159 | "functions" 160 | ], 161 | "no-fallthrough": 2, 162 | "no-floating-decimal": 2, 163 | "no-func-assign": 2, 164 | "no-implied-eval": 2, 165 | "no-inner-declarations": [ 166 | 2, 167 | "functions" 168 | ], 169 | "no-invalid-regexp": 2, 170 | "no-irregular-whitespace": 2, 171 | "no-iterator": 2, 172 | "no-label-var": 2, 173 | "no-labels": [ 174 | 2, 175 | { 176 | "allowLoop": false, 177 | "allowSwitch": false 178 | } 179 | ], 180 | "no-lone-blocks": 2, 181 | "no-mixed-spaces-and-tabs": 2, 182 | "no-multi-spaces": 2, 183 | "no-multi-str": 2, 184 | "no-multiple-empty-lines": [ 185 | 2, 186 | { 187 | "max": 1 188 | } 189 | ], 190 | "no-native-reassign": 2, 191 | "no-negated-in-lhs": 2, 192 | "no-new-object": 2, 193 | "no-new-require": 2, 194 | "no-new-symbol": 2, 195 | "no-new-wrappers": 2, 196 | "no-obj-calls": 2, 197 | "no-octal": 2, 198 | "no-octal-escape": 2, 199 | "no-path-concat": 2, 200 | "no-proto": 2, 201 | "no-redeclare": 2, 202 | "no-regex-spaces": 2, 203 | "no-return-assign": [ 204 | 2, 205 | "except-parens" 206 | ], 207 | "no-self-assign": 2, 208 | "no-self-compare": 2, 209 | "no-sequences": 2, 210 | "no-shadow-restricted-names": 2, 211 | "no-spaced-func": 2, 212 | "no-sparse-arrays": 2, 213 | "no-this-before-super": 2, 214 | "no-throw-literal": 2, 215 | "no-trailing-spaces": 2, 216 | "no-undef": 2, 217 | "no-undef-init": 2, 218 | "no-unexpected-multiline": 2, 219 | "no-unmodified-loop-condition": 2, 220 | "no-unneeded-ternary": [ 221 | 2, 222 | { 223 | "defaultAssignment": false 224 | } 225 | ], 226 | "no-unreachable": 2, 227 | "no-unsafe-finally": 2, 228 | "no-unused-vars": [ 229 | 2, 230 | { 231 | "vars": "all", 232 | "args": "none" 233 | } 234 | ], 235 | "no-useless-call": 2, 236 | "no-useless-computed-key": 2, 237 | "no-useless-constructor": 2, 238 | "no-useless-escape": 0, 239 | "no-whitespace-before-property": 2, 240 | "no-with": 2, 241 | "one-var": [ 242 | 2, 243 | { 244 | "initialized": "never" 245 | } 246 | ], 247 | "operator-linebreak": [ 248 | 2, 249 | "after", 250 | { 251 | "overrides": { 252 | "?": "before", 253 | ":": "before" 254 | } 255 | } 256 | ], 257 | "padded-blocks": [ 258 | 2, 259 | "never" 260 | ], 261 | "quotes": [ 262 | 2, 263 | "single", 264 | { 265 | "avoidEscape": true, 266 | "allowTemplateLiterals": true 267 | } 268 | ], 269 | "semi": [ 270 | 2, 271 | "never" 272 | ], 273 | "semi-spacing": [ 274 | 2, 275 | { 276 | "before": false, 277 | "after": true 278 | } 279 | ], 280 | "space-before-blocks": [ 281 | 2, 282 | "always" 283 | ], 284 | "space-before-function-paren": [ 285 | 2, 286 | "never" 287 | ], 288 | "space-in-parens": [ 289 | 2, 290 | "never" 291 | ], 292 | "space-infix-ops": 2, 293 | "space-unary-ops": [ 294 | 2, 295 | { 296 | "words": true, 297 | "nonwords": false 298 | } 299 | ], 300 | "spaced-comment": [ 301 | 2, 302 | "always", 303 | { 304 | "markers": [ 305 | "global", 306 | "globals", 307 | "eslint", 308 | "eslint-disable", 309 | "*package", 310 | "!", 311 | "," 312 | ] 313 | } 314 | ], 315 | "template-curly-spacing": [ 316 | 2, 317 | "never" 318 | ], 319 | "use-isnan": 2, 320 | "valid-typeof": 2, 321 | "wrap-iife": [ 322 | 2, 323 | "any" 324 | ], 325 | "yield-star-spacing": [ 326 | 2, 327 | "both" 328 | ], 329 | "yoda": [ 330 | 2, 331 | "never" 332 | ], 333 | "prefer-const": 2, 334 | "object-curly-spacing": [ 335 | 2, 336 | "always", 337 | { 338 | "objectsInObjects": false 339 | } 340 | ], 341 | "array-bracket-spacing": [ 342 | 2, 343 | "never" 344 | ] 345 | }, 346 | "parserOptions": { 347 | "parser": "babel-eslint" 348 | } 349 | }, 350 | "postcss": { 351 | "plugins": { 352 | "autoprefixer": {} 353 | } 354 | }, 355 | "browserslist": [ 356 | "> 1%", 357 | "last 2 versions", 358 | "not ie <= 11", 359 | "Android >= 4.0", 360 | "iOS >= 8" 361 | ], 362 | "transformModules": { 363 | "cube-ui": { 364 | "transform": "cube-ui/src/modules/${member}", 365 | "kebabCase": true 366 | } 367 | } 368 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/public/favicon.ico -------------------------------------------------------------------------------- /public/img/01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/public/img/01.jpg -------------------------------------------------------------------------------- /public/img/02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/public/img/02.jpg -------------------------------------------------------------------------------- /public/img/03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/public/img/03.jpg -------------------------------------------------------------------------------- /public/img/04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/public/img/04.jpg -------------------------------------------------------------------------------- /public/img/05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/public/img/05.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | mini-vue-mall 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 33 | 81 | 82 | 144 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/mini-vue-mall/f1f751c36c81931a09dfa8895bd418e39233e665/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/BallAnimation.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 60 | 61 | 84 | -------------------------------------------------------------------------------- /src/components/GoodsList.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 48 | 49 | 73 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | 28 | 42 | -------------------------------------------------------------------------------- /src/cube-ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // By default we import all the components. 4 | // Only reserve the components on demand and remove the rest. 5 | // Style is always required. 6 | import { 7 | /* eslint-disable no-unused-vars */ 8 | Style, 9 | // basic 10 | Button, 11 | TabBar, 12 | Input, 13 | Form, 14 | // popup 15 | Toast, 16 | Drawer, 17 | ImagePreview, 18 | Slide 19 | } from 'cube-ui' 20 | 21 | Vue.use(Button) 22 | Vue.use(TabBar) 23 | Vue.use(Input) 24 | Vue.use(Form) 25 | Vue.use(Toast) 26 | Vue.use(Drawer) 27 | Vue.use(ImagePreview) 28 | Vue.use(Slide) 29 | -------------------------------------------------------------------------------- /src/http-interceptor.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from './store' 3 | import router from './router' 4 | 5 | axios.interceptors.request.use(config => { 6 | if (store.state.token) { 7 | config.headers.token = store.state.token 8 | } 9 | return config 10 | }) 11 | 12 | axios.interceptors.response.use( 13 | response => { 14 | if (response.status === 200) { 15 | const data = response.data 16 | if (data.code === -1) { 17 | clearHandler() 18 | } 19 | } 20 | return response 21 | }, 22 | err => { 23 | if (err.response.status === 401) { 24 | clearHandler() 25 | } 26 | } 27 | ) 28 | 29 | function clearHandler() { 30 | store.commit('setToken', '') 31 | localStorage.removeItem('token') 32 | router.push({ 33 | path: '/login', 34 | query: { 35 | redirect: router.currentRoute.path 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import './cube-ui' 3 | import App from './App.vue' 4 | import router from './router' 5 | import store from './store' 6 | import axios from 'axios' 7 | import './http-interceptor' 8 | import XHeader from './components/Header.vue' 9 | 10 | Vue.config.productionTip = false 11 | 12 | Vue.component('x-header', XHeader) 13 | 14 | Vue.prototype.$http = axios 15 | 16 | new Vue({ 17 | router, 18 | store, 19 | render: h => h(App) 20 | }).$mount('#app') 21 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import Login from './views/Login.vue' 5 | import store from './store' 6 | import History from './utils/history' 7 | 8 | Vue.use(Router) 9 | Vue.use(History) 10 | 11 | Router.prototype.goBack = function() { 12 | this.isBack = true 13 | this.back() 14 | } 15 | 16 | const router = new Router({ 17 | routes: [ 18 | { 19 | path: '/', 20 | name: 'home', 21 | component: Home 22 | }, 23 | { 24 | path: '/Login', 25 | name: 'login', 26 | component: Login 27 | }, 28 | { 29 | path: '/Cart', 30 | name: 'cart', 31 | meta: { auth: true }, 32 | // route level code-splitting 33 | // this generates a separate chunk (about.[hash].js) for this route 34 | // which is lazy-loaded when the route is visited. 35 | component: () => 36 | import(/* webpackChunkName: "about" */ './views/Cart.vue') 37 | } 38 | ] 39 | }) 40 | 41 | router.beforeEach((to, from, next) => { 42 | if (to.meta.auth) { 43 | if (store.state.token) { 44 | next() 45 | } else { 46 | next({ 47 | path: '/login', 48 | query: { redirect: to.path } 49 | }) 50 | } 51 | } else { 52 | next() 53 | } 54 | }) 55 | 56 | router.afterEach((to) => { 57 | if (router.isBack) { 58 | History.pop() 59 | router.isBack = false 60 | router.transitionName = 'route-back' 61 | } else { 62 | History.push(to.path) 63 | router.transitionName = 'route-forward' 64 | } 65 | }) 66 | 67 | export default router 68 | -------------------------------------------------------------------------------- /src/services/create.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | function create(Component, props) { 4 | const instance = new Vue({ 5 | render(h) { 6 | return h(Component, { props }) 7 | } 8 | }).$mount() // 执行挂载 9 | // 将真实dom节点插入文档中 10 | document.body.appendChild(instance.$el) 11 | // 获取组件实例 12 | const comp = instance.$children[0] 13 | // 销毁组件,释放内存 14 | comp.remove = () => { 15 | instance.$destroy() 16 | } 17 | return comp 18 | } 19 | 20 | export default create 21 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | const store = new Vuex.Store({ 7 | state: { 8 | token: localStorage.getItem('token') || '', 9 | cart: JSON.parse(localStorage.getItem('cart')) || [] 10 | }, 11 | mutations: { 12 | setToken(state, token) { 13 | state.token = token 14 | }, 15 | addCart(state, item) { 16 | const good = state.cart.find(v => v.id === item.id) 17 | if (good) { 18 | good.cartCount += 1 19 | } else { 20 | state.cart.push({ 21 | ...item, 22 | cartCount: 1 23 | }) 24 | } 25 | }, 26 | countMinus(state, index) { 27 | const item = state.cart[index] 28 | if (item.cartCount > 1) { 29 | item.cartCount -= 1 30 | } else { 31 | state.cart.splice(index, 1) 32 | } 33 | }, 34 | countAdd(state, index) { 35 | state.cart[index].cartCount += 1 36 | } 37 | }, 38 | actions: {}, 39 | getters: { 40 | isLogin: state => { 41 | return !!state.token 42 | }, 43 | cartTotal: state => { 44 | let num = 0 45 | state.cart.forEach(v => { 46 | num += v.cartCount 47 | }) 48 | return num 49 | }, 50 | total: state => 51 | state.cart.reduce((num, v) => (num += v.cartCount * v.price), 0) 52 | } 53 | }) 54 | 55 | store.subscribe((mutation, state) => { 56 | switch (mutation.type) { 57 | case 'setToken': 58 | localStorage.setItem('token', JSON.stringify(state.token)) 59 | break 60 | case 'addCart': 61 | localStorage.setItem('cart', JSON.stringify(state.cart)) 62 | break 63 | } 64 | }) 65 | 66 | export default store 67 | -------------------------------------------------------------------------------- /src/theme.styl: -------------------------------------------------------------------------------- 1 | @require "~cube-ui/src/common/stylus/var/color.styl" 2 | 3 | 4 | // action-sheet 5 | $action-sheet-color := $color-grey 6 | $action-sheet-active-color := $color-orange 7 | $action-sheet-bgc := $color-white 8 | $action-sheet-active-bgc := $color-light-grey-opacity 9 | $action-sheet-title-color := $color-dark-grey 10 | $action-sheet-space-bgc := $color-mask-bg 11 | /// picker style 12 | $action-sheet-picker-cancel-color := $color-light-grey 13 | $action-sheet-picker-cancel-active-color := $color-light-grey-s 14 | 15 | // bubble 16 | 17 | // button 18 | $btn-color := $color-white 19 | $btn-bgc := $color-regular-blue 20 | $btn-bdc := $color-regular-blue 21 | $btn-active-bgc := $color-blue 22 | $btn-active-bdc := $color-blue 23 | $btn-disabled-color := $color-white 24 | $btn-disabled-bgc := $color-light-grey-s 25 | $btn-disabled-bdc := $color-light-grey-s 26 | /// primary 27 | $btn-primary-color := $color-white 28 | $btn-primary-bgc := $color-orange 29 | $btn-primary-bdc := $color-orange 30 | $btn-primary-active-bgc := $color-dark-orange 31 | $btn-primary-active-bdc := $color-dark-orange 32 | /// light 33 | $btn-light-color := $color-grey 34 | $btn-light-bgc := $color-light-grey-sss 35 | $btn-light-bdc := $color-light-grey-sss 36 | $btn-light-active-bgc := $color-active-grey 37 | $btn-light-active-bdc := $color-active-grey 38 | /// outline 39 | $btn-outline-color := $color-grey 40 | $btn-outline-bgc := transparent 41 | $btn-outline-bdc := $color-grey 42 | $btn-outline-active-bgc := $color-grey-opacity 43 | $btn-outline-active-bdc := $color-grey 44 | /// outline-primary 45 | $btn-outline-primary-color := $color-orange 46 | $btn-outline-primary-bgc := transparent 47 | $btn-outline-primary-bdc := $color-orange 48 | $btn-outline-primary-active-bgc := $color-orange-opacity 49 | $btn-outline-primary-active-bdc := $color-dark-orange 50 | 51 | // toolbar 52 | $toolbar-bgc := $color-light-grey-sss 53 | $toolbar-active-bgc := $color-active-grey 54 | 55 | // checkbox 56 | $checkbox-color := $color-grey 57 | $checkbox-icon-color := $color-light-grey-s 58 | /// checked 59 | $checkbox-checked-icon-color := $color-orange 60 | $checkbox-checked-icon-bgc := $color-white 61 | /// disabled 62 | $checkbox-disabled-icon-color := $color-light-grey-ss 63 | $checkbox-disabled-icon-bgc := $color-light-grey-ss 64 | // checkbox hollow 65 | $checkbox-hollow-checked-icon-color := $color-orange 66 | $checkbox-hollow-disabled-icon-color := $color-light-grey-ss 67 | // checkbox-group 68 | $checkbox-group-bgc := $color-white 69 | $checkbox-group-horizontal-bdc := $color-light-grey-s 70 | 71 | // radio 72 | $radio-group-bgc := $color-white 73 | $radio-group-horizontal-bdc := $color-light-grey-s 74 | $radio-color := $color-grey 75 | $radio-icon-color := $color-light-grey-s 76 | /// selected 77 | $radio-selected-icon-color := $color-white 78 | $radio-selected-icon-bgc := $color-orange 79 | /// disabled 80 | $radio-disabled-icon-bgc := $color-light-grey-ss 81 | // radio hollow 82 | $radio-hollow-selected-icon-color := $color-orange 83 | $radio-hollow-disabled-icon-color := $color-light-grey-ss 84 | 85 | // dialog 86 | $dialog-color := $color-grey 87 | $dialog-bgc := $color-white 88 | $dialog-icon-color := $color-regular-blue 89 | $dialog-icon-bgc := $color-background 90 | $dialog-title-color := $color-dark-grey 91 | $dialog-close-color := $color-light-grey 92 | $dialog-btn-color := $color-light-grey 93 | $dialog-btn-bgc := $color-white 94 | $dialog-btn-active-bgc := $color-light-grey-opacity 95 | $dialog-btn-highlight-color := $color-orange 96 | $dialog-btn-highlight-active-bgc := $color-light-orange-opacity 97 | $dialog-btn-disabled-color := $color-light-grey 98 | $dialog-btn-disabled-active-bgc := transparent 99 | $dialog-btns-split-color := $color-row-line 100 | 101 | // index-list 102 | $index-list-bgc := $color-white 103 | $index-list-title-color := $color-dark-grey 104 | $index-list-anchor-color := $color-light-grey 105 | $index-list-anchor-bgc := #f7f7f7 106 | $index-list-item-color := $color-dark-grey 107 | $index-list-item-active-bgc := $color-light-grey-opacity 108 | $index-list-nav-color := $color-grey 109 | $index-list-nav-active-color := $color-orange 110 | 111 | // loading 112 | 113 | // picker 114 | $picker-bgc := $color-white 115 | $picker-title-color := $color-dark-grey 116 | $picker-subtitle-color := $color-light-grey 117 | $picker-confirm-btn-color := $color-orange 118 | $picker-confirm-btn-active-color := $color-light-orange 119 | $picker-cancel-btn-color := $color-light-grey 120 | $picker-cancel-btn-active-color := $color-light-grey-s 121 | $picker-item-color := $color-dark-grey 122 | 123 | // popup 124 | $popup-mask-bgc := rgb(37, 38, 45) 125 | $popup-mask-opacity := .4 126 | 127 | //scroll 128 | 129 | // slide 130 | $slide-dot-bgc := $color-light-grey-s 131 | $slide-dot-active-bgc := $color-orange 132 | 133 | // time-picker 134 | 135 | // tip 136 | $tip-color := $color-white 137 | $tip-bgc := $color-dark-grey-opacity 138 | 139 | // toast 140 | $toast-color := $color-light-grey-s 141 | $toast-bgc := rgba(37, 38, 45, 0.9) 142 | 143 | // upload 144 | $upload-btn-color := $color-grey 145 | $upload-btn-bgc := $color-white 146 | $upload-btn-active-bgc := $color-light-grey-opacity 147 | $upload-btn-box-shadow := 0 0 6px 2px $color-grey-opacity 148 | $upload-btn-border-color := #e5e5e5 149 | $upload-file-bgc := $color-white 150 | $upload-file-remove-color := rgba(0, 0, 0, .8) 151 | $upload-file-remove-bgc := $color-white 152 | $upload-file-state-bgc := $color-mask-bg 153 | $upload-file-success-color := $color-orange 154 | $upload-file-error-color := #f43530 155 | $upload-file-status-bgc := $color-white 156 | $upload-file-progress-color := $color-white 157 | 158 | // switch 159 | $switch-on-bgc := $color-orange 160 | $switch-off-bgc := $color-white 161 | $switch-off-border-color := #e4e4e4 162 | 163 | // input 164 | $input-color := $color-grey 165 | $input-bgc := $color-white 166 | $input-border-color := $color-row-line 167 | $input-focus-border-color := $color-orange 168 | $input-placeholder-color := $color-light-grey-s 169 | $input-clear-icon-color := $color-light-grey 170 | 171 | //textarea 172 | $textarea-color := $color-grey 173 | $textarea-bgc := $color-white 174 | $textarea-border-color := $color-row-line 175 | $textarea-focus-border-color := $color-orange 176 | $textarea-outline-color := $color-orange 177 | $textarea-placeholder-color := $color-light-grey-s 178 | $textarea-indicator-color := $color-light-grey-s 179 | 180 | // validator 181 | $validator-msg-def-color := #e64340 182 | 183 | // select 184 | $select-color := $color-grey 185 | $select-bgc := $color-white 186 | $select-disabled-color := #b8b8b8 187 | $select-disabled-bgc := $color-light-grey-opacity 188 | $select-border-color := $color-light-grey-s 189 | $select-border-active-color := $color-orange 190 | $select-icon-color := $color-light-grey 191 | $select-placeholder-color := $color-light-grey-s 192 | 193 | // swipe 194 | $swipe-btn-color := $color-white 195 | 196 | // form 197 | $form-color := $color-grey 198 | $form-bgc := $color-white 199 | $form-invalid-color := #e64340 200 | $form-group-legend-color := $color-light-grey 201 | $form-group-legend-bgc := $color-background 202 | $form-label-required-color := #e64340 203 | 204 | // drawer 205 | $drawer-color := $color-dark-grey 206 | $drawer-title-bdc := $color-light-grey-ss 207 | $drawer-title-bgc := $color-white 208 | $drawer-panel-bgc := $color-white 209 | $drawer-item-active-bgc := $color-light-grey-opacity 210 | 211 | // scroll-nav 212 | $scroll-nav-bgc := $color-white 213 | $scroll-nav-color := $color-grey 214 | $scroll-nav-active-color := $color-orange 215 | 216 | // image-preview 217 | $image-preview-counter-color := $color-white 218 | 219 | // tab-bar & tab-panel 220 | $tab-color := $color-grey 221 | $tab-active-color := $color-dark-orange 222 | $tab-slider-bgc := $color-dark-orange 223 | -------------------------------------------------------------------------------- /src/utils/history.js: -------------------------------------------------------------------------------- 1 | const History = { 2 | _history: [], 3 | install(Vue) { 4 | Object.defineProperty(Vue.prototype, '$routerHistory', { 5 | get() { 6 | return History 7 | } 8 | }) 9 | }, 10 | push(path) { 11 | this._history.push(path) 12 | }, 13 | pop() { 14 | this._history.pop() 15 | }, 16 | canBack() { 17 | return this._history.length > 1 18 | } 19 | } 20 | export default History 21 | -------------------------------------------------------------------------------- /src/views/Cart.vue: -------------------------------------------------------------------------------- 1 | 42 | 69 | 70 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 107 | 108 | 119 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 112 | 113 | 122 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { 3 | loaderOptions: { 4 | stylus: { 5 | 'resolve url': true, 6 | import: ['./src/theme'] 7 | } 8 | } 9 | }, 10 | pluginOptions: { 11 | 'cube-ui': { 12 | postCompile: true, 13 | theme: true 14 | } 15 | }, 16 | configureWebpack: { 17 | devServer: { 18 | before(app) { 19 | app.get('/api/goods', function(req, res) { 20 | res.json({ 21 | code: 0, 22 | slider: [ 23 | { 24 | id: 21, 25 | img: '/img/01.jpg' 26 | }, 27 | { 28 | id: 22, 29 | img: '/img/02.jpg' 30 | }, 31 | { 32 | id: 23, 33 | img: '/img/03.jpg' 34 | }, 35 | { 36 | id: 24, 37 | img: '/img/04.jpg' 38 | } 39 | ], 40 | data: { 41 | phone: [ 42 | { 43 | id: 1, 44 | title: '小米手机', 45 | price: '100', 46 | img: '/img/01.jpg', 47 | count: 100 48 | }, 49 | { 50 | id: 2, 51 | title: 'iphone', 52 | price: '120', 53 | img: '/img/03.jpg', 54 | count: 100 55 | }, 56 | { 57 | id: 3, 58 | title: '华为', 59 | price: '80', 60 | img: '/img/02.jpg', 61 | count: 100 62 | }, 63 | { 64 | id: 4, 65 | title: 'vivo', 66 | price: '110', 67 | img: '/img/04.jpg', 68 | count: 100 69 | }, 70 | { 71 | id: 5, 72 | title: 'oppo', 73 | price: '200', 74 | img: '/img/02.jpg', 75 | count: 100 76 | }, 77 | { 78 | id: 6, 79 | title: '360手机', 80 | price: '30', 81 | img: '/img/05.jpg', 82 | count: 100 83 | } 84 | ], 85 | equipment: [ 86 | { 87 | id: 7, 88 | title: '电饭煲', 89 | price: '120', 90 | img: '/img/03.jpg', 91 | count: 101 92 | }, 93 | { 94 | id: 8, 95 | title: '电冰箱', 96 | price: '80', 97 | img: '/img/02.jpg', 98 | count: 100 99 | }, 100 | { 101 | id: 9, 102 | title: '电热水器', 103 | price: '110', 104 | img: '/img/01.jpg', 105 | count: 100 106 | }, 107 | { 108 | id: 10, 109 | title: '电磁炉', 110 | price: '200', 111 | img: '/img/04.jpg', 112 | count: 100 113 | } 114 | ], 115 | market: [ 116 | { 117 | id: 11, 118 | title: '零食', 119 | price: '80', 120 | img: '/img/02.jpg', 121 | count: 100 122 | }, 123 | { 124 | id: 12, 125 | title: '水果', 126 | price: '110', 127 | img: '/img/01.jpg', 128 | count: 100 129 | }, 130 | { 131 | id: 13, 132 | title: '蔬菜', 133 | price: '30', 134 | img: '/img/04.jpg', 135 | count: 100 136 | } 137 | ], 138 | book: [ 139 | { 140 | id: 14, 141 | title: '文学', 142 | price: '200', 143 | img: '/img/01.jpg', 144 | count: 100 145 | }, 146 | { 147 | id: 15, 148 | title: '历史', 149 | price: '120', 150 | img: '/img/03.jpg', 151 | count: 100 152 | }, 153 | { 154 | id: 16, 155 | title: 'IT科技', 156 | price: '80', 157 | img: '/img/02.jpg', 158 | count: 100 159 | } 160 | ], 161 | sport: [ 162 | { 163 | id: 17, 164 | title: '跑步鞋', 165 | price: '100', 166 | img: '/img/01.jpg', 167 | count: 100 168 | }, 169 | { 170 | id: 18, 171 | title: '山地车', 172 | price: '120', 173 | img: '/img/03.jpg', 174 | count: 100 175 | }, 176 | { 177 | id: 19, 178 | title: '篮球', 179 | price: '80', 180 | img: '/img/02.jpg', 181 | count: 100 182 | }, 183 | { 184 | id: 20, 185 | title: '瑜伽垫', 186 | price: '110', 187 | img: '/img/05.jpg', 188 | count: 100 189 | } 190 | ] 191 | }, 192 | keys: ['phone', 'equipment', 'market', 'book', 'sport'] 193 | }) 194 | }) 195 | 196 | app.get('/api/login', function(req, res) { 197 | const { username, password } = req.query 198 | if (username === 'dora' && password === '123') { 199 | res.json({ 200 | code: 0, 201 | token: 'xxx' 202 | }) 203 | } else { 204 | res.json({ 205 | code: 1, 206 | message: '用户名或密码错误' 207 | }) 208 | } 209 | }) 210 | 211 | app.get('/api/logout', function(req, res) { 212 | res.json({ code: -1 }) 213 | }) 214 | } 215 | } 216 | } 217 | } 218 | --------------------------------------------------------------------------------