vue2.0+webpack高仿餓了麼商家頁面
一、項目說明
1. 項目技術
- vue cli
- vue
- ES6
- vue-resource
- vue-router
- better-scroll
- sass
- webpack
- git
2. 項目地址:Github
3. 項目演示
- sell
手機掃描二維碼
二、項目初始化
1. 使用vue官網給出的命令行工具
# 全局安裝 vue-cli
$ npm install --global vue-cli
# 創建一個基於 webpack 模板的新項目
$ vue init webpack my-project(sell)
# 安裝依賴,走你
$ cd my-project(sell)
$ npm install
$ npm run dev
2. 初始項目目錄(小部分修改)
├── build // 構建服務和webpack配置 ├── config // 項目不同環境的配置 ├── index.html // 項目入口文件 ├── package.json // 項目配置文件 ├── src // 生產目錄 │ ├── common // 公共的css js 資源 │ ├── components // 各種組件 │ ├── router // 路由 │ ├── App.vue // 主頁面 │ └── main.js // Webpack 預編譯入口
3. 完整項目目錄
接下來開始項目的開發
1. webpack 預編譯入口文件 main.js
import Vue from 'vue';
import App from './App';
import router from './router';
// index.js會自動尋找到,也可以寫全(import router from './router/index.js')
import VueResource from 'vue-resource';
import './common/styles/index.scss';
/* 設置爲 false 以阻止 vue 在啓動時生成生產提示 */
Vue.config.productionTip = false;
Vue.use(VueResource); // 全局註冊
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
});
2. 主頁面 App.vue
(1)先上代碼
<template>
<div id="app">
<v-header v-bind:seller="seller"></v-header>
<div class='tab border-b-1px'>
<div class='item-tab'>
<router-link to="/goods">商品</router-link>
</div>
<div class='item-tab'>
<router-link to="/ratings">評論</router-link>
</div>
<div class='item-tab'>
<router-link to="/seller">商家</router-link>
</div>
</div>
<!--主要用於保留組件狀態或避免重新渲染-->
<keep-alive>
<router-view v-bind:seller="seller"></router-view>
</keep-alive>
</div>
</template>
<script>
import header from './components/header/header.vue';
import {urlParse} from './common/js/util.js';
// import data from './common/json/data.json';
const ERR_OK=0;
export default {
name:'App',
data () {
return {
seller:{
id: (() => {
let queryParam=urlParse();
return queryParam.id;
})()
}
};
},
created (){
this.$http.get('/api/seller?id=' + this.seller.id).then(response => {
if(response.data.errno===ERR_OK){
// 用於對象的合併 , Object.assign方法的第一個參數是目標對象,後面的參數都是源對象。
this.seller=Object.assign({},this.seller,response.data.data);
}else{
console.log('no data');
}
});
// this.seller=Object.assign({},this.seller,data.seller);
},
components:{
'v-header':header
}
};
</script>
<style lang="scss" scoped>
@import "./common/styles/mixin.scss";
#app{
.tab{
display:flex;
width:100%;
height:40px;
line-height:40px;
font-size:14px;
color:rgb(240,20,20);
@include border-b-1px(rgba(7,17,27,0.1));
.item-tab{
flex:1;
text-align:center;
.active{
color:rgb(240,20,20);
}
}
}
}
</style>
(2)代碼關鍵說明
主頁面的結構
- v-header 組件
- 路由渲染組件
功能實現
1) 獲取mock數據
- 在入口文件中main.js 中, 引入 vue-resource ,並全局註冊
import VueResource from 'vue-resource';
Vue.use(VueResource); // 全局註冊
- 在 App.vue 中 ,使用生命週期鉤子 created , 在實例創建完成後被立即調用,請求數據。且需要在data 選項中返回數據對象(其中的id 放在後面介紹)。
data () {
return {
seller:{
id: (() => {
let queryParam=urlParse();
return queryParam.id;
})()
}
};
},
created (){
this.$http.get('/api/seller?id=' + this.seller.id).then(response => {
if(response.data.errno===ERR_OK){
// 用於對象的合併 , Object.assign方法的第一個參數是目標對象,後面的參數都是源對象。
this.seller=Object.assign({},this.seller,response.data.data);
}else{
console.log('no data');
}
});
// this.seller=Object.assign({},this.seller,data.seller);
},
當一個組件被定義,data 必須聲明爲返回一個初始數據對象的函數,因爲組件可能被用來創建多個實例。如果 data 仍然是一個純粹的對象,則所有的實例將共享引用同一個數據對象!通過提供 data 函數,每次創建一個新實例後,我們能夠調用 data 函數,從而返回初始數據的一個全新副本數據對象。
3) 在父組件中使用子組件
- 首先,引入子組件
import header from './components/header/header.vue';
- 然後 ,父組件 通過 components 選項聲明
components:{
'v-header':header
}
- 使用很簡單
<v-header v-bind:seller="seller"></v-header>
4) 父子組件的數據傳遞
- 父組件 App.vue:通過 v-bind:seller=”seller” 將數據傳遞
- 子組件 header.vue :通過 props 接收(只顯示核心代碼)
<template>
...
</template>
<script>
export default{
...
props: {
seller:{
type:Object,
required:true
}
}
...
};
</script>
<style lang="scss" scoped>
...
</style>
這樣, 在子組件中就可以使用父組件的數據了
5) 路由
- 首先我們需要將 路由的定義 單獨放在一個文件中 index.js
import Vue from 'vue';
import Router from 'vue-router';
import goods from '@/components/goods/goods.vue';
import ratings from '@/components/ratings/ratings.vue';
import seller from '@/components/seller/seller.vue';
Vue.use(Router);
const routes=[
{
path: '/',
redirect: 'goods', // 重定向
},
{
path: '/goods',
component: goods
},
{
path: '/ratings',
component: ratings
},
{
path: '/seller',
component: seller
}
];
export default new Router({
// 激活class類名
linkActiveClass:'active',
routes
});
- 然後在 main.js 中 引入router 即 index.js ,並且傳一個路由屬性給Vue實例 ,實現路由的注入
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
});
在 App.vue 中通過 router-link 組件實現
<router-link to="/goods">商品</router-link>
注意:不支持target=”_blank”屬性,如果你想打開一個新標籤頁,你必須用標籤。
最後通過 router-view 組件渲染
<keep-alive>
<router-view v-bind:seller="seller"></router-view>
</keep-alive>
// keep-alive 主要用於保留組件狀態或避免重新渲染
以上就實現了一個簡單的路由
(3) 第一個組件 header.vue
<template>
<div class="header">
<!--內容區-->
<div class="content-wrapper">
<div class="avatar">
<img v-bind:src="seller.avatar" width="64" height='64'/>
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鐘送達
</div>
<div v-if="seller.supports" class="support"><!--判斷是否存在-->
<span class="icon" v-bind:class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
<div v-if="seller.supports" class="support-count" @click="showDetail">
<span class="count">{{seller.supports.length}}個</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
<!--公告區-->
<div class="bulletin-wrapper" @click="showDetail">
<span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
<!--背景-->
<div class="background">
<img v-bind:src="seller.avatar" width="100%" height="100%" />
</div>
<!--遮罩層 ,詳情頁-->
<transition name="fade">
<div class="detail" v-show="detailShow">
<div class="detail-wrapper clearfix">
<div class="detail-main">
<h1 class="name">{{seller.name}}</h1>
<div class="star-wrapper">
<star :size="48" :score="seller.score"></star>
</div>
<div class="title">
<div class="line"></div>
<div class="text">優惠信息</div>
<div class="line"></div>
</div>
<ul v-if="seller.supports" class="supports">
<li class="support-item" v-for="(item ,index) in seller.supports">
<span class="icon" :class="classMap[seller.supports[index].type]"></span>
<span class="text">{{seller.supports[index].description}}</span>
</li>
</ul>
<div class="title">
<div class="line"></div>
<div class="text">商家公告</div>
<div class="line"></div>
</div>
<div class="bulletin">
<p class="content">{{seller.bulletin}}</p>
</div>
</div>
</div>
<div class="detail-close" @click="hideDetail">
<i class="icon-close"></i>
</div>
</div>
</transition>
</div>
</template>
<script>
import star from "../star/star.vue";
export default{
data(){
return{
classMap:[],
detailShow:false
}
},
methods:{
showDetail(){
this.detailShow=true;
},
hideDetail(){
this.detailShow=false;
}
},
props: {
seller:{
type:Object,
required:true
}
},
created (){
this.classMap=['decrease','discount','guarantee','invoice','special'];
},
components:{
star
}
};
</script>
<style lang="scss" scoped>
@import "../../common/styles/mixin.scss";
.header {
position:relative;
overflow:hidden;
color:#fff;
background:rgba(7,17,27,0.5);
.content-wrapper {
position:relative;
padding:24px 12px 18px 24px;
font-size:0;
.avatar {
display:inline-block;
vertical-align:top;
img{
border-radius:2px;
}
}
.content {
display:inline-block;
margin-left:16px;
font-size:14px;
.title {
margin:2px 0 8px 0;
.brand {
display:inline-block;
vertical-align:top;
width:30px;
height:18px;
@include bg-image("img/brand");
background-size:30px 18px;
background-repeat: no-repeat;
}
.name {
margin-left:6px;
font-size:16px;
line-height:18px;
font-weight: bold;
}
}
.description {
margin-bottom: 10px;
line-height:12px;
font-size:12px;
}
.support {
.icon {
display:inline-block;
vertical-align:top;
width:12px;
height:12px;
margin-right: 4px;
background-size: 12px 12px;
background-repeat: no-repeat;
&.decrease {
@include bg-image("img/decrease_1");
}
&.discount {
@include bg-image("img/discount_1");
}
&.guarantee {
@include bg-image("img/guarantee_1");
}
&.invoice {
@include bg-image("img/invoice_1");
}
&.special {
@include bg-image("img/special_1");
}
}
.text{
line-height:12px;
font-size:10px;
}
}
}
.support-count {
position:absolute;
right:12px;
bottom:14px;
padding:0 8px;
height:24px;
line-height: 24px;
border-radius:14px;
background-color:rgba(0,0,0,0.2);
.count{
vertical-align:top;
font-size: 10px;
}
.icon-keyboard_arrow_right{
margin-left: 2px;
line-height:24px;
font-size: 10px;
}
}
}
.bulletin-wrapper{
position:relative;
height:28px;
line-height: 28px;
padding:0 22px 0 12px;
white-space: nowrap;
overflow:hidden;
text-overflow:ellipsis;
background:rgba(7,17,27,0.2);
.bulletin-title{
display:inline-block;
vertical-align:top;
margin-top:8px;
width:22px;
height:12px;
@include bg-image("img/bulletin");
background-size:22px 12px;
background-repeat:no-repeat;
}
.bulletin-text{
vertical-align:top;
margin:0 4px;
font-size:10px;
}
.icon-keyboard_arrow_right{
position:absolute;
font-size:10px;
right:12px;
top:8px;
}
}
.background{
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
background:red;
z-index:-1;
filter:blur(10px);
}
.detail{
position:fixed;
z-index:100;
top:0;
left:0;
width:100%;
height:100%;
overflow:auto;
background:rgba(7,17,27,0.8);
backdrop-filter:blur(10px);
transition: all 0.5s;
&.fade-enter,&.fade-leave-to{
opacity:0;
}
.detail-wrapper{
width:100%;
min-height: 100%;
.detail-main{
margin-top:64px;
padding-bottom:64px;
.name{
line-height:16px;
text-align:center;
font-size:16px;
font-weight: 700;
}
.star-wrapper{
margin-top:16px;
padding:2px 0;
text-align:center;
}
.title{
display:flex;
width:80%;
margin:28px auto 24px auto;
.line{
flex:1;
position:relative;
top:-6px;
border-bottom:1px solid rgba(255,255,255,0.2);
}
.text{
padding:0 12px;
font-size:15px;
font-weight:700;
}
}
.supports{
width:80%;
margin:0 auto;
.support-item{
padding:0 12px;
margin-bottom:12px;
font-size:0px;
&:last-child{
margin-bottom:0;
}
.icon{
display:inline-block;
vertical-align: top;
width:16px;
height:16px;
margin-right:6px;
background-size:16px 16px;
background-repeat: no-repeat;
&.decrease {
@include bg-image("img/decrease_2");
}
&.discount {
@include bg-image("img/discount_2");
}
&.guarantee {
@include bg-image("img/guarantee_2");
}
&.invoice {
@include bg-image("img/invoice_2");
}
&.special {
@include bg-image("img/special_2");
}
}
.text{
line-height:16px;
font-size:12px;
}
}
}
.bulletin{
width:80%;
margin:0 auto;
.content{
padding:0 12px;
line-height:24px;
font-size:12px;
}
}
}
}
.detail-close{
position:relative;
width:32px;
height:32px;
margin:-64px auto 0 auto;
font-size:32px;
}
}
}
</style>
主要知識點
指令
- v-if :根據表達式的值的真假條件渲染元素
- v-for :基於源數據多次渲染元素或模板塊
- v-bind :動態地綁定一個或多個特性,或一個組件 prop 到表達式
- v-on :綁定事件監聽器
- 修飾符
- stop:阻止事件冒泡
- prevent:阻止默認事件
v-on:click.stop="doThis"
過渡動畫 :transition 組件
- 選項 :methods,created
功能實現
1)動畫 : 利用 transition 組件
- 在需要實現動畫的容器最外層 包裹 transition 組件,並且指定name 屬性
<transition name="fade">
...
</transition>
- 在css 中制定樣式
transition: all 0.5s;
&.fade-enter,&.fade-leave-to{
opacity:0;
}