01-首页-路由与组件
目的: 搭建页面架子,便于接下来进行页面布局组件编写。
根组件下定义一级路由组件出口 src/App.vue
<template> <!-- 一级路由 --> <router-view></router-view> </template>
一级路由布局容器 src/views/Layout.vue ```vue
头部
**1)准备要用的变量和混入代码** - 变量 src/assets/styles/variables.less ```less // 主题 @xtxColor:#27BA9B; // 辅助 @helpColor:#E26237; // 成功 @sucColor:#1DC779; // 警告 @warnColor:#FFB302; // 价格 @priceColor:#CF4444; ``` - 混入 src/assets/styles/mixins.less ```less // less混入就是,申明一段css代码(选择器包裹的代码)或者函数,在其他css选择器调用,可复用包裹的代码。 // 鼠标经过上移阴影动画 .hoverShadow () { transition: all .5s; &:hover { transform: translate3d(0,-3px,0); box-shadow: 0 3px 8px rgba(0,0,0,0.2); } } ``` less混入就是,申明一段css代码(选择器包裹的代码)或者函数,在其他css选择器调用,可复用包裹的代码。
**2)完成自动注入公用变量和混入**
**遇到问题:** 每次使用公用的变量和mixin的时候需要单独引入到文件中。

**解决方法:** 使用vuecli的style-resoures-loader插件来完成自动注入到每个less文件或者vue组件中style标签中。 - 在当前项目下执行一下命令vue add style-resources-loader,添加一个vuecli的插件  - 安装完毕后会在vue.config.js中自动添加配置,如下: ```javascript module.exports = { pluginOptions: { 'style-resources-loader': { preProcessor: 'less', patterns: [] } } } ``` - 把你需要注入的文件配置一下后,重启服务即可。 ```javascript +const path = require('path') module.exports = { pluginOptions: { 'style-resources-loader': { preProcessor: 'less', patterns: [ + path.join(__dirname, './src/assets/styles/variables.less'), + path.join(__dirname, './src/assets/styles/mixins.less') ] } } } ``` **总结:** 知道如何定义less变量和混入代码并使用他们,通过vue-resources-loader完成代码注入再每个less文件和vue组件中。 ## 03-首页-样式重置与公用 **目的:** 准备网站所需的重置样式代码,以及一些公用样式代码。 - 重置样式 执行 npm i normalize.css 安装重置样式的包,然后在 main.js 导入 normalize.css 即可。 ```javascript import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' +import 'normalize.css' createApp(App).use(store).use(router).mount('#app') ``` - 公用样式 新建文件 src/assets/styles/common.less 在该文件写入常用的样式,然后在 main.js 导入即可。
src/assets/styles/common.less ```less // 重置样式 * { box-sizing: border-box; } html { height: 100%; font-size: 14px; } body { height: 100%; color: #333; min-width: 1240px; font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif } ul, h1, h3, h4, p, dl, dd { padding: 0; margin: 0; } a { text-decoration: none; color: #333; outline: none; } i { font-style: normal; } input[type="text"], input[type="search"], input[type="password"], input[type="checkbox"]{ padding: 0; outline: none; border: none; -webkit-appearance: none; &::placeholder{ color: #ccc; } } img { max-width: 100%; max-height: 100%; vertical-align: middle; } ul { list-style: none; } #app { background: #f5f5f5; user-select: none; } .container { width: 1240px; margin: 0 auto; position: relative; } .ellipsis { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .ellipsis-2 { word-break: break-all; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } .fl { float: left; } .fr { float: right; } .clearfix:after { content: "."; display: block; visibility: hidden; height: 0; line-height: 0; clear: both; } ``` src/main.js ```javascript import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' import 'normalize.css' +import '@/assets/styles/common.less' createApp(App).use(store).use(router).mount('#app') ``` **总结:** 重置样式使用normalize.css,项目公用样式common.less ## 04-首页-顶部通栏布局 **目的:** 完成顶部通栏组件。

大致步骤:
1)在 public/index.html 引入字体图标文件。 ```javascript +
4)根据当前的登录状态显示 用户名和退出登录
```vue
<script>
import { useStore } from 'vuex'
import { computed } from 'vue'
export default {
name: 'AppTopnav',
setup () {
const store = useStore()
// 根据当前的登录状态显示 用户名和退出登录
const profile = computed(() => {
return store.state.user.profile
})
return {
profile
}
}
}
</script>
05-首页-头部布局
目的: 完成首页头部布局,了解结构。
大致步骤:
- 1)在 src/components/ 下新建 app-header.vue 组件,基础布局如下:
```vue

首先,在 src/components/ 下新建 app-footer.vue 组件,基础布局如下: ```vue ``` 最后,在 src/views/Layout.vue 中导入使用。 ```vue

第一步:提取头部导航为一个组件 - 新建src/components/app-header-nav.vue 组件。 ```vue ``` - 在 app-header.vue 中使用组件。注意,删除结构和样式。 ```vue
**基本步骤:** - 定义一个常量数据和后台保持一致(约定好9大分类),这样不请求后台就能展示一级分类,不至于白屏。 - 在API目录定义接口函数 - 在vuex中的category模块,基于常量数据定义state数据,定义修改分类列表函数,定义获取数据函数。 - 在Layout组件获取调用actions获取数据,在头部导航组件渲染即可。 **落地代码:** - 定义九个分类常量数据 src/api/constants.js ```javascript // 顶级分类 export const topCategory = [ '居家', '美食', '服饰', '母婴', '个护', '严选', '数码', '运动', '杂货' ] ``` - 定义API函数 src/api/category.js ```javascript // 定义首页需要的接口函数 import request from '@/utils/request' /** * 获取首页头部分类数据 */ export const findAllCategory = () => { return request('/home/category/head', 'get') } ``` - vuex在category模块,来存储分类数据,提供修改和获取的函数。 src/store/modules/category.js ```javascript // 存储的分类数据 import { topCategory } from '@/api/constants' import { findAllCategory } from '@/api/category' // 分类模块 export default { namespaced: true, state () { return { // 分类信息集合 // 如果默认是[]数组,看不见默认的9个分类,等你数据加载完毕才会看到。 // 所以:根据常量数据来生成一个默认的顶级分类数据,不会出现空白(没数据的情况) // 这里的数组顺序要和后端返回的顺序一样,要约定一下 list: topCategory.map(item => ({ name: item })) } }, // 加载数据成功后需要修改list所以需要mutations函数 // payload所有的分类集合 mutations: { setList (state, payload) { state.list = payload } }, // 需要向后台加载数据,所以需要actions函数获取数据 actions: { async getList ({ commit }) { // 获取分类数据 const data = await findAllCategory() // 修改分类数据 commit('setList', data.result) } } } ``` - 获取数据在 src/views/Layout.vue 初始化的时候 ```javascript export default { name: 'Layout', components: { AppTopnav, AppHeader, AppFooter }, + // 获取下分类数据 + setup () { + const store = useStore() + store.dispatch('category/getList') + } } ``` - 在头部导航组件渲染 src/compotents/app-header-nav.vue ```javascript import { useStore } from 'vuex' import { computed } from 'vue' export default { name: 'AppHeaderNav', setup () { const store = useStore() const list = computed(()=>{ return store.state.category.list }) return { list } } } ``` ```vue
首页 -
{{item.name}} -
{{sub.name}}
-
描述:由于是单页面路由跳转不会刷新页面,css的hover一直触发无法关闭分类弹窗。
大致逻辑: - 配置路由组件支持分类跳转 - 鼠标进入一级分类展示对应的二级分类弹窗 - 点击一级分类,二级分类,隐藏二级分类弹窗 - 离开一级分类,二级分类,隐藏二级分类弹窗 落地代码:
**1) 配置路由和组件实现跳转** - 配置路由规则 src/router/index.js ```javascript +import TopCategory from '@/views/category' +import SubCategory from '@/views/category/sub' const routes = [ { path: '/', component: Layout, children: [ { path: '/', component: Home }, + { path: '/category/:id', component: TopCategory }, + { path: '/category/sub/:id', component: SubCategory } ] } ] ``` - 创建分类组件 src/views/category/index.vue ```vue
-
+
{{sub.name}}
大致步骤: - 准备吸顶组件基础布局 - 页面滚动到78px以上,显示吸顶组件。 落地代码: - 新建 src/components/app-header-sticky.vue 组件完成布局 ```vue
安装:@vueuse/core 包,它封装了常见的一些交互逻辑。
npm i @vueuse/core@4.9.0
使用:src/components/app-header-sticky.vue 组件 ```vue
大致步骤: - 准备左侧分类组件和基础布局 - 从vuex中拿出9个分类数据,且值需要两个子分类,但是左侧是10个,需要补充一个品牌数据。 - 使用计算属性完成上面逻辑 - 渲染组件 落地代码: - 准备组件:src/views/home/components/home-category.vue ```vue
大致步骤: - 准备布局 - 得到数据 - 鼠标经过记录ID - 通过ID得到分类推荐商品,使用计算属性 - 完成渲染 落地代码: 1. 准备布局:src/views/home/components/home-category.vue ```vue
分类推荐 根据您的购买或浏览记录推荐
-
【定金购】严选零食大礼包(12件)
超值组合装,满足馋嘴欲
¥100.00
当vue中,显示隐藏,创建移除,一个元素或者一个组件的时候,可以通过transition实现动画。

如果元素或组件离开,完成一个淡出效果: ```vue
100
定义一个骨架布局组件:
src/views/home/components/home-skeleton.vue ```vue
-
{{item.title}}
{{item.alt}}
-
{{item.name}}
¥{{item.price}}
我们可以使用 @vueuse/core 中的 useIntersectionObserver 来实现监听进入可视区域行为,但是必须配合vue3.0的组合API的方式才能实现。
**大致步骤:** - 理解 useIntersectionObserver 的使用,各个参数的含义 - 改造 home-new 组件成为数据懒加载,掌握 useIntersectionObserver 函数的用法 - 封装 useLazyData 函数,作为数据懒加载公用函数 - 把 home-new 和 home-hot 改造成懒加载方式 **落的代码:** 1. 先分析下这个useIntersectionObserver 函数: ```javascript // stop 是停止观察是否进入或移出可视区域的行为 const { stop } = useIntersectionObserver( // target 是观察的目标dom容器,必须是dom容器,而且是vue3.0方式绑定的dom对象 target, // isIntersecting 是否进入可视区域,true是进入 false是移出 // observerElement 被观察的dom ([{ isIntersecting }], observerElement) => { // 在此处可根据isIntersecting来判断,然后做业务 }, ) ``` 2. 开始改造 home-new 组件:rc/views/home/components/home-new.vue - 进入可视区后获取数据 ```vue
**基本步骤:** - 准备基础布局组件 - 获取数据实现渲染,完成切换效果 - 加上骨架效果和数据懒加载 **落的代码:** 1. 基础结构:src/views/home/components/home-brand.vue ```vue
- 使用组件:src/views/home/index.vue
```vue
<!-- 人气推荐 -->
<HomeHot />
<!-- 热门品牌 -->
+ <HomeBrand />
- 获取数据和切换效果:
**大致步骤:** - 准备一个商品盒子组件 home-goods 展示单个商品 - 定义产品区块组件 home-product 使用 home-goods 完成基础布局 - 在首页中使用 home-product 组件 - 定义API函数,获取数据,进行渲染 - 处理板块需要进入可视区太多内容才能加载数据问题。 **落地代码:** 1. 单个商品组件:src/views/home/components/home-goods.vue ```vue

美威 智利原味三文鱼排 240g/袋 4片装
海鲜年货
¥108.00
2. 产品区块组件:src/views/home/components/home-product.vue
```vue
<template>
<div class="home-product">
<HomePanel title="生鲜" v-for="i in 4" :key="i">
<template v-slot:right>
<div class="sub">
<RouterLink to="/">海鲜</RouterLink>
<RouterLink to="/">水果</RouterLink>
<RouterLink to="/">蔬菜</RouterLink>
<RouterLink to="/">水产</RouterLink>
<RouterLink to="/">禽肉</RouterLink>
</div>
<XtxMore />
</template>
<div class="box">
<RouterLink class="cover" to="/">
<img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/fresh_goods_cover.jpg" alt="">
<strong class="label">
<span>生鲜馆</span>
<span>全场3件7折</span>
</strong>
</RouterLink>
<ul class="goods-list">
<li v-for="i in 8" :key="i">
<HomeGoods />
</li>
</ul>
</div>
</HomePanel>
</div>
</template>
<script>
import HomePanel from './home-panel'
import HomeGoods from './home-goods'
export default {
name: 'HomeProduct',
components: { HomePanel, HomeGoods }
}
</script>
<style scoped lang='less'>
.home-product {
background: #fff;
height: 2900px;
.sub {
margin-bottom: 2px;
a {
padding: 2px 12px;
font-size: 16px;
border-radius: 4px;
&:hover {
background: @xtxColor;
color: #fff;
}
&:last-child {
margin-right: 80px;
}
}
}
.box {
display: flex;
.cover {
width: 240px;
height: 610px;
margin-right: 10px;
position: relative;
img {
width: 100%;
height: 100%;
}
.label {
width: 188px;
height: 66px;
display: flex;
font-size: 18px;
color: #fff;
line-height: 66px;
font-weight: normal;
position: absolute;
left: 0;
top: 50%;
transform: translate3d(0,-50%,0);
span {
text-align: center;
&:first-child {
width: 76px;
background: rgba(0,0,0,.9);
}
&:last-child {
flex: 1;
background: rgba(0,0,0,.7);
}
}
}
}
.goods-list {
width: 990px;
display: flex;
flex-wrap: wrap;
li {
width: 240px;
height: 300px;
margin-right: 10px;
margin-bottom: 10px;
&:nth-last-child(-n+4) {
margin-bottom: 0;
}
&:nth-child(4n) {
margin-right: 0;
}
}
}
}
}
</style>
- 使用组件:src/views/home/index.vue
```vue
```javascript +import HomeProduct from './components/home-product' export default { name: 'xtx-home-page', + components: { HomeCategory, HomeBanner, HomeNew, HomeHot, HomeBrand, HomeProduct } }
- 获取数据渲染:
定义API src/api/home.js
export const findGoods = () => { return request('home/goods', 'get') }
进行渲染
src/views/home/components/home-product.vue
<template>
<div class="home-product" ref="target">
+ <HomePanel :title="cate.name" v-for="cate in list" :key="cate.id">
<template v-slot:right>
<div class="sub">
+ <RouterLink v-for="sub in cate.children" :key="sub.id" to="/">{{sub.name}}</RouterLink>
</div>
<XtxMore />
</template>
<div class="box">
<RouterLink class="cover" to="/">
+ <img :src="cate.picture" alt="">
<strong class="label">
+ <span>{{cate.name}}馆</span>
+ <span>{{cate.saleInfo}}</span>
</strong>
</RouterLink>
<ul class="goods-list">
+ <li v-for="item in cate.goods" :key="item.id">
+ <HomeGoods :goods="item" />
</li>
</ul>
</div>
</HomePanel>
</div>
</template>
<script>
import HomePanel from './home-panel'
import HomeGoods from './home-goods'
+import { findGoods } from '@/api/home'
+import { useLazyData } from '@/hooks'
export default {
name: 'HomeProduct',
components: { HomePanel, HomeGoods },
+ setup () {
+ const { target, result } = useLazyData(findGoods)
+ return { target, list: result }
+ }
}
</script>
src/views/home/components/home-goods.vue
<template>
<div class="goods-item">
<RouterLink to="/" class="image">
+ <img :src="goods.picture" alt="" />
</RouterLink>
+ <p class="name ellipsis-2">{{goods.name}}</p>
+ <p class="desc">{{goods.tag}}</p>
+ <p class="price">¥{{goods.price}}</p>
<div class="extra">
<RouterLink to="/">
<span>找相似</span>
<span>发现现多宝贝 ></span>
</RouterLink>
</div>
</div>
</template>
<script>
export default {
name: 'HomeGoods',
+ props: {
+ goods: {
+ type: Object,
+ default: () => {}
+ }
+ }
}
</script>
- 处理问题:
- 产品区域需要滚动比较多才能去加载数据。
```javascript
const { stop } = useIntersectionObserver(
container,
([{ isIntersecting }], dom) => {
if (isIntersecting) { stop() apiFn && apiFn().then(({ result }) => { data.value = result }) }
- }, {
- threshold: 0
- }
)
```
- threshold 容器和可视区交叉的占比(进入的面积/容器完整面试) 取值,0-1 之间,默认比0大,所以需要滚动较多才能触发进入可视区域事件。
27-首页主体-最新专题
目的: 完成最新专题展示。
基础布局:src/views/home/components/home-special.vue ```vue100 100 100
使用组件:src/views/home/index.vue
```vue
<!-- 商品区域 -->
<HomeProduct />
<!-- 最新专题 -->
+ <HomeSpecial />
+import HomeSpecial from './components/home-special'
export default {
name: 'xtx-home-page',
+ components: { HomeCategory, HomeBanner, HomeNew, HomeHot, HomeBrand, HomeProduct, HomeSpecial }
}
获取数据:
定义API src/api/home.js
export const findSpecial = () => { return request('home/special', 'get') }
渲染组件 src/views/home/components/home-speical.vue ```vue
{{item.title}}{{item.summary}}
- ¥{{item.lowestPrice}}起
- {{item.collectNum}}
- {{item.viewNum}}
- {{item.replyNum}}
28-首页主体-图片懒加载
目的: 当图片进入可视区域内去加载图片,且处理加载失败,封装成指令。介绍一个webAPI:IntersectionObserver(opens new window) ```javascript // 创建观察对象实例 const observer = new IntersectionObserver(callback[, options]) // callback 被观察dom进入可视区离开可视区都会触发 // - 两个回调参数 entries , observer // - entries 被观察的元素信息对象的数组 [{元素信息},{}],信息中isIntersecting判断进入或离开 // - observer 就是观察实例 // options 配置参数 // - 三个配置属性 root rootMargin threshold // - root 基于的滚动容器,默认是document // - rootMargin 容器有没有外边距 // - threshold 交叉的比例
// 实例提供两个方法 // observe(dom) 观察哪个dom // unobserve(dom) 停止观察那个dom
基于vue3.0和IntersectionObserver封装懒加载指令<br />src/components/library/index.js
```javascript
export default {
install (app) {
app.component(XtxSkeleton.name, XtxSkeleton)
app.component(XtxCarousel.name, XtxCarousel)
app.component(XtxMore.name, XtxMore)
+ defineDirective(app)
}
}
import defaultImg from '@/assets/images/200.png'
// 指令
const defineDirective = (app) => {
// 图片懒加载指令
app.directive('lazyload', {
mounted (el, binding) {
const observer = new IntersectionObserver(([{ isIntersecting }]) => {
if (isIntersecting) {
observer.unobserve(el)
el.onerror = () => {
el.src = defaultImg
}
el.src = binding.value
}
}, {
threshold: 0.01
})
observer.observe(el)
}
})
}
使用指令:
src/views/home/component/home-product.vue
<RouterLink class="cover" to="/">
+ <img alt="" v-lazyload="cate.picture">
<strong class="label">
<span>{{cate.name}}馆</span>
<span>{{cate.saleInfo}}</span>
</strong>
</RouterLink>
src/views/home/component/home-goods.vue
<RouterLink to="/" class="image">
+ <img alt="" v-lazyload="goods.picture" />
</RouterLink>
src/views/home/component/home-product.vue
<RouterLink class="cover" to="/">
+ <img v-lazyload="item.picture" alt="">
<strong class="label">
<span>{{item.name}}馆</span>
<span>{{item.saleInfo}}</span>
</strong>
</RouterLink>
总结:
- 在img上使用使用v-lazyload值为图片地址,不设置src属性。