小米商城 -- vue项目实战
后端连接已不可用,该项目无效!想看看前端代码还是可以接着阅读!github:小米商城源码账号:sunyu密码:123456该项目是对小米商城系统的模仿,实现了从浏览商品到结算商品的整个过程,其中包括了商品列表、根据价格筛选商品、对商品 排序、登录、加入购物车、结算等功能 前台使用vue-cli构建了请求服务器,使用了Vue框架,还使用了vue-router...
后端连接已不可用,该项目无效!
想看看前端代码还是可以接着阅读!
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 <372842848@qq.com>)
? Author Spock <372842848@qq.com>
? 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>
更多推荐
所有评论(0)