後端連接配接已不可用,該項目無效!
想看看前端代碼還是可以接着閱讀!
github: 小米商城源碼
賬号:sunyu 密碼:123456
該項目是對小米商城系統的模仿,實作了從浏覽商品到結算商品的整個過程,其中包括了商品清單、根據價格篩選商品、對商品 排序、登入、加入購物車、結算等功能 前台使用vue-cli建構了請求伺服器,使用了Vue架構,還使用了vue-router、axios、Vuex等中間件 背景使用了node.js,express架構建構了背景伺服器
1. 項目初始化
全局環境下安裝vue,vue-cli 腳手架
npm install vue -g
npm install vue-cli -g
初始化項目:
$ vue init webpack MiMall
? Project name (MiMall) mistore
? Project name mistore
? Project description (A Vue.js project) xiaomi store with vue
? Project description xiaomi store with vue
? Author (Spock <[email protected]>)
? Author Spock <[email protected]>
? Vue build (Use arrow keys)
? Vue build standalone
? Install vue-router? (Y/n)
? Install vue-router? Yes
? Use ESLint to lint your code? (Y/n) n
? Use ESLint to lint your code? No
? Set up unit tests (Y/n) n
? Set up unit tests No
? Setup e2e tests with Nightwatch? (Y/n) n
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recom
? Should we run `npm install` for you after the project has been created? (recom
mended) npm
先安裝幾個插件:
npm i babel-runtime fastclick babel-polyfill
"babel-polyfill": "^6.26.0",//es6的API轉義
"babel-runtime": "^6.26.0",//對es6的文法進行轉義
"fastclick": "^1.0.6",//解決移動端300ms延遲的問題
main.js中的設定:
import 'babel-polyfill'
import fastclick from 'fastclick'
fastclick.attach(document.body)//這樣就能解決body下按鈕點選300ms的延遲
2. 配置路由
先配置路徑别名:( // 别名,隻針對于js庫,css的引入還是要寫相對路徑,不能省略)
build/webpack.base.conf.js:
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'components': resolve('src/components'),
//當在js檔案中import其他檔案時路徑直接寫commont相當于../src/components
'api': resolve('src/api') //後面會用到
}
src/router :
配置别名的好處就在下面import的時候展現出來了。
先配置首頁,購物車,及位址欄的路由。
import Vue from 'vue'
import Router from 'vue-router'
import Goods from 'components/goods'
import Car from 'components/car'
import Address from 'components/address'
Vue.use(Router)
export default new Router ({
routes: [
{
path: '/',
component: Goods
},
{
path: '/car',
component: Car
},
{
path: '/address',
component: Address
}
]
})
回到首頁面配置顯示的資訊:
src/App.vue:
<template>
<div id="app">
<m-header></m-header>
<tab></tab>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>
<script>
import MHeader from 'components/m-header'
import Tab from 'components/tab'
export default {
components: {
MHeader,
Tab
}
}
</script>
<style>
</style>
可以看到引用了幾個元件,還未建立。接下來建立這幾個頁面:
src/ components/goods.vue (示例:car.vue,address.vue,m-header.vue,tab.vue也類似該結建構立)
<template>
<p>商品頁面</p>
</template>
<script>
</script>
<style type="text/css">
</style>
現在 控制台 輸入指令: npm run dev , 打開localhost:8080 就可以看到首頁面的資訊了。
3. “Sticky Footer”布局:
指的就是一種網頁效果: 如果頁面内容不足夠長時,頁腳固定在浏覽器視窗的底部;如果内容足夠長時,頁腳固定在頁面的最底部。但如果網頁内容不夠長,置底的頁腳就會保持在浏覽器視窗底部。
src/ components/footer.vue:
<template>
<div class="footer">
<div class="footer-contain">
<div class="area-select">
<span>地區:</span>
<select>
<option value="中國">中國</option>
<option value="USA">USA</option>
<option value="India">India</option>
</select>
</div>
<ul>
<li>隐私政策</li>
<li>團隊合作</li>
<li>關于我們</li>
<li>©2018 taoMall.com 版權所有</li>
</ul>
</div>
</div>
</template>
<script>
</script>
<style type="text/css">
.footer {
margin-top: -100px;
width: 100%;
height: 100px;
background-color: #bbb;
overflow: hidden;
}
.footer-contain {
padding:0 120px;
height: 100%;
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
.footer-contain .area-select {
flex-width: 200px;
width: 200px;
}
.footer-contain ul {
display: flex;
}
.footer-contain ul li {
margin-left: 10px;
}
</style>
然後更改App.vue中的内容:
<template>
<div id="app">
<div class="content-wrap">
<div class="content">
<m-header></m-header>
<tab></tab>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</div>
<m-footer></m-footer>
</div>
</template>
<script>
import MHeader from 'components/m-header'
import Tab from 'components/tab'
import Footer from 'components/footer'
export default {
components: {
MHeader,
Tab,
MFooter:Footer,
}
}
</script>
<style>
#app {
height: 100%;
}
.content-wrap {
min-height: 100%;
}
.content {
padding-bottom: 100px;
}
</style>
(缺點: 添加了兩個div标簽)
done! ✿✿ヽ(°▽°)ノ✿
4. 根據路由位址顯示不同的文本資訊
src/components/tab.vue: 利用計算屬性及this.$route.path 得到路由位址
<template>
<div class="tab">
<ul class="tab-contain">
<li class="tab-item"><router-link tag="a" to="/">首頁</router-link></li>
<li class="tab-item"><span>{{tabTitle}}</span></li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
pathMap: {
'/': '商品資訊',
'/car': '購物車資訊',
'/address': '位址資訊',
}
}
},
//計算屬性:邏輯計算,根據pathMap資料中的位址綁定顯示的文字
computed: {
tabTitle() {
return this.pathMap[this.$route.path]
}
}
}
</script>
<style type="text/css">
.tab {
padding: 20px 100px;
box-sizing: border-box;
background-color: #f3f0f0;
}
.tab-contain {
font-size: 0;
}
.tab-contain .tab-item {
display: inline-block;
font-size: 18px;
margin-right: 10px;
}
.tab-contain .tab-item span {
margin-left:10px;
color: #de1442;
}
</style>
5. 利用axios擷取資料
(ps: 本打算利用axios僞造referer擷取資料,伺服器原api擷取有困惑,遂敗。直接設定跨域處理)
在前端 good.js 請求位址,不是直接請求服務端,而是請求我們自己的server端,然後我們的位址再去請求服務端(使用axios發送http請求)
下載下傳axios:
npm install axios --save
src/api/goods.js:
import axios from 'axios'
export function getGoodsList() {
const url = '/goods/list'
const data = {
page: 0,
pageSize: 8,
orderFlag: true,
priceLeave: 'All'
}
return axios.get(url, {
params:data
}).then((res) => {
return Promise.resolve(res)
})
}
config/index.js:(設定跨域請求)
proxyTable: {
'/goods/list': {
target: 'http://linyijiu.cn:3000',
changeOrigin: true
}
},
src/components/goods.vue:(請求資料)
<template>
<p>商品頁面</p>
</template>
<script>
import {getGoodsList} from 'api/goods'
export default {
data() {
return {
goods : {}
}
},
created() {
this._getGoodsList()
},
methods: {
_getGoodsList() {
getGoodsList().then((res) => {
this.goods = res.data
console.log(this.goods)
})
}
}
}
</script>
<style type="text/css">
</style>
重新啟動npm run dev,就能夠在控制台看到輸出的資料。
6. 實作商品頁面資料運用
src/compnents/goods.vue : 樣式還是用stylu寫比較好啊(´థ౪థ)σ
<template>
<div class="goods-contain">
<div class="goods-sort">
<p>排序:</p>
<p>預設</p>
<p class="goods-price">價格<span class="icon-arrow">↑</span></p>
</div>
<div class="goods-items">
<ul class="price-inter">
<li class="price-name">價格:</li>
<li class="price-item active">全部</li>
<li class="price-item" v-for="(item, index) in price">{{item.startPrice}}-{{item.endPrice}}</li>
</ul>
<ul class="goods-info">
<li class="goods-des" v-for="(item, index) in goods" :key="index">
<div class="good-all">
<div class="good-image">
<img :src="'/static/images/' + item.productImg">
</div>
<p class="good-name">{{item.productName}}</p>
<p class="good-price">¥{{item.productPrice}}</p>
<div class="add-car">加入購物車</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import {getGoodsList} from 'api/goods'
export default {
data() {
return {
goods : [],
price:[
{
"startPrice":"0.00",
"endPrice":"100.00"
},
{
"startPrice":"100.00",
"endPrice":"500.00"
},
{
"startPrice":"500.00",
"endPrice":"1000.00"
},
{
"startPrice":"1000.00",
"endPrice":"8000.00"
},
],
}
},
created() {
this._getGoodsList()
},
methods: {
_getGoodsList() {
getGoodsList().then((res) => {
this.goods = res.data
console.log(this.goods)
})
}
}
}
</script>
<style type="text/css">
</style>
7. 圖檔懶加載
安裝插件:
npm i vue-lazyload
main.js 中引入:(先在static位址中放入圖檔資源)
import VueLazyLoad from 'vue-lazyload'
Vue.use(VueLazyLoad,{
loading: '/static/images/Loading/loading-balls.svg'
})
src/components/goods.vue : 在圖檔的使用位址上更改為 v-lazy 即可。
<div class="good-image">
<img v-lazy="'/static/images/' + item.productImg">
</div>
這下重新整理頁面就能夠看到有滾動的小球的加載過程。
8. 價格排序
邏輯:通過觀察 Headers
通過點選左邊不同區間的價格發現: priceLevel 控制顯示區間(全部時為all,其他時候為0,1,2,3)
通過點選價格排序發現:orderPrice升序為true,降序為false。
page 應該是拿來實作下拉加載圖檔的優化。
實作:是以這些參數應該在傳遞的時候變化,是以需要修改api請求的參數
src/api/goods.js: (将axios請求分離出來,不會讓頁面顯得冗長)
import axios from 'axios'
export function getGoodsList(page, pageSize,orderFlag,priceLevel) {
const url = '/goods/list'
const data = {
page,
pageSize,
orderFlag,
priceLevel
}
return axios.get(url, {
params:data
}).then((res) => {
return Promise.resolve(res)
})
}
src/components/goods.vue:
<template>
<div class="goods-contain">
<div class="goods-sort">
<p>排序:</p>
<p>預設</p>
<p class="goods-price" @click="sortBy()">價格<span class="icon-arrow" :class="{arrow_turn:!orderFlag}">↑</span></p>
</div>
<div class="goods-items">
<ul class="price-inter">
<li class="price-name">價格:</li>
<li class="price-item" :class="{active: priceLevel === 'all'}" @click="selectInter('all')">全部</li>
<li class="price-item" :class="{active: priceLevel === index}" v-for="(item, index) in price" @click="selectInter(index)">{{item.startPrice}}-{{item.endPrice}}</li>
</ul>
<ul class="goods-info">
<li class="goods-des" v-for="(item, index) in goods" :key="index">
<div class="good-all">
<div class="good-image">
<img v-lazy="'/static/images/' + item.productImg">
</div>
<p class="good-name">{{item.productName}}</p>
<p class="good-price">¥{{item.productPrice}}</p>
<div class="add-car">加入購物車</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import {getGoodsList} from 'api/goods'
export default {
data() {
return {
goods : [],
price:[
{
"startPrice":"0.00",
"endPrice":"100.00"
},
{
"startPrice":"100.00",
"endPrice":"500.00"
},
{
"startPrice":"500.00",
"endPrice":"1000.00"
},
{
"startPrice":"1000.00",
"endPrice":"8000.00"
},
],
page: 0, //下拉加載
pageSize: 8,
orderFlag: true, //升序還是降序
priceLevel: 'all', //顯示的區間
}
},
created() {
this._getGoodsList()
},
methods: {
_getGoodsList() {
getGoodsList(this.page, this.pageSize, this.orderFlag, this.priceLevel).then((res) => {
this.goods = res.data //得到商品清單資料存在goods變量中
})
},
sortBy() {
this.orderFlag = !this.orderFlag
this.page = 0
this._getGoodsList(false)
},
selectInter(index) {
this.priceLevel = index
this.page = 0
this._getGoodsList(false)
}
}
}
</script>
<style type="text/css">
//
</style>
9. 下拉加載
建議看過此文再接着學習: vue 元件實作下拉加載
下載下傳元件:
npm install vue-infinite-scroll --save
main.js : 全局環境下設定
import InfiniteScroll from 'vue-infinite-scroll'
Vue.use(InfiniteScroll)
api/ components/ goods.vue:
<template>
<div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="30">
<!-- 加載更多 -->
</div>
</template>
<script>
export default {
data() {
return {
busy: false,
}
},
mounted() { //将擷取資料的函數放在mounted中執行是為了能夠在重新整理的時候也得到資料
this._getGoodsList(false)
},
methods: {
_getGoodsList(flag) {
getGoodsList(this.page, this.pageSize, this.orderFlag, this.priceLevel).then((res) => {
if (flag) {
//多次加載資料,則需要把資料相加
this.goods = this.goods.concat(res.data)
if (res.data.length === 0) {
//沒有資料可加載就關閉無限滾動
this.busy = true
} else {
//否則仍可以觸發無限滾動
this.busy = false
}
} else {
//第一次加載資料并且允許滾動
this.goods = res.data
this.busy = false
}
})
},
loadMore() {
this.busy = true
//0.3s 後加載下一頁的資料
setTimeout(() => {
this.page ++
this._getGoodsList(true) //滾動的時候調用axios加載資料,參數判斷不是第一次加載
}, 300)
}
}
}
</script>
10. 登入頁面
邏輯: 點選登入後,顯示登入視窗,如果資訊正确則顯示登入後的資訊。是以添加一個事件showLogin() 控制視窗的登入。
<template>
<div class="sign">
<p class="signin" v-if="showLoginOut" @click = "showLogin()">登入</p>
<div class="signout" v-if="!showLoginOut">
<span class="sign-name"></span>
<span class="sign-out">退出</span>
<router-link to="/car" class="sign-car">購物車</router-link>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
showLoginOut : true,
}
},
methods: {
// 顯示登入視窗
showLogin() {
this.showLogDialog = true
}
}
</script>
看了兩眼登入頁面,其實可以寫成一個子元件的形式,用于登入、注冊等的載體(沒有注冊頁啊!!!(ノ`Д)ノ ),然後利用插槽插入基本內容。(運用了元件之間的參數傳遞)
建立一個基礎元件:
src/components/base/dialog.vue:
<template>
<!-- 設計彈窗的架構樣式,再利用slot插槽插進不同的内容 -->
<div>
<div class="dialog-wrap" v-if="isShow">
<div class="dialog-cover" @click="closeMyself"></div>
<!-- 動畫效果 -->
<transition name="drop">
<div class="dialog-content" v-if="isShow">
<p class="dialog-close" @click="closeMyself">X</p>
<!-- 插槽的位置 -->
<slot>hello</slot>
</div>
</transition>
</div>
</div>
</template>
<script>
export default {
props:{
isShow:{
type:Boolean,
default:false
}
},
methods:{
closeMyself(){
this.$emit('on-close') //發送給父元件處理
}
}
};
</script>
<style scoped>
.drop-enter-active {
transition: all .5s ease;
}
.drop-leave-active {
transition: all .3s ease;
}
.drop-enter {
transform: translateY(-500px);
}
.drop-leave-active {
transform: translateY(-500px);
}
.dialog-wrap {
position: fixed;
width: 100%;
height: 100%;
}
.dialog-cover {
background: #000;
opacity: .3;
position: fixed;
z-index: 5;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.dialog-content {
width: 50%;
position: fixed;
max-height: 50%;
overflow: auto;
background: #fff;
top: 20%;
left: 50%;
margin-left: -25%;
z-index: 10;
border: 2px solid #464068;
padding: 2%;
line-height: 1.6;
}
.dialog-close {
position: absolute;
right: 5px;
top: 5px;
width: 20px;
height: 20px;
text-align: center;
cursor: pointer;
}
.dialog-close:hover {
color: #4fc08d;
}
</style>
在頭部引用:
src/components/m-header.vue:(父元件傳遞值給子元件,并且處理子元件傳遞來的事件)
<template>
<my-dialog :is-show="showLogDialog" @on-close="closeDialog('showLogDialog')">
<!-- my-dialog 插件控制彈窗,父元件綁定is-show屬性傳遞給子元件,并且根據值判斷彈窗是否展示 -->
</my-dialog>
</template>
<script>
import Dialog from './base/dialog'
export default {
data() {
return {
showLogDialog: false //該變量控制視窗是否顯示
}
},
methods: {
closeDialog(attr) {
this[attr] = false
},
showLogin() {
this.showLogDialog = true
}
},
components: {
MyDialog: Dialog // 名稱
}
}
</script>
然後填充插槽内容即可。
11. 往插槽裡面填内容,并且豐富登入後的顯示
先在config/index.js,配置跨域通路路由:
proxyTable: {
'/goods/*': {
target: 'http://hotemotion.fun:3389',
changeOrigin: true
},
'/users/*':{
target: 'http://hotemotion.fun:3389',
changeOrigin: true
}
邏輯:點選登入後應該post一個使用者的資訊(賬号、密碼),檢查使用者的登入狀态。并且get一個carList的資料
登入事件:showSignout() 傳遞使用者名和密碼給後端,判斷正确後顯示登陸後的資訊
退出事件:_getLogout() 将資訊清空
(使用者名和密碼都用了v-model 綁定輸入的資料)
src/components/m-header.vue:
<template>
<div>
<div class="header">
<div class="logo">
<img src="/static/images/logo.jpg">
</div>
<div class="sign">
<p class="signin" v-if="showLoginOut" @click = "showLogin()">登入</p>
<div class="signout" v-if="!showLoginOut">
<span class="sign-name" v-text="userName"></span>
<span class="sign-out" @click="_getLogout">退出</span>
<router-link to="/car" class="sign-car">購物車</router-link>
</div>
</div>
</div>
<my-dialog :is-show="showLogDialog" @on-close="closeDialog('showLogDialog')">
<!-- my-dialog 插件控制彈窗,父元件綁定is-show屬性傳遞給子元件,并且根據值判斷彈窗是否展示 -->
<div class="signin-slot">
<p class="signin-logo">登入:</p>
<form>
<div class="signin-name">
<span class="name-icon icon">1</span>
<input type="input" name="username" placeholder="使用者名" v-model="userName"/>
</div>
<div class="signin-psd">
<span class="pwd-icon icon">2</span>
<input type="password" name="password" placeholder="密碼" v-model="userPwd">
</div>
<button type="button" class="signin-submit" @click="showSignout">登入</button>
</form>
</div>
</my-dialog>
</div>
</template>
<script>
import axios from 'axios'
import Dialog from './base/dialog'
export default {
data() {
return {
showLogDialog: false,
showLoginOut : true,
userName:'',
userPwd:'',
}
},
// 重新整理後能夠保持登入狀态
mounted() {
this._getCheckLogin();
},
methods: {
closeDialog(attr) {
this[attr] = false
},
// 顯示登入視窗
showLogin() {
this.showLogDialog = true
},
//檢查登入狀态
_getCheckLogin() {
axios.get('/users/checkLogin').then((res) => {
if (res.data.status == '0') {
this.showLoginOut = false
this.userName = res.data.result.userName
this.getCartList()
}
})
},
// 登出
_getLogout() {
axios.post('/users/logout').then((res) => {
if (res.data.status == '0') {
this.showLoginOut = true
this.userName = ''
this.userPwd = ''
}
})
},
// 登入
showSignout() {
axios.post('/users/login',{
userName: this.userName,
userPwd: this.userPwd,
}).then((res) => {
if (res.data.status == '0') {
// console.log(res)
this.showLogDialog = false;
this.showLoginOut = false;
}
})
}
},
components: {
MyDialog: Dialog
}
}
</script>
<style type="text/css" scoped>
</style>
此時重新啟動npm run dev,就能夠在控制台看到登入後取得的資料。此時加上頁面樣式,該頁面基本資訊完成。
11. 購物車頁面
學習一個flex的布局技巧: width:1%;
子元件count.vue的運用,以及父元件的傳參
route(路線), router(路由) 傻傻分不清楚。
this,$route.query.addressId (得到路徑上的參數)
this.$router.push() ( 跳轉到某一路由)
12. 使用Vuex管理資料
安裝:
npm install vuex --save
利用Vuex得到商品的數量,用于在購物車中顯示。
src/store/store.js:
state 用來存儲變量的狀态
mutations:記錄變量狀态的變化(setCartCount是設定cartCount初始值,updateCartCount 改變cartCount數值)
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
cartCount:0
},
mutations: {
setCartCount(state,cartCount){
state.cartCount = cartCount;
},
updateCartCount(state,cartCount){
state.cartCount += cartCount;
}
}
});
export default store
src/ components/goods.vue:
_addToCar() 添加到購物車就加一個數。
import store from './../store/store'
export default {
methods: {
_addToCar(productId) {
//post 送出資料
addToCar(productId).then((res) => {
//mmp, 這個狀态碼是字元串
if (res.data.status == '0') {
this.showAddCart = true
// 如果請求成功,資料存入store中
store.commit('updateCartCount', 1)
} else {
this.showErrDialog = true
}
})
},
}
}
src/components/car.vue:
點選按鈕改變數量的時候狀态加1或減1, 删除的時候則删除目前的數量
import store from '../store/store'
export default {
methods: {
editNum(flag, item) {
if (flag === 'min') {
if (item.productNum === 1) {
return
}
item.productNum--
store.commit('updateCartCount', -1)
} else {
item.productNum++
store.commit('updateCartCount', 1)
}
},
delItem() {
axios.post('/users/carDel', {
productId: this.productId
}).then((res) => {
if (res.data.status == '0') {
this.showDelDialog = false
this.getCarList()
store.commit('updateCartCount', -this.productNum)
}
})
}
}
在m-header.vue中使用:
<template>
<router-link to="/car" class="sign-car">購物車{{carCount}}</router-link>
</template>
<script>
import store from '../store/store'
export default {
// 重新整理的時候也檢查登入狀态
mounted() {
this._getCheckLogin();
},
computed: {
carCount() {
return store.state.cartCount
}
},
methods: {
// 登出
_getLogout() {
store.commit('setCartCount', 0)
},
// 獲得商品資料,存入store中
getCartList() {
axios.get('/users/carList').then((res) => {
if (res.data.status == '0') {
let cartCount = 0
const carList = res.data.result
carList.forEach((item) => {
cartCount += item.productNum
// console.log(cartCount)
store.commit('setCartCount', cartCount)
})
}
})
}
}
</script>