Vue综合练习
通过前面学习了Vue等知识、我们使用Vue写一个简单的小项目
一位大佬写的很详细: https://blog.csdn.net/wuyxinu/article/details/103684950
基本构建
使用Vue-Cli4.x
创建项目
记得安装 Vuex
、Vue-Router
之后安装 axios: npm install -s axios
项目基本目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ├── public 用于存放静态资源 │ ├── favicon.ico 图标资源 │ └── index.html 是一个模板文件,作用是生成项目的入口文件 ├── src 项目源码目录 │ ├── main.js 入口js文件 │ ├── App.vue 根组件 │ ├── components 公共组件目录 │ │ └── common 在其他项目下也可以使用的 │ │ └── contents 业务相关组件 │ ├── common 工具 │ ├── assets 资源目录,这里的资源会被wabpack构建 │ │ └── img │ │ └── css │ ├── network 封装网络请求 │ │ └── request.js │ ├── routes 前端路由 │ │ └── index.js │ ├── store Vuex应用级数据(state) │ │ └── index.js │ └── views 页面目录 └── package.json npm包配置文件,里面定义了项目的npm脚本,依赖包等信息
|
配置别名
在根目录下创建vue.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = { configureWebpack: { resolve:{ extensions:[], alias:{ 'assets':'@/assets', 'components':'@/components', 'network':'@/network', 'views':'@/views', } } } }
|
在根目录下创建.editorconfig
文件用来规范缩进
等…
1 2 3 4 5 6 7 8 9
| root = true
[*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true
|
CSS导入
在src/assets/css
下创建normalize.css
内容为: https://github.com/necolas/normalize.css/blob/master/normalize.css
创建base.css
内容为: https://github.com/maclxf/supermall/blob/master/src/assets/css/base.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| @import "./normalize.css";
:root { --color-text: #666; --color-high-text: #ff5777; --color-tint: #ff8198; --color-background: #fff; --font-size: 14px; --line-height: 1.5; }
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; user-select: none; -webkit-tap-highlight-color: transpanett; background: var(--color-background); color: var(--color-text); width: 100vw; }
a { color: var(--color-text); text-decoration: none; }
.clear-fix::after { clear: both; content: ''; display: block; width: 0; height: 0; visibility: hidden; }
.clear-fix { zoom: 1; }
.left { float: left; }
.right { float: right; }
|
在App.vue
中导入
1 2 3 4
| <style> @import './assets/css/base.css'; </style>
|
基本页面
复制2020\07\Vue\supermall\public\favicon.ico
下的所有文件到当前项目public\favicon.ico
下
复制2020\07\Vue\supermall\src\assets\img
下的所有文件到当前项目src\assets\img
下
在src\views
下创建
在src\views\cart
下创建
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <h2>购物车</h2> </template>
<script> export default { name: "Cart" } </script>
<style scoped>
</style>
|
在src\views\category
下创建
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <h2>品类</h2> </template>
<script> export default { name: "Category" } </script>
<style scoped>
</style>
|
在src\views\home
下创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <h2>我是主页</h2> </template>
<script> export default { name: "Home", components: {}, data() { return {} }, computed: {}, created() {
}, methods: {} } </script>
<style scoped>
</style>
|
在src\views\me
下创建
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <h2>关于我</h2> </template>
<script> export default { name: "Me" } </script>
<style scoped>
</style>
|
之后配置路由router\index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import Vue from 'vue' import VueRouter from 'vue-router'
const Home = () => import('views/home/Home') const Category = () => import('views/category/Category') const Cart = () => import('views/cart/Cart') const Me = () => import('views/me/Me')
const originalPush = VueRouter.prototype.push VueRouter.prototype.push = function push(location) { return originalPush.call(this, location).catch(err => err) }
Vue.use(VueRouter)
const routes = [ { path: '', redirect: '/home' }, { path: '/home', component: Home }, { path: '/category', component: Category }, { path: '/cart', component: Cart }, { path: '/me', component: Me } ]
const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes })
export default router
|
复制: https://github.com/maclxf/supermall/tree/master/src/components/common/tabbar 里的内容到 \src\components\common\tabbar
最后在src\App.vue
编辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <template> <div id="app"> <router-view/> <main-tar-bar/> </div> </template>
<script>
import MainTarBar from "./components/contents/maintarbar/MainTarBar";
export default { name: "App", data(){ return{
} }, components:{ MainTarBar } } </script>
<style> @import 'assets/css/base.css';
</style>
|
通过npm run serve
测试效果
首页开发
导航组件
顶部文字显示区域
在\src\components\common\navbar
下创建NavBar.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <template> <div class="nav-bar"> <div class="left"><slot name="left"></slot></div> <div class="center"><slot name="center"></slot></div> <div class="right"><slot name="right"></slot></div> </div> </template>
<script> export default { name: "NavBar.vue" } </script>
<style scoped>
.nav-bar{ display: flex; height: 44px; line-height: 44px; text-align: center; }
.left{ width: 60px; }
.right{ width: 60px; }
.center{ flex: 1; }
</style>
|
编辑src\views\home\Home.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template> <div id="home"> <NavBar class="home-nav-bar"> <div slot="center">购物街</div> </NavBar> </div> </template>
<script>
import NavBar from "components/common/navbar/NavBar";
export default { name: "Home", components: { NavBar }, } </script>
<style scoped> #home{ padding-top: 44px; height: 100vh; position: relative; } .home-nav-bar{ background-color: var(--color-tint); color: #fff; box-shadow: 0 1px 1px 1px rgba(100,100,100,.1); position: fixed; left: 0; top: 0; z-index: 9; } </style>
|
数据请求
在src\network
下创建request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import axios from 'axios';
export function request(config) { const instance = axios.create({ baseUrl: "http://123.207.32.32:8000", timeout: 5000 })
instance.interceptors.request.use(config => { return config; }, err => { console.log(err) })
instance.interceptors.response.use(response => { return response.data }, err => { console.log(err) }) return instance(config); }
|
之后创建home.js
1 2 3 4 5 6 7
| import { request } from './request'
export function getHomeMultiData() { return request({ url: "/home/multidata" }) }
|
在Home.vue
编辑、主要是注意在created()
函数中发起请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <template> <div> ... </div> </template>
<script> import {getHomeMultiData} from "../../network/home";
export default { name: "Home", data() { return { banners: [], recommends: [] } }, created() { this.getHomeMultiData(); }, methods:{ getHomeMultiData(){ getHomeMultiData().then(res=>{ this.banners = res.data.banner.list; this.recommends = res.data.recommend.list; }) } } } </script>
|
轮播图组件
使用的是作者封装好的组件: https://github.com/maclxf/supermall/tree/master/src/components/common/swiper 放在src\components\common
下
为了使Home里的组件不那么复杂、我们需要提取一下、在src\views\home
下创建childComps文件夹
、里面创建HomeSwiper.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <template> <swiper class="home-swiper"> <swiper-item v-for="item in banners" :key="item.link"> <a :href="item.link"> <img :src="item.image" alt=""> </a> </swiper-item> </swiper> </template>
<script> import {Swiper, SwiperItem} from 'components/common/swiper';
export default { name: "HomeSwiper", props: { banners: { type: Array, default() { return [] } } }, components: { Swiper, SwiperItem } } </script>
<style scoped>
</style>
|
一个轮播图就导入成功了。
在Home.vue
添加
每日推荐组件
同样为了使Home里的组件不那么复杂、我们需要提取一下、在src\views\home\childComps
里面创建HomeRecommend.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| <template> <div class="home-recommend"> <div class="home-recommend-item" v-for="item in recommends" :key="item.title"> <a :href="item.link"> <img :src="item.image" alt=""> <span>{{ item.title }}</span> </a> </div> </div> </template>
<script> export default { name: "HomeRecommend", props: { recommends: { type: Array, default() { return []; } } } } </script>
<style scoped> .home-recommend{ display: flex; text-align: center; font-size: 12px; width: 100%; padding: 5px; border-bottom: 10px solid #eee; }
.home-recommend .home-recommend-item{ flex: 1; }
.home-recommend-item img{ width: 70px; height: 70px; border-radius: 100%; }
.home-recommend-item span{ display: block; } </style>
|
之后统一写到Home.vue
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <template> <div id="home"> ... <HomeSwiper :banners="banners"/> ... </div> </template>
<script> import HomeSwiper from "./childComps/HomeSwiper"; import {getHomeMultiData} from "network/home";
export default { name: "Home", components: { HomeSwiper }, data() { return { banners: [], } }, created() { this.getHomeMultiData(); }, methods:{ getHomeMultiData(){ getHomeMultiData().then(res=>{ this.banners = res.data.banner.list; this.recommends = res.data.recommend.list; }) } } } </script>
|
本周流行组件
同样为了使Home里的组件不那么复杂、我们需要提取一下、在src\views\home\childComps
里面创建HomeFeatureView.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div class="home-feature"> <a href="https://act.mogujie.com/zzlx67"> <img src="~assets/img/home/recommend_bg.jpg" alt=""> </a> </div> </template>
<script> export default { name: "HomeFeatureView" } </script>
<style scoped> .home-feature img{ width: 100%; } </style>
|
之后写到Home.vue
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <template> <div id="home"> ... <HomeRecommend :recommends="recommends"/> ... </div> </template>
<script> import HomeRecommend from "./childComps/HomeRecommend";
import {getHomeMultiData} from "network/home";
export default { name: "Home", components: { HomeRecommend }, data() { return { recommends: [] } }, created() { this.getHomeMultiData(); }, methods:{ getHomeMultiData(){ getHomeMultiData().then(res=>{ this.banners = res.data.banner.list; this.recommends = res.data.recommend.list; }) } } } </script>
|
TabControl组件
在src\component\tabcontrol\
里面创建TabControl.vue
通过索引index
与curnettIndex
来判断是否选中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| <template> <div class="tab-control"> <div v-for="(item,index) in titles" :key="item" class="tab-control-item" @click="tabClick(index)"> <span :class="{ active: index == curnettIndex }" >{{ item }}</span> </div> </div> </template>
<script> export default { name: "TabControl", data() { return { curnettIndex : 0 } }, props: { titles: { type: Array, data() { return []; } } }, methods:{ tabClick(index){ this.curnettIndex = index; this.$emit("tabClick",index); } } } </script>
<style scoped> .tab-control{ display: flex; text-align: center; line-height: 40px; height: 40px; font-size: 15px; background-color: #fff; }
.tab-control-item{ flex: 1; }
.tab-control-item span{ padding: 5px; }
.active{ border-bottom: 2px solid var(--color-tint); color: var(--color-high-text); }
</style>
|
编辑Home.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <template> <div id="home"> ... <tab-control class="home-tab-control" :titles="['流行', '新款', '精选']" /> </div> </template>
<script>
import TabControl from "components/contents/tabcontrol/TabControl";
export default { name: "Home", components: { TabControl }, } </script>
<style> .home-tab-control{ position: sticky; top: 44px; z-index: 9; } </style>
|
后面发现position: sticky;
没有作用了。。。不知道什么原因
商品数据封装
数据
我们需要这样的一个数据结构来存储商品数据
pop、new、sell
代表商品、page
代表页数、list
代表商品、curnettType
当前类型
1 2 3 4 5 6
| goods: { 'pop': {page: 0, list: []}, 'new': {page: 0, list: []}, 'sell': {page: 0, list: []}, }, curnettType: "pop"
|
network
编辑src\network\home.js
1 2 3 4 5 6 7 8 9
| export function getHomeGoods(type,page) { return request({ url: "/data", params:{ type, page } }) }
|
views
最后编辑Home.vue
注意:
import {getHomeGoods} from "network/home";
导入、不要忘记
往数组里添加数据使用push方法
、然而我们需要添加另一个数组的数据我们需要使用扩展运算符...
如下面this.goods[type].list.push(...res.data.list)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| <template> <div id="home"> ... <tab-control class="home-tab-control" :titles="['流行', '新款', '精选']" @tabClick="tabClick"/> <good-list :goods="showGoods"/>
</div> </template> <script> import {getHomeMultiData ,getHomeGoods} from "network/home";
export default { name: "Home", data() { return { goods: { 'pop': {page: 0, list: []}, 'new': {page: 0, list: []}, 'sell': {page: 0, list: []} }, curnettType: "pop" } }, computed: { showGoods(){ return this.goods[this.curnettType].list } }, created() { this.getHomeMultiData(); this.getHomeGoods("pop"); this.getHomeGoods("new"); this.getHomeGoods("sell"); }, methods: {
tabClick(index) { switch (index) { case 0: this.curnettType = "pop" break case 1: this.curnettType = "new" break case 2: this.curnettType = "sell" break } },
getHomeMultiData() { getHomeMultiData().then(res => { this.banners = res.data.banner.list; this.recommends = res.data.recommend.list; }) }, getHomeGoods(type) { let page = this.goods[type].page + 1; getHomeGoods(type, page).then(res => { this.goods[type].list.push(...res.data.list) this.goods[type].page += 1; }) } } } </script>
|
Batter-Srcoll
一个更好的解决移动端滚动问题的框架、官网: https://ustbhuangyi.github.io/better-scroll/#/zh
基本使用
安装
1
| npm install better-scroll --save
|
在Vue中使用
我们需要在标签里包裹另一个标签
如:div标签包裹ul标签
我们需要在mounted
使用BScroll
第一个参数: 包裹ul标签的div
第二个参数: 绑定参数、如:probeType
、值为0时不派发 scroll 事件、值为1时屏幕滑动超过一定时间后派发scroll 事件;值为2时在屏幕滑动的过程中实时的派发 scroll 事件(如滑到底部时的动画然不会有事件)、值为3时屏幕滑动的过程中,滚动动画运行过程中实时派发 scroll 事件(如滑到底部时的动画然会有事件)
div
需要高度、否则不起作用
通过on
方法绑定参数
绑定pullingUp
参数时需要添加pullUpLoad: true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <template> <div class="wrapper" ref="wrapper"> <ul class="content"> <li>1</li> ... <li>8</li> <li>9</li> <li>10</li> ... <li>25</li> </ul> </div> </template>
<script>
import BScroll from 'better-scroll'
export default { data() { return { scroll: null } }, mounted() { this.scroll = new BScroll(this.$refs.wrapper, { probeType: 2, pullUpLoad: true });
this.scroll.on('scroll',(position)=>{ console.log(position) })
this.scroll.on('pullingUp',()=>{ console.log("上拉刷新") }) } } </script>
<style scoped> .wrapper { height: 200px; background-color: red; overflow: hidden; }
</style>
|
简单封装
功能:
在src\components\common\scroll
下创建Scroll
probeType
值不为0时、才会监听scroll
事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| <template> <div class="wrapper" ref="wrapper"> <div class="content"> <slot></slot> </div> </div> </template>
<script>
import BScroll from "better-scroll"
export default { name: "Scroll", props:{ probeType: { type: Number, default: 0 }, pullUpLoad: { type: Boolean, default: false } }, data() { return { scroll: null } }, mounted() { this.scroll = new BScroll(this.$refs.wrapper, { probeType: this.probeType, pullUpLoad: this.pullUpLoad, click: true }) this.scroll.on("scroll",( position =>{ this.$emit('scroll',position) })) this.scroll.on('pullingUp',()=>{ this.$emit("pullingUp") }) }, methods: { scrollTo(x, y, time = 500) { this.scroll.scrollTo(x, y, time) } finishPullUp(){ this.scroll.finishPullUp() } refresh() { this.scroll.refresh() } } } </script>
<style scoped>
</style>
|
backtop
在src\components\contents\backtop
下创建BackTop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <template> <div class="back-top"> <img src="~assets/img/common/top.png" alt=""> </div> </template>
<script> export default { name: "BackTop" } </script>
<style scoped>
.back-top{ position: fixed; bottom: 53px; right: 3px; } .back-top img{ width: 50px; height: 50px; }
</style>
|
使用
- 通过绝对定位
scroll
组件就可以不用指定高度来显示滚动区域了、或者使用height:calc(100%-93px); 但是又bug
ref="refname"
可以绑定组件、this.$refs.refname
使用- 自定义组件本身不能添加自定义事件、需要添加事件修饰符
.native
this.isShowBackTop = -position.y > 1000
判断 滑动距离大于1000时才会显示BackTop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| <template> <div id="home"> ... <scroll class="wrapper" ref="scroll" @scroll="contentScroll" :probeType="3" :pullUpLoad="true" @pullingUp="loadMore"> <HomeSwiper :banners="banners"/> <HomeRecommend :recommends="recommends"/> <HomeFeatureView/> <tab-control class="home-tab-control" :titles="['流行', '新款', '精选']" @tabClick="tabClick"/> <good-list :goods="showGoods"/> </scroll> <BackTop @click.native="topClick" v-show="isShowBackTop"/> </div> </template>
<script>
import Scroll from "components/common/scroll/Scroll"; import BackTop from "components/contents/backtop/BackTop";
export default { name: "Home", components: { GoodList, Scroll,BackTop }, data(){ return{ isShowBackTop:false } } methods: {
topClick(){ console.log(this.$refs.scroll.scroll) this.$refs.scroll.scrollTo(0,0) }, contentScroll(position){ this.isShowBackTop = -position.y > 1000 }, loadMore(){ this.getHomeGoods(this.curnettType) }, getHomeGoods(type) { let page = this.goods[type].page + 1; getHomeGoods(type, page).then(res => { this.goods[type].list.push(...res.data.list) this.goods[type].page += 1; this.$refs.scroll.finishPullUp(); }) } } } </script>
<style scoped> #home { height: 100vh; position: relative; }
.home-nav-bar { background-color: var(--color-tint); color: #fff;
position: fixed; left: 0; right: 0; top: 0; z-index: 9;
}
.home-tab-control { position: sticky; top: 44px; z-index: 8; }
.wrapper{ position: absolute; top: 44px; bottom: 49px; right: 0; left: 0; }
</style>
|
问题
对于非父子组件通信来说我们需要使用集中式的事件中间件:Bus
。
在组件中,可以使用$emit, $on, $off
分别来分发、监听、取消监听
事件
需要在main.js中注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store'
Vue.config.productionTip = false
Vue.prototype.$bus = new Vue()
new Vue({ router, store, netder: h => h(App) }).$mount('#app')
|
编辑GoodListItem.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <template> <div class="goods-item"> <img :src="goodItem.showLarge.img" alt="" @load="imageLoad"> <div class="goods-info"> <p>{{ goodItem.title }}</p> <span class="price">{{goodItem.price}}</span> <span class="collect">{{ goodItem.cfav }}</span> </div> </div> </template>
<script> export default { name: "GoodListItem", props: { goodItem: { type: Object, default() { return {} } } }, methods:{ imageLoad(){ this.$bus.$emit('imageLoad'); } } } </script>
|
Home.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <template> <div id="home"> ... </div> </template>
<script>
export default { name: "Home", components: { }, mounted() { this.$bus.$on('imageLoad',()=>{ this.$refs.scroll.refresh() }) }, } </script>
|
防抖动
浅谈js防抖和节流
1 2 3 4 5 6 7 8 9 10 11
| function debounce(fn,delay){ let timer = null return function() { if(timer){ clearTimeout(timer) } timer = setTimeout(fn,delay) } } window.onscroll = debounce(showTop,1000)
|
刷新频繁找不到refresh
的解决办法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template> <div id="home"> ... </div> </template>
<script> export default { name: "Home", mounted() { const refresh = this.debounce(this.$refs.scroll.refresh,50) this.$bus.$on('imageLoad', () => { refresh() }) }, methods: {
debounce(func, delay) { let timer = null; return function (...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => { func.apply(this, args) }, delay) } }, } } </script>
|
我们可以把函数封装到src\common\utils.js
里
tabControl的吸顶效果
获取到tabControl的offsetTop
必须知道滚动到多少时,开始有吸顶效果,这个时候就需要获取tabControl的offsetTop
但是,如果直接在mounted中获取tabControl的offsetTop,那么值是不正确.
如何获取正确的值了?
- 监听HomeSwiper中img的加载完成.
- 加载完成后,发出事件,在Home.vue中,获取正确的值.
补充:
- 为了不让HomeSwiper多次发出事件,
- 可以使用isLoad的变量进行状态的记录.
- 注意:这里不进行多次调用和debounce的区别
监听滚动,动态的改变tabControl的样式
- 问题动态的改变tabControl的样式时,会出现两个问题:
- 问题一:下面的商品内容,会突然上移
- 问题二: tabControl虽然设置了fixed,但是也随着Better-Scroll-起滚出去了.
其他方案来解决停留问题.
- 在最上面,多复制了一份PlaceHolderTabControl组件对象,利用它来实现停留效果.
- 当用户滚动到一定位置时, PlaceHolderTabControl显示出来.
- 当用户滚动没有达到- -定位置时, PlaceHolderTabControl隐藏起来.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| <template> <div id="home"> <NavBar class="home-nav-bar"> <div slot="center">购物街</div> </NavBar> <tab-control class="home-tab-control" :titles="['流行', '新款', '精选']" @tabClick="tabClick" ref="tabControl1" v-show="isFixed"/> <scroll ref="scroll" @scroll="contentScroll" :probeType="3" :pullUpLoad="true" @pullingUp="loadMore"> <HomeSwiper :banners="banners" @swiperImageLoad="swiperLoad" ref="swiper"/> <HomeRecommend :recommends="recommends"/> <HomeFeatureView/> <tab-control :titles="['流行', '新款', '精选']" @tabClick="tabClick" ref="tabControl2"/> <good-list :goods="showGoods"/> </scroll> <BackTop @click.native="topClick" v-show="isShowBackTop"/> </div> </template>
<script>
import NavBar from "components/common/navbar/NavBar"; import TabControl from "components/contents/tabcontrol/TabControl"; import Scroll from "components/common/scroll/Scroll";
import BackTop from "components/contents/backtop/BackTop"; import GoodList from "../../components/contents/good/GoodList"; import HomeSwiper from "./childComps/HomeSwiper"; import HomeRecommend from "./childComps/HomeRecommend"; import HomeFeatureView from "./childComps/HomeFeatureView";
import {getHomeMultiData, getHomeGoods} from "network/home"; import {debounce} from "common/utils";
export default { name: "Home", components: { GoodList, NavBar, TabControl, Scroll, HomeSwiper, HomeRecommend, HomeFeatureView, BackTop }, data() { return { tabOffsetTop: 0, isFixed: false } }, methods: {
tabClick(index) { switch (index) { case 0: this.curnettType = "pop" break case 1: this.curnettType = "new" break case 2: this.curnettType = "sell" break } this.$refs.tabControl1.curnettIndex = index; this.$refs.tabControl2.curnettIndex = index; }, contentScroll(position) { this.isShowBackTop = (-position.y) > 1000 this.isFixed = (-position.y) > this.tabOffsetTop }, swiperLoad() { this.tabOffsetTop = this.$refs.tabControl2.$el.offsetTop }, } } </script>
<style scoped> #home { height: 100vh; position: relative; }
.home-nav-bar { background-color: var(--color-tint); color: #fff; }
.home-tab-control { position: relative; background-color: #fff; z-index: 9; margin-top: -1px; }
.wrapper{ overflow: hidden; position: absolute; top: 44px; bottom: 49px; right: 0; left: 0; }
</style>
|
让Home保持原来的状态
- 让Home不要随意销毁掉
keep-alive
(可能使用还会有问题、但是我测试时候没问题)
- 让Home中的内容保持原来的位置
- 离开时,保存一一个位置信息TopY,
- 进来时,将位置设置为原来保存的位置TopY信息即可.
编辑App.app
1 2 3 4 5 6 7 8
| <template> <div id="app"> <keep-alive exclude="Detail"> <router-view/> </keep-alive> <main-tar-bar/> </div> </template>
|
编辑Home.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script> export default { data() { return { TopY: 0 } }, activated() { this.$refs.scroll.scrollTo(0,this.TopY,0); this.$refs.scroll.refresh() }, deactivated() { this.TopY = this.$refs.scroll.getTopY(); } } </script>
|
详情页开发
在src/views/detail
下创建Detail.vue
编辑src/router/index.js
1 2 3 4 5 6 7 8 9 10 11
| const Detail = ()=>import('views/detail/Detail')
const routes = [ { path: '/detail/:iid', component: Detail } ]
|
再编辑src/components/contents/good/GoodListItem.vue
给goods-items
添加一个点击事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div class="goods-item" @click="clickDetail"> ... </div> </template>
<script> export default { methods:{ clickDetail(){ this.$router.push("/detail/"+ this.goodItem.iid) } } } </script>
|
封装顶部导航栏
在src/views/detail/childComps
下创建DetailNav.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| <template> <div> <NavBar> <div slot="left"> <div class="back" @click="back"> <img src="~assets/img/common/back.svg" alt=""> </div> </div> <div slot="center" class="detail"> <div v-for="(item,index) in titles" class="detail-item" :class="{active: curnettIndex == index}" @click="itemClick(index)"> {{ item }} </div> </div> </NavBar> </div> </template>
<script>
import NavBar from "components/common/navbar/NavBar";
export default { name: "DetailNavBar", components:{ NavBar }, data(){ return{ titles: ['商品','参数','评论','推荐'], curnettIndex: 0 } }, methods:{ itemClick(index){ this.curnettIndex = index }, back(){ this.$router.go(-1); } } } </script>
<style scoped> .detail{ display: flex; font-size: 15px; }
.detail-item{ flex:1; }
.active{ color: var(--color-high-text); }
.back img{ vertical-align: middle; } </style>
|
轮播图展示
在src/network
下创建detail.js
1 2 3 4 5 6 7 8 9 10
| import { request } from "./request";
export function getDetail(iid) { return request({ url: '/detail', params:{ iid } }) }
|
在src/views/detail/childComps
下创建DetailSwiper.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <template> <div class="detail-swiper"> <swiper> <swiper-item v-for="(item,index) in banners" :key="index" class="detail-swiper"> <img :src="item" alt=""> </swiper-item> </swiper> </div> </template>
<script> import {Swiper, SwiperItem} from '@/components/common/swiper/index'
export default { name: "DetailSwiper", components: { Swiper,SwiperItem }, props:{ banners: { type:Array, default(){ return [] } } } } </script>
<style scoped> .detail-swiper{ height:300px; overflow: hidden; } </style>
|
基本信息
编辑src/network/detail.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export class Goods { constructor(itemInfo, columns, services) { this.title = itemInfo.title; this.desc = itemInfo.desc; this.newPrice = itemInfo.price; this.lowNowPrice = itemInfo.lowNowPrice; this.oldPrice = itemInfo.oldPrice; this.discount = itemInfo.discountDesc; this.discountBgColor = itemInfo.discountBgColor; this.columns = columns; this.services = services; this.realPrice = itemInfo.lowNowPrice; } }
|
在src/views/detail/childComps
下创建DetailBaseInfo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| <template> <div class="base-info" v-if="Object.keys(goods).length !== 0"> <div class="info-title">{{ goods.title }}</div> <div class="info-price"> <span class="n-price">{{ goods.newPrice }}</span> <span v-if="goods.oldPrice" class="o-price">{{ goods.oldPrice }}</span> <span :style="{ backgroundColor: goods.discountBgColor }" class="discount" v-if="goods.discount" > {{ goods.discount }} </span> </div> <div class="info-other"> <span>{{ goods.columns[0] }}</span> <span>{{ goods.columns[1] }}</span> <span>{{ goods.services[goods.services.length - 1].name }}</span> </div> <div class="info-service"> <span :key="index" class="info-service-item" v-for="index in goods.services.length - 1" v-if="goods.services[index - 1].icon" > <img :src="goods.services[index - 1].icon" alt="" /> <span>{{ goods.services[index - 1].name }}</span> </span> </div> </div> </template>
<script> export default { name: "DetailBaseInfo", props: { goods: { type: Object, default() { return {}; } } } }; </script>
<style scoped> .base-info { width: 100%; margin-top: 15px; padding: 0 10px; color: #999999; border-bottom: 5px solid #f2f5f8; }
.info-title { text-align: justify; color: #222222; }
.info-price { margin-top: 10px; }
.info-price .n-price { font-size: 24px; color: #ff5777; }
.info-price .o-price { font-size: 13px; margin-left: 5px; text-decoration: line-through; }
.info-price .discount { font-size: 12px; position: relative; top: -4px; margin-left: 5px; padding: 3px 6px; color: #ffffff; border-radius: 8px; background-color: #ff5777; }
.info-other { font-size: 13px; line-height: 30px; display: flex; justify-content: space-between; margin-top: 15px; border-bottom: 1px solid rgba(100, 100, 100, 0.1); }
.info-service { line-height: 60px; display: flex; justify-content: space-between; }
.info-service-item img { position: relative; top: 2px; width: 14px; height: 14px; }
.info-service-item span { font-size: 13px; margin-left: 5px; color: #333333; } </style>
|
店铺信息
编辑src/network/detail.js
1 2 3 4 5 6 7 8 9 10 11
| export class Shop { constructor(shopInfo) { this.logo = shopInfo.shopLogo; this.name = shopInfo.name; this.fans = shopInfo.cFans; this.sells = shopInfo.cSells; this.score = shopInfo.score; this.goodsCount = shopInfo.cGoods; } }
|
在src/views/detail/childComps
下创建DetailShopInfo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
| <template> <div class="shop-info" v-if="Object.keys(shop).length !== 0"> <div class="shop-top"> <img :src="shop.logo" alt="" v-if="shop.logo" /> <span class="title">{{ shop.name }}</span> </div> <div class="shop-middle"> <div class="shop-middle-item shop-middle-left"> <div class="info-sells"> <div class="sells-count"> {{ shop.sells | sellCountFilter }} </div> <div class="sells-text">总销量</div> </div> <div class="info-goods"> <div class="goods-count"> {{ shop.goodsCount }} </div> <div class="goods-text">全部宝贝</div> </div> </div> <div class="shop-middle-item shop-middle-right"> <table> <tr :key="index" v-for="(item, index) in shop.score"> <td>{{ item.name }}</td> <td :class="{ 'score-better': item.isBetter }" class="score"> {{ item.score }} </td> <td :class="{ 'better-more': item.isBetter }" class="better"> <span>{{ item.isBetter ? "高" : "低" }}</span> </td> </tr> </table> </div> </div> <div class="shop-bottom"> <div class="enter-shop">进店逛逛</div> </div> </div> </template>
<script> export default { name: "DetailShopInfo", props: { shop: { type: Object, default() { return {}; } } }, filters: { sellCountFilter(value) { if (value < 10000) return value; return (value / 10000).toFixed(1) + "万"; } } }; </script>
<style scoped> .shop-info { padding: 25px 8px; border-bottom: 5px solid #f2f5f8; }
.shop-top { line-height: 45px; display: flex; align-items: center; }
.shop-top img { width: 45px; height: 45px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 50%; }
.shop-top .title { margin-left: 10px; vertical-align: center; }
.shop-middle { display: flex; align-items: center; margin-top: 15px; }
.shop-middle-item { flex: 1; }
.shop-middle-left { display: flex; justify-content: space-evenly; text-align: center; color: #333333; border-right: 1px solid rgba(0, 0, 0, 0.1); }
.sells-count, .goods-count { font-size: 18px; }
.sells-text, .goods-text { font-size: 12px; margin-top: 10px; }
.shop-middle-right { font-size: 13px; color: #333333; }
.shop-middle-right table { width: 120px; margin-left: 30px; }
.shop-middle-right table td { padding: 5px 0; }
.shop-middle-right .score { color: #5ea732; }
.shop-middle-right .score-better { color: #f13e3a; }
.shop-middle-right .better span { padding: 3px; text-align: center; color: #ffffff; background-color: #5ea732; }
.shop-middle-right .better-more span { background-color: #f13e3a; }
.shop-bottom { margin-top: 10px; text-align: center; }
.enter-shop { font-size: 14px; line-height: 30px; display: inline-block; width: 150px; height: 30px; text-align: center; border-radius: 10px; background-color: #f2f5f8; } </style>
|
商品信息
编辑src/network/detail.js
1 2 3 4 5 6 7 8 9 10 11
| export class Shop { constructor(shopInfo) { this.logo = shopInfo.shopLogo; this.name = shopInfo.name; this.fans = shopInfo.cFans; this.sells = shopInfo.cSells; this.score = shopInfo.score; this.goodsCount = shopInfo.cGoods; } }
|
在src/views/detail/childComps
下创建DetailGoodsInfo.vue
- data中
counter
为计算图片加载个数
- data中
detailLength
为获取的数据detailInfo
中detailImage
数组的长度 - 通过侦听属性
watch
监听detailImage
数组的长度、提高性能 - 当counter等于detailImage时才向父组件发送
imgLoad
事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| <template> <div v-if="Object.keys(detailInfo).length !== 0"> <div class="info-text-wrap"> <div class="text-top-style"></div> <div class="desc info-text-desc">{{detailInfo.desc}}</div> <div class="text-bot-style"></div> </div> <div class="img-list-wrap" v-for="item in detailInfo.detailImage" :key="item.id"> <div class="desc">{{item.key}}</div> <div v-for="(item, index) in item.list" :key="index"> <img :src="item" alt="" class="img" @load="imgLoad"> </div> </div> </div> </template>
<script> export default { name: 'DetailGoodsInfo', props: { detailInfo: { type: Object, default() { return {} } } }, data() { return { counter: 0, detailLength: 0 } }, methods: { imgLoad() { if (++this.counter == this.detailLength) this.$emit('imgLoad') } }, watch: { detailInfo() { this.detailInfo.detailImage.forEach((item,index)=>{ this.detailLength += this.detailInfo.detailImage[index].list.length }) } } } </script>
<style scoped>
.info-text-wrap { position: relative; }
.info-text-wrap .text-top-style { width: 60px; height: 1px; background-color: #333; margin-left: 4px; }
.info-text-wrap .text-top-style::before { position: absolute; left: 4px; top: -2.5px; display: block; content: ''; width: 5px; height: 5px; background-color: #333333; }
.info-text-wrap .text-top-style .text-bot-style { width: 60px; height: 1px; background-color: #333; position: absolute; right: 4px; bottom: 0; }
.info-text-wrap .text-top-style .text-bot-style::before { position: absolute; left: 4px; top: -2.5px; display: block; content: ''; width: 5px; height: 5px; background-color: #333333; }
.info-text-wrap .text-bot-style { width: 60px; height: 1px; background-color: #333; position: absolute; right: 4px; bottom: 0; }
.info-text-wrap .text-bot-style::after { position: absolute; right: 0; top: -2.5px; display: block; content: ''; width: 5px; height: 5px; background-color: #333; }
.info-text-wrap .info-text-desc { padding: 10px 4px; }
.desc { font-size: 14px; padding-bottom: 6px; line-height: 20px; margin: 4px 0; text-indent: 10px; }
.img { width: 100%; } </style>
|
参数信息
编辑src/network/detail.js
1 2 3 4 5 6 7 8 9
| export class GoodsParams { constructor(info, rule) { this.image = info.images ? info.images[0] : ""; this.infos = info.set; this.sizes = rule.tables; } }
|
在src/views/detail/childComps
下创建DetailParamInfo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <template> <div v-if="Object.keys(paramInfo).length !== 0" class="params-wrap"> <div v-for="item in paramInfo.rule" :key="item.id"> <div v-for="list in item" class="flex" :key="list.id"> <div v-for="listitem in list" class="rule-list-item" :key="listitem.id"> {{listitem}} </div> </div> </div> <div v-for="(info, index) in paramInfo.info" :key="index" class="flex info-list-wrap"> <div class="info-list-tit">{{info.key}}</div> <div>{{info.value}}</div> </div> </div> </template>
<script> export default { name: 'DetailParamInfo', props: { paramInfo: { type: Object, default() { return {} } } } } </script>
<style scoped> .params-wrap { border-top: 4px solid #ececec; border-bottom: 4px solid #ececec; }
.rule-list-item { font-size: 12px; width: 20%; border-bottom: 1px solid #ececec; padding: 10px 4px; }
.info-list-wrap { font-size: 14px;; border-bottom: 1px solid #ececec; padding: 10px 4px; line-height: 20px;
}
.info-list-wrap .info-list-tit { width: 18%; } </style>
|
评论信息
编辑src/common/utils.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| export function formatDate(date, fmt) { if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); }
let o = { "M+": date.getMonth() + 1, "d+": date.getDate(), "h+": date.getHours(), "m+": date.getMinutes(), "s+": date.getSeconds() };
for (let k in o) { if (new RegExp(`(${k})`).test(fmt)) { let str = o[k] + ""; fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? str : padLeftZero(str)); } }
return fmt; }
function padLeftZero(str) { return ("00" + str).substr(str.length); }
|
在src/views/detail/childComps
下创建DetailCommentInfo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| <template> <div> <div class="comment-info" v-if="Object.keys(commentInfo).length !== 0"> <div class="info-header"> <div class="header-title">用户评价</div> <div class="header-more"> 更多 <i class="arrow-right" /> </div> </div> <div class="info-user"> <img :src="commentInfo.user.avatar" alt="" /> <span>{{ commentInfo.user.uname }}</span> </div> <div class="info-detail"> <p>{{ commentInfo.content }}</p> <div class="info-other"> <span class="date">{{ commentInfo.created | showDate }}</span> <span>{{ commentInfo.style }}</span> </div> <div class="info-imgs"> <img :key="index" :src="item" alt="" v-for="(item, index) in commentInfo.images" /> </div> </div> </div> </div> </template>
<script> import { formatDate } from "common/utils";
export default { name: "DetailCommentInfo", props: { commentInfo: { type: Object } }, filters: { showDate: function (value) { let date = new Date(value * 1000); return formatDate(date, "yyyy-MM-dd hh:mm"); } } }; </script>
<style scoped> .comment-info { padding: 5px 12px; color: #333333; border-bottom: 5px solid #f2f5f8; }
.info-header { line-height: 50px; height: 50px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); }
.header-title { font-size: 15px; float: left; }
.header-more { font-size: 13px; float: right; margin-right: 10px; }
.info-user { padding: 10px 0 5px; }
.info-user img { width: 42px; height: 42px; border-radius: 50%; }
.info-user span { font-size: 15px; position: relative; top: -15px; margin-left: 10px; }
.info-detail { padding: 0 5px 15px; }
.info-detail p { font-size: 14px; line-height: 1.5; color: #777777; }
.info-detail .info-other { font-size: 12px; margin-top: 10px; color: #999999; }
.info-other .date { margin-right: 8px; }
.info-imgs { margin-top: 10px; }
.info-imgs img { width: 70px; height: 70px; margin-right: 5px; } </style>
|
推荐信息
编辑src/network/detail.js
1 2 3 4 5
| export function getRecommend() { return request({ url: "/recommend" }) }
|
由于推荐信息中图片显示的GoodsList与首页显示的不同所以需要修改GoodsListItem.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| <template> <div class="goods-item" @click="clickDetail"> <img :src="showImage" alt="" @load="imageLoad"> <div class="goods-info"> <p>{{ goodItem.title }}</p> <span class="price">{{goodItem.price}}</span> <span class="collect">{{ goodItem.cfav }}</span> </div> </div> </template>
<script> export default { name: "GoodListItem", props: { goodItem: { type: Object, default() { return {} } } }, computed:{ showImage(){ return this.goodItem.img || this.goodItem.image } }, methods:{ imageLoad(){ this.$bus.$emit('imageLoad'); }, clickDetail(){ this.$router.push("/detail/"+ this.goodItem.iid) } } } </script>
<style scoped> .goods-item { padding-bottom: 40px; position: relative;
width: 48%; }
.goods-item img { width: 100%; border-radius: 5px; }
.goods-info { font-size: 12px; position: absolute; bottom: 5px; left: 0; right: 0; overflow: hidden; text-align: center; }
.goods-info p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 3px; }
.goods-info .price { color: var(--color-high-text); margin-right: 20px; }
.goods-info .collect { position: relative; }
.goods-info .collect::before { content: ''; position: absolute; left: -15px; top: -1px; width: 14px; height: 14px; background: url("~assets/img/common/collect.svg") 0 0/14px 14px; }
</style>
|
详情页
Detail.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
| <template> <div class="detail"> <detail-nav-bar class="detail-nav"/> <Scroll class="content"> <DetailSwiper :banners="banners"/> <DetailBaseInfo :goods="goods"/> <DetailShopInfo :shop="shop"/> <DetailGoodsInfo :detailInfo="detailInfo" @imageLoad="imageLoad"/> <DetailParamInfo :paramInfo="paramInfo"/> <DetailCommentInfo :commentInfo="commentInfo"/> <GoodList :goods="recommendList"/> </Scroll> </div> </template>
<script> import DetailNavBar from "./childComps/DetailNav"; import DetailSwiper from "./childComps/DetailSwiper"; import DetailBaseInfo from "./childComps/DetailBaseInfo"; import DetailShopInfo from "./childComps/DetailShopInfo"; import DetailGoodsInfo from "./childComps/DetailGoodsInfo"; import DetailParamInfo from "./childComps/DetailParamInfo"; import DetailCommentInfo from "./childComps/DetailCommentInfo"; import GoodList from "components/contents/good/GoodList";
import {getDetail, getRecommend, Goods, Shop, GoodsParams} from "network/detail"; import Scroll from "components/common/scroll/Scroll";
export default { name: "Detail", components: { DetailCommentInfo, DetailNavBar, DetailSwiper, DetailBaseInfo, DetailShopInfo, DetailGoodsInfo, DetailParamInfo, GoodList, Scroll }, data() { return { iid: null, banners: [], goods: {}, shop: {}, detailInfo: {}, paramInfo: {}, commentInfo: {}, recommendList: [] } }, created() { this.iid = this.$route.params.iid
this.getProductDetail(); this.getRecommend(); }, methods: { imageLoad() { this.$refs.scroll.refresh() }, getProductDetail() { getDetail(this.iid).then((res) => { const data = res.result; console.log(res) this.banners = data.itemInfo.topImages
this.goods = new Goods(data.itemInfo, data.columns, data.shopInfo.services);
this.shop = new Shop(data.shopInfo);
this.detailInfo = data.detailInfo
this.paramInfo = new GoodsParams(data.itemParams.info, data.itemParams.rule || {});
if (data.rate.cRate !== 0) { this.commentInfo = data.rate.list[0] || {}; } }) }, getRecommend() { getRecommend().then((res) => { console.log(res.data) this.recommendList = res.data.list; }) } } } </script>
<style scoped>
.detail { position: relative; z-index: 9; background-color: #fff; height: 100vh; }
.content { position: relative; height: calc(100% - 44px); overflow: hidden; } </style>
|
mixin的使用
解决全局监听产生的bug https://cn.vuejs.org/v2/api/#mixins mixins也就是把其他地方的东西整合在一个地方
创建src/common/mixins.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import {debounce} from "./utils";
export const imgListenerMixin = { data() { return { imageListener: null, newRefresh:null } }, mounted() { this.newRefresh = debounce(this.$refs.scroll.refresh, 50)
this.imageListener = () =>{ this.newRefresh() }
this.$bus.$on('imageLoad', this.imageListener) } }
|
编辑src/views/home/Home.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <script> import { imgListenerMixin } from "common/mixins";
export default { name: "Home", mixins: [imgListenerMixin], computed: { showGoods() { return this.goods[this.curnettType].list } }, mounted() {
}, destroyed() { this.$bus.$off('imageLoad',this.imageListener); } } </script>
|
编辑src/views/detail/Detail.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script> import {imgListenerMixin} from "common/mixins";
export default { name: "Detail", mixins: [imgListenerMixin], destroyed() { this.$bus.$off("imageLoad",this.imageListener) } } </script>
|
再次解决详情页滑动的小bug
Detail
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script>
export default { methods: { imageLoad() { this.newRefresh() }, } } </script>
|
标题内容联动
点击标题,滚动到对应的主题
在detail中监听标题的点击,获取index
滚动到对应的主题:
获取所有主题的offsetTop
问题:在哪里才能获取到正确的offsetTop
- created肯定不行, 压根不能获取元素
- mounted也不行,数据还没有获取到
- 获取到数据的回调中也不行,DOM还没有渲染完
$nextTick
也不行,因为图片的高度没有被计算在类- 在图片加载完成后,获取的高度才是正确
编辑src/views/detail/childComps/DetailNav.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <script>
import NavBar from "components/common/navbar/NavBar";
export default { methods:{ itemClick(index){ this.curnettIndex = index this.$emit("titleClick",index) }, back(){ this.$router.go(-1); } } } </script>
|
编辑src/views/detail/Detail.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| <template> <div class="detail"> <detail-nav-bar class="detail-nav" @titleClick="titleClick" ref="nav"/> <Scroll class="content" ref="scroll" :probeType="3" @scroll="contentScroll"> <DetailSwiper :banners="banners"/> <DetailBaseInfo :goods="goods"/> <DetailShopInfo :shop="shop"/> <DetailGoodsInfo :detailInfo="detailInfo" @imageLoad="imageLoad"/> <DetailParamInfo :paramInfo="paramInfo" ref="paramInfo"/> <DetailCommentInfo :commentInfo="commentInfo" ref="commentInfo"/> <GoodList :goods="recommendList" ref="recommendList"/> </Scroll> </div> </template>
<script>
export default { data() { return { navBarTops: [], getTops: null, curnettIndex: 0 } }, mixins: [imgListenerMixin], created() { this.getTops = debounce(() => { this.navBarTops = [] this.navBarTops.push(0) this.navBarTops.push(this.$refs.paramInfo.$el.offsetTop) this.navBarTops.push(this.$refs.commentInfo.$el.offsetTop) this.navBarTops.push(this.$refs.recommendList.$el.offsetTop) this.navBarTops.push(Number.MAX_VALUE) console.log(this.navBarTops) }, 100) }, mounted() {
}, methods: { imageLoad() { this.newRefresh() this.getTops() }, titleClick(index) { this.$refs.scroll.scrollTo(0, -this.navBarTops[index], 200) }, contentScroll(position) { let positionY = -position.y let length = this.navBarTops.length for (let i = 0; i < length - 1; i++) { if (this.curnettIndex !== i && (positionY >= this.navBarTops[i] && positionY < this.navBarTops[i + 1])) { this.curnettIndex = i this.$refs.nav.curnettIndex = this.curnettIndex } }
} }, } </script>
|
底部栏
在src/views/detail/childComps
下创建DetailBottomBar.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| <template> <div class="bottom-bar"> <div class="bar-item bar-left"> <div> <i class="icon service"></i> <span class="text">客服</span> </div> <div> <i class="icon shop"></i> <span class="text">店铺</span> </div> <div> <i class="icon select"></i> <span class="text">收藏</span> </div> </div> <div class="bar-item bar-right"> <div class="cart" @click="addToCart">加入购物车</div> <div class="buy">购买</div> </div> </div> </template>
<script> export default { name: "DetailBottomBar", methods: { addToCart() { this.$emit('addToCart') } } } </script>
<style scoped> .bottom-bar { height: 58px; position: fixed; background-color: #fff; left: 0; right: 0; bottom: 0; display: flex; text-align: center; } .bar-item { flex: 1; display: flex; } .bar-item>div { flex: 1; } .bar-left .text { font-size: 13px; } .bar-left .icon { display: block; width: 22px; height: 22px; margin: 10px auto 3px; background: url("~assets/img/detail/detail_bottom.png") 0 0/100%; } .bar-left .service { background-position:0 -54px; } .bar-left .shop { background-position:0 -98px; } .bar-right { font-size: 15px; color: #fff; line-height: 58px; } .bar-right .cart { background-color: #ffe817; color: #333; } .bar-right .buy { background-color: #f69; } </style>
|
BackTop的封装
由于我们需要在Home.vue
和Detail.vue
都使用BackTop、此时我们就需要使用mixins
编辑src/common/mixins.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import BackTop from "components/contents/backtop/BackTop";
export const backTopMixin = { data(){ return{ isShowBackTop: false, } }, components: { BackTop }, methods:{ topClick() { this.$refs.scroll.scrollTo(0, 0, 1000) }, } }
|
然后在Home.vue
和Detail.vue
导入
并且在监听Scroll滚动事件中添加
来判断是否显示BackTop
1
| this.isShowBackTop = (-position.y) > 1000
|
购物车相关
Vuex
确保安装了Vuex
在src/store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import Vue from 'vue' import Vuex from 'vuex' import mutations from "./mutations"; import actions from "./actions";
Vue.use(Vuex)
const state = { cartList: [] }
export default new Vuex.Store({ state, mutations, actions })
|
1 2
| export const ADD_COUNTER = 'add_counter' export const ADD_TO_CART = 'add_to_cart'
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { ADD_TO_CART, ADD_COUNTER } from "./mutations-types";
export default { [ADD_COUNTER](state, payload) { payload.count+=1 }, [ADD_TO_CART](state, payload) { state.cartList.push(payload) } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { ADD_TO_CART, ADD_COUNTER } from "./mutations-types";
export default { addCart(context, payload) { let oldProduct = context.state.cartList.find(item => item.iid == payload.iid);
if (oldProduct) { context.commit(ADD_COUNTER, oldProduct) } else { payload.count = 1 context.commit(ADD_TO_CART, payload) } } }
|
编辑Detail.vue
1 2 3 4 5 6 7 8 9 10 11 12
| <DetailBottomBar @addToCart="addToCart"/>
addToCart(){ const product = {} product.image = this.banners[0]; product.title = this.goods.title; product.desc = this.goods.desc; product.price = this.goods.realPrice; product.iid = this.iid;
this.$store.dispatch("addCart",product) }
|
导航栏
创建src/store/getters.js
1 2 3 4 5 6 7 8
| export default { cartLength(state){ return state.cartList.length }, cartList(state) { return state.cartList } }
|
编辑src/store/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import Vue from 'vue' import Vuex from 'vuex' import mutations from "./mutations"; import actions from "./actions"; import getters from "./getters";
Vue.use(Vuex)
const state = { cartList: [] }
export default new Vuex.Store({ state, mutations, actions, getters })
|
编辑src/views/Cart.vue
这里使用了mapGetters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <template> <div id="cart"> <NavBar class="cart-nav-bar"> <div slot="center">购物街({{ length }})</div> </NavBar> </div> </template>
<script>
import NavBar from "components/common/navbar/NavBar";
import {mapGetters} from 'vuex'
export default { name: "Cart", components: { NavBar, }, computed:{ ...mapGetters({ length: 'cartLength' }) } } </script>
<style scoped>
#cart { height: 100vh; position: relative; }
.cart-nav-bar { background-color: var(--color-tint); color: #fff; }
</style>
|
在src/components/content/checkbutton
下创建CheckButton.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <template> <div> <div class="icon-selector" :class="{'active': value}" > <img src="~assets/img/cart/tick.svg" alt=""> </div> </div> </template>
<script> export default { name: "CheckButton", props: { value: { type: Boolean, default: true } }, } </script>
<style scoped> .icon-selector { position: relative; margin: 0; width: 18px; height: 18px; border-radius: 50%; border: 2px solid #ccc; cursor: pointer; } .active { background-color: #ff8198; border-color: #ff8198; } </style>
|
封装商品信息
在src/views/cart/childComps
下创建
CartListItem.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| <template> <div id="shop-item"> <div class="item-selector"> <CheckButton @checkBtnClick="checkedChange" v-model="itemInfo.checked"></CheckButton> </div> <div class="item-img"> <img :src="itemInfo.image" alt="商品图片"> </div> <div class="item-info"> <div class="item-title">{{itemInfo.title}}</div> <div class="item-desc">商品描述: {{itemInfo.desc}}</div> <div class="info-bottom"> <div class="item-price left">¥{{itemInfo.price}}</div> <div class="item-count right">x{{itemInfo.count}}</div> </div> </div> </div> </template>
<script> import CheckButton from "components/contents/checkbutton/CheckButton"; export default { name: "ShopCartItem", props: { itemInfo: Object }, components: { CheckButton }, methods: { checkedChange: function () { this.itemInfo.checked = !this.itemInfo.checked; } } } </script>
<style scoped> #shop-item { width: 100%; display: flex; font-size: 0; padding: 5px; border-bottom: 1px solid #ccc; }
.item-selector { width: 14%; display: flex; justify-content: center; align-items: center; }
.item-title, .item-desc { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.item-img { padding: 5px; }
.item-img img { width: 80px; height: 100px; display: block; border-radius: 5px; }
.item-info { font-size: 17px; color: #333; padding: 5px 10px; position: relative; overflow: hidden; }
.item-info .item-desc { font-size: 14px; color: #666; margin-top: 15px; }
.info-bottom { margin-top: 10px; position: absolute; bottom: 10px; left: 10px; right: 10px; }
.info-bottom .item-price { color: orangered; } </style>
|
CartList.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <template> <Scroll ref="content"> <div> <cart-list-item v-for="(item,index) in cartList" :key="index" :item-info="item" /> </div> </Scroll> </template>
<script>
import Scroll from "components/common/scroll/Scroll"; import CartListItem from "./CartListItem";
export default { name: "CartList", components: { Scroll, CartListItem }, props: { cartList:{ type:Array, default(){ return [] } } }, activated() { this.$refs.content.refresh() } } </script>
<style scoped>
</style>
|
编辑Cart.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| <template> <div id="cart"> <NavBar class="cart-nav-bar"> <div slot="center">购物街({{ cartLength }})</div> </NavBar> <CartList class="cart-list" :cart-list="cartList" /> </div> </template>
<script>
import NavBar from "components/common/navbar/NavBar"; import CartList from "./childComps/CartList";
import {mapGetters} from 'vuex'
export default { name: "Cart", components: { NavBar, CartList }, computed:{ ...mapGetters(['cartLength','cartList']) } } </script>
<style scoped>
#cart { height: 100vh; position: relative; }
.cart-nav-bar { background-color: var(--color-tint); color: #fff; }
.cart-list { overflow: hidden; position: absolute; top: 44px; bottom: 49px; width: 100%; }
</style>
|
底部信息栏
在src/views/cart/childComps
下创建CartBottomBar
几个注意点:
- 计算总价格时我们通过
filter
筛选出选中状态的价格、再使用reduce
对价格进行累加 every()
是对数组中每一项运行给定函数,如果该函数所有一项返回true,则返回true。一旦有一项不满足则返回false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| <template> <div class="bottom-menu"> <div class="checkButton"> <CheckButton class="select-all" :value="isSelectAll" @click.native="checkClick"></CheckButton> <span class="checkAll">全选</span> </div> <span class="total-price">合计: ¥{{totalPrice}}</span> <span class="buy-product">去计算({{cartLength}})</span> </div> </template>
<script> import CheckButton from "components/contents/checkbutton/CheckButton";
export default { name: "BottomBar", components: { CheckButton }, computed: { totalPrice() { const cartList = this.$store.getters.cartList; return cartList.filter(item => { return item.checked }).reduce((preValue, item) => { return preValue + item.count * item.price }, 0).toFixed(2) }, cartLength() { return this.$store.state.cartList.filter((item) => item.checked).length }, isSelectAll() { if(this.$store.state.cartList.length == 0) return false return this.$store.state.cartList.every((item)=> item.checked == true) } }, methods: { checkClick(){ if (this.isSelectAll){ this.$store.state.cartList.forEach((item)=> item.checked = false) }else { this.$store.state.cartList.forEach((item)=> item.checked = true) } } } } </script>
<style scoped> .bottom-menu { position: fixed; left: 0; right: 0; bottom: 49px; display: flex; height: 44px; text-align: center; }
.checkButton { display: flex; align-items: center; justify-content: center; margin-left: 10px; }
.checkButton .checkAll { line-height: 44px; margin-left: 5px; }
.bottom-menu .total-price { flex: 1; margin-left: 15px; font-size: 16px; color: #666; line-height: 44px; text-align: left; }
.bottom-menu .buy-product { line-height: 44px; width: 90px; float: right; background-color: red; color: #fff; } </style>
|
封装Toast
将actions返回Promise对象
1 2 3 4 5 6 7 8 9 10 11 12 13
| addCart(context, payload) { return new Promise(((resolve, reject) => { let oldProduct = context.state.cartList.find(item => item.iid == payload.iid); if (oldProduct) { context.commit(ADD_COUNTER, oldProduct) resolve("当前商品数量+1") } else { payload.count = 1 context.commit(ADD_TO_CART, payload) resolve("添加了此商品") } })) }
|
由于我们需要在Detail.vue
和Cart.vue
中都使用
在src/components/common/toast
下创建
Toast.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <template> <div class="toast" v-show="isShow"> {{ message }} </div> </template>
<script> export default { name: "Toast", data() { return { message: "", isShow: false } }, methods: { show(message, duration = 2000) { this.isShow = true this.message = message setTimeout(()=>{ this.isShow = false this.message = "" }, duration) } } } </script>
<style scoped>
.toast{ transform: translate(-50%, -50%); position: fixed; left: 50%; top: 50%; z-index: 999; padding: 8px 15px; color: #fff; background-color: rgba(0,0,0,.7); border-radius: 3px;
}
</style>
|
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import Toast from "./Toast"; const obj = {} obj.install = function (Vue) { const toastContrustor = Vue.extend(Toast) const toast = new toastContrustor() toast.$mount(document.createElement('div')) document.body.appendChild(toast.$el)
Vue.prototype.$toast = toast } export default obj
|
在src/main.js
上使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import toast from 'components/common/toast'
Vue.config.productionTip = false
Vue.prototype.$bus = new Vue()
Vue.use(toast)
new Vue({ router, store, netder: h => h(App) }).$mount('#app')
|
最后在Detail.vue
使用
1 2 3 4 5 6 7 8 9 10 11 12
| addToCart(){ const product = {} product.image = this.banners[0]; product.title = this.goods.title; product.desc = this.goods.desc; product.price = this.goods.realPrice; product.iid = this.iid;
this.$store.dispatch("addCart",product).then((res)=>{ this.$toast.show(res) }) }
|
或者修改CartBottomBar.vue
添加一个点击事件
1 2 3 4 5
| cartClick(){ if (!this.isSelectAll){ this.$toast.show("请选择商品") } }
|
分类页
数据请求
在src/network
下创建category.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import {request} from './request'
export function getCategory(){ return request({ url: '/category' }) }
export function getSubcategory(maitKey) { return request({ url: '/subcategory', params: { maitKey } }) } export function getCategoryDetail(miniWallkey, type) { return request({ url: '/subcategory/detail', params: { miniWallkey, type } }) }
|
在src/views/category/childComps
下创建TabMenu.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <template> <scroll id="tab-menu"> <div class="menu-list"> <div class="menu-list-item" :class="{ active: index === curnettIndex }" v-for="(item, index) in categories" :key="index" @click="itemClick(index)" > {{ item.title }} </div> </div> </scroll> </template>
<script> import Scroll from "components/common/scroll/Scroll";
export default { name: "TabMenu", components: { Scroll }, props: { categories: Array }, data() { return { curnettIndex: 0 }; }, methods: { itemClick(index) { this.curnettIndex = index; this.$emit("selectItem", index); } } }; </script>
<style scoped> #tab-menu { background-color: #f6f6f6; height: 100%; width: 100px; box-sizing: border-box; }
.menu-list-item { height: 45px; line-height: 45px; text-align: center; font-size: 14px; }
.menu-list-item.active { font-weight: 700; color: var(--color-high-text); background-color: #fff; border-left: 3px solid var(--color-high-text); } </style>
|
GridView
在src/compoments/common/gridView
下创建GridView.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| <template> <div class="grid-view" ref="gridView"> <slot></slot> </div> </template>
<script> export default { name: "GridView", props: { cols: { type: Number, default: 2 }, hMargin: { type: Number, default: 8 }, vMargin: { type: Number, default: 8 }, itemSpace: { type: Number, default: 8 }, lineSpace: { type: Number, default: 8 } }, mounted: function() { setTimeout(this._autoLayout, 20); },
updated: function() { this._autoLayout(); },
methods: { _autoLayout: function() { let gridEl = this.$refs.gridView; let childnet = gridEl.childnet; gridEl.style.padding = `${this.vMargin}px ${this.hMargin}px`; let itemWidth = (gridEl.clientWidth - 2 * this.hMargin - (this.cols - 1) * this.itemSpace) / this.cols; for (let i = 0; i < childnet.length; i++) { let item = childnet[i]; item.style.width = itemWidth + "px"; if ((i + 1) % this.cols !== 0) { item.style.marginRight = this.itemSpace + "px"; } if (i >= this.cols) { item.style.marginTop = this.lineSpace + "px"; } } } } }; </script>
<style scoped> .grid-view { display: flex; flex-wrap: wrap; justify-content: space-around; } </style>
|
在src/views/category/childComps
下创建TabContentCategory.vue
用来封装GridView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <template> <div> <grid-view :cols="3" :lineSpace="15" :v-margin="20" v-if="subcategories.list"> <div class="item" v-for="(item, index) in subcategories.list" :key="index"> <a :href="item.link"> <img class="item-img" :src="item.image" alt=""> <div class="item-text">{{item.title}}</div> </a> </div> </grid-view> </div> </template>
<script> import GridView from 'components/common/gridView/GridView'
export default { name: "TabContentCategory", components: { GridView }, props: { subcategories: { type: Object, default() { return [] } } } } </script>
<style scoped> .panel img { width: 100%; }
.item { text-align: center; font-size: 12px; }
.item-img { width: 80%; }
.item-text { margin-top: 15px; } </style>
|
其他整合
Category.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
| <template> <div id="category"> <nav-bar class="nav-bar"><div slot="center">商品分类</div></nav-bar> <div class="content"> <tab-menu :categories="categories" @selectItem="selectItem"/>
<scroll id="tab-content" :data="[categoryData]" ref="scroll"> <div> <tab-content-category :subcategories="showSubcategory"/> <tab-control :titles="['综合', '新品', '销量']" @itemClick="tabClick"/> <goods-list :goods="showCategoryDetail"/> </div> </scroll> </div> </div> </template>
<script> import NavBar from 'components/common/navbar/NavBar'
import TabMenu from './childComps/TabMenu' import TabContentCategory from './childComps/TabContentCategory'
import TabControl from 'components/contents/tabcontrol/TabControl' import Scroll from 'components/common/scroll/Scroll' import GoodsList from 'components/contents/good/GoodList'
import {getCategory, getSubcategory, getCategoryDetail} from "network/category";
import {tabControlMixin} from "@/common/mixins";
export default { name: "Category", components: { NavBar, TabMenu, TabControl, Scroll, TabContentCategory, GoodsList }, mixins: [tabControlMixin], data() { return { categories: [], categoryData: { }, curnettIndex: -1 } }, created() { this._getCategory()
this.$bus.$on('imgLoad', () => { this.$refs.scroll.refresh() }) }, computed: { showSubcategory() { if (this.curnettIndex === -1) return {} return this.categoryData[this.curnettIndex].subcategories }, showCategoryDetail() { if (this.curnettIndex === -1) return [] return this.categoryData[this.curnettIndex].categoryDetail[this.curnettType] } }, methods: { _getCategory() { getCategory().then(res => { this.categories = res.data.category.list for (let i = 0; i < this.categories.length; i++) { this.categoryData[i] = { subcategories: {}, categoryDetail: { 'pop': [], 'new': [], 'sell': [] } } } this._getSubcategories(0) }) }, _getSubcategories(index) { this.curnettIndex = index; const mailKey = this.categories[index].maitKey; getSubcategory(mailKey).then(res => { this.categoryData[index].subcategories = res.data this.categoryData = {...this.categoryData} this._getCategoryDetail('pop') this._getCategoryDetail('sell') this._getCategoryDetail('new') }) }, _getCategoryDetail(type) { const miniWallkey = this.categories[this.curnettIndex].miniWallkey; getCategoryDetail(miniWallkey, type).then(res => { this.categoryData[this.curnettIndex].categoryDetail[type] = res this.categoryData = {...this.categoryData} }) },
selectItem(index) { this._getSubcategories(index) } } } </script>
<style scoped> #category { height: 100vh; }
.nav-bar { background-color: var(--color-tint); font-weight: 700; color: #fff; z-index: 99; }
.content { position: absolute; left: 0; right: 0; top: 44px; bottom: 49px; display: flex; overflow: hidden; }
#tab-content { height: 100%; flex: 1; } </style>
|
移动端300ms延迟
安装fastclick
插件
1
| npm install fastclick --save
|
使用: index.js
1 2 3 4 5 6 7
| import fastclick from 'fastclick'
fastclick.attach(document.body)
|
图片懒加载
https://github.com/hilongjw/vue-lazyload
安装
1
| npm install --save vue-lazyload
|
使用:
1 2 3 4 5
| import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload,{ loading: require('./assets/img/common/placeholder.png') })
|
在需要懒加载的图片添加v-lazy
px转vw
安装
1
| npm install postcss-px-to-viewport -dev--save
|
根目录新建postcss.config.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| module.exports = { plugins: { 'postcss-px-to-viewport': { unitToConvert: 'px', viewportWidth: 375, viewportHeight: 667, unitPrecision: 5, viewportUnit: 'vw', selectorBlackList: ['ignore', 'tarbar','tab-bar-item'], minPixelValue: 1, mediaQuery: false, exclude: [/^TabBar/], } } }
|
总结
虽然跟着视频敲了一边、但是还是有很多不是特别理解的、样式方面还是不是特别熟练