[TOC]

01-首页-路由与组件

目的: 搭建页面架子,便于接下来进行页面布局组件编写。

  • 根组件下定义一级路由组件出口 src/App.vue

    <template>
    <!-- 一级路由 -->
    <router-view></router-view>
    </template>
    
  • 一级路由布局容器 src/views/Layout.vue ```vue

``` - 二级路由首页组件 src/views/home/index.vue ```vue ``` - 配置路由规则 src/router/index.js ```javascript import { createRouter, createWebHashHistory } from 'vue-router' +const Layout = () => import('@/views/Layout') +const Home = () => import('@/views/home/index') const routes = [ + { + path: '/', + component: Layout, + children: [ + { path: '/', component: Home } + ] + } ] const router = createRouter({ history: createWebHashHistory(), routes }) export default router ``` **总结:** 配置首页的路由规则,由Layout和首页组件组成。 ## 02-首页-less的自动化导入 **目的:** 准备常用less变量,混入代码,完成自动导入。
**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的时候需要单独引入到文件中。
![image.png](https://cdn.nlark.com/yuque/0/2022/png/27272831/1658665438566-a4d6c837-a180-4026-872e-cb232566b88a.png#clientId=ua1107ef6-cdc0-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ua8b757cd&margin=%5Bobject%20Object%5D&name=image.png&originHeight=191&originWidth=1096&originalType=url&ratio=1&rotation=0&showTitle=false&size=25315&status=done&style=none&taskId=uc5c29009-ed33-4189-9188-fa17cb39349&title=)
**解决方法:** 使用vuecli的style-resoures-loader插件来完成自动注入到每个less文件或者vue组件中style标签中。 - 在当前项目下执行一下命令vue add style-resources-loader,添加一个vuecli的插件 ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27272831/1658665438621-10ffbd11-9244-445f-9446-da2a46c97370.png#clientId=ua1107ef6-cdc0-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u83f29553&margin=%5Bobject%20Object%5D&name=image.png&originHeight=412&originWidth=1213&originalType=url&ratio=1&rotation=0&showTitle=false&size=83351&status=done&style=none&taskId=ud82e5299-af8c-4fd6-a467-6440cba6227&title=) - 安装完毕后会在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-首页-顶部通栏布局 **目的:** 完成顶部通栏组件。
![image.png](https://cdn.nlark.com/yuque/0/2022/png/27272831/1658665438317-e4d6fa25-5b73-412b-84d8-23667fba155e.png#clientId=ua1107ef6-cdc0-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u4d2bcf29&margin=%5Bobject%20Object%5D&name=image.png&originHeight=65&originWidth=1282&originalType=url&ratio=1&rotation=0&showTitle=false&size=4650&status=done&style=none&taskId=u9434763d-51fe-4ba0-8c36-cf4069dbc37&title=)
大致步骤:
1)在 public/index.html 引入字体图标文件。 ```javascript + <%= htmlWebpackPlugin.options.title %> ``` 2)在 src/components/ 下新建 app-topnav.vue 组件,基础布局如下: ```vue ``` 3)在 src/views/Layout.vue 中导入使用。 ```vue
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-首页-头部布局

目的: 完成首页头部布局,了解结构。
image.png
大致步骤:

  • 1)在 src/components/ 下新建 app-header.vue 组件,基础布局如下: ```vue
``` - 2)在 src/views/Layout.vue 中导入使用。 ```vue ``` **总结:** 准备了一个静态的头部组件,了解其结构。 ## 06-首页-底部布局 **目的:** 完成首页底部布局。
![image.png](https://cdn.nlark.com/yuque/0/2022/png/27272831/1658665438625-ff143367-8ad7-46ca-8f43-2faaea914497.png#clientId=ua1107ef6-cdc0-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u3a1592b0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=631&originWidth=1313&originalType=url&ratio=1&rotation=0&showTitle=false&size=73277&status=done&style=none&taskId=u1475e926-2940-46f5-865b-4f4499aa2dc&title=)
首先,在 src/components/ 下新建 app-footer.vue 组件,基础布局如下: ```vue ``` 最后,在 src/views/Layout.vue 中导入使用。 ```vue ``` ## 07-首页-头部分类导航组件 **目的:** 提取头部分类导航组件,提供给头部,和将来的吸顶头部使用。
![image.png](https://cdn.nlark.com/yuque/0/2022/png/27272831/1658665439126-1b5eb388-4f64-4743-9508-21e774f05aad.png#clientId=ua1107ef6-cdc0-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u7a9a3447&margin=%5Bobject%20Object%5D&name=image.png&originHeight=268&originWidth=1295&originalType=url&ratio=1&rotation=0&showTitle=false&size=81048&status=done&style=none&taskId=u00c2d338-7f02-4373-af2c-d0bef183a86&title=)
第一步:提取头部导航为一个组件 - 新建src/components/app-header-nav.vue 组件。 ```vue ``` - 在 app-header.vue 中使用组件。注意,删除结构和样式。 ```vue ``` 第二步:完善子级分类布局 src/components/app-header-nav.vue ```vue ``` ## 08-首页-头部分类导航渲染 **目的:** 实现头部一级分类和二级分类渲染。
**基本步骤:** - 定义一个常量数据和后台保持一致(约定好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}}
    • 首页模块 - 图4

      {{sub.name}}

``` **总结:** 数据在vuex中管理,然后再组件使用数据进行渲染。 ## 09-首页-头部分类导航交互 目的:实现点击的时候跳转,且能关闭二级分类弹窗。
描述:由于是单页面路由跳转不会刷新页面,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 ``` src/views/category/sub.vue ```vue ``` **2)跳转后关闭二级分类弹窗** - 给每一个一级分类定义控制显示隐藏的数据,open 布尔类型,通过open设置类名控制显示隐藏。 - 当进入一级分类的时候,将open改为true - 当离开一级分类的时候,将open改为false - 点击一级分类,二级分类,将open改为false 在vuex种给一级分类加open数据 src/store/modules/category.js ```javascript actions: { async getList ({ commit }) { const data = await findAllCategory() // 获取数据成功后提交mutations进行数据修改 // 给每个分类加上控制二级分类显示隐藏的数据 data.result.forEach(top => { top.open = false }) commit('setList', data.result) } } ``` 添加了 show hide vuex的mutations函数修改 open src/store/modules/category.js ```javascript // 修改当前一级分类下的open数据为true show (state, item) { const category = state.list.find(category => category.id === item.id) category.open = true }, // 修改当前一级分类下的open数据为false hide (state, item) { const category = state.list.find(category => category.id === item.id) category.open = false } ``` 在头部导航组件 实现显示和隐藏 src/components/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 }) + const show = (item) => { + store.commit('category/show', item) + } + const hide = (item) => { + store.commit('category/hide', item) + } + return { list, show, hide} } } ``` ```vue +
  • + {{item.name}}
    • + 首页模块 - 图5

      {{sub.name}}

  • ``` ```less - // > .layer { - // height: 132px; - // opacity: 1; - // } } } } .layer { + &.open { + height: 132px; + opacity: 1; + } ``` **总结:** 在组件中调用vuex的mutation函数控制每个一级分类下二级分类的显示隐藏。 ## 10-首页-吸顶头部组件-传统实现 **目的:** 完成吸顶头部
    大致步骤: - 准备吸顶组件基础布局 - 页面滚动到78px以上,显示吸顶组件。 落地代码: - 新建 src/components/app-header-sticky.vue 组件完成布局 ```vue ``` - 在滚动到78px完成显示效果,需要滑出动画。 第一步:默认移出顶部且完全透明,定义一个类回到默认吸顶位置完全显示。 ```less .app-header-sticky { width: 100%; height: 80px; position: fixed; left: 0; top: 0; z-index: 999; background-color: #fff; border-bottom: 1px solid #e4e4e4; + transform: translateY(-100%); + opacity: 0; + &.show { + transition: all 0.3s linear; + transform: none; + opacity: 1; + } ``` 第二步:组件渲染后,监听滚动距离超过78px隐藏 ```javascript import AppHeaderNav from './app-header-nav' +import { onMounted, ref } from 'vue' export default { name: 'AppHeaderSticky', components: { AppHeaderNav }, + setup () { + const y = ref(0) + onMounted(()=>{ + window.onscroll = () => { + const scrollTop = document.documentElement.scrollTop + y.value = scrollTop + } + }) + return { y } + } } ``` ```vue +
    ``` 第三步:v-show使用,为了吸顶头部的内容不遮住不吸顶的头部。 ```vue +
    ``` ## 11-首页-吸顶头部组件-组合API **目的:** 体验基于组合API的@vueuse/core工具库
    安装:@vueuse/core 包,它封装了常见的一些交互逻辑。
    npm i @vueuse/core@4.9.0
    使用:src/components/app-header-sticky.vue 组件 ```vue ``` **总结:** - useWindowScroll() 是@vueuse/core提供的api可返回当前页面滚动时候蜷曲的距离。x横向,y纵向 - vue3.0组合API提供了更多逻辑代码封装的能力。@vueuse/core 基于组合API封装好用的工具函数。 ## 12-首页主体-左侧分类-结构渲染 **目的:** 实现首页主体内容-左侧分类
    大致步骤: - 准备左侧分类组件和基础布局 - 从vuex中拿出9个分类数据,且值需要两个子分类,但是左侧是10个,需要补充一个品牌数据。 - 使用计算属性完成上面逻辑 - 渲染组件 落地代码: - 准备组件:src/views/home/components/home-category.vue ```vue ``` - 预览组件:src/views/home/index.vue ```vue ``` - 从vuex中拿出分类,取出子分类中的前两项。给一级分类追加一项品牌,进行渲染。 ```vue ``` ## 13-首页主体-左侧分类-弹层展示 **目的:** 实现首页主体内容-左侧分类-鼠标进入弹出
    大致步骤: - 准备布局 - 得到数据 - 鼠标经过记录ID - 通过ID得到分类推荐商品,使用计算属性 - 完成渲染 落地代码: 1. 准备布局:src/views/home/components/home-category.vue ```vue

    分类推荐 根据您的购买或浏览记录推荐

    • 首页模块 - 图6

      【定金购】严选零食大礼包(12件)

      超值组合装,满足馋嘴欲

      ¥100.00

    ``` ```less .layer { width: 990px; height: 500px; background: rgba(255,255,255,0.8); position: absolute; left: 250px; top: 0; display: none; padding: 0 15px; h4 { font-size: 20px; font-weight: normal; line-height: 80px; small { font-size: 16px; color: #666; } } ul { display: flex; flex-wrap: wrap; li { width: 310px; height: 120px; margin-right: 15px; margin-bottom: 15px; border: 1px solid #eee; border-radius: 4px; background: #fff; &:nth-child(3n) { margin-right: 0; } a { display: flex; width: 100%; height: 100%; align-items: center; padding: 10px; &:hover { background: #e3f9f4; } img { width: 95px; height: 95px; } .info { padding-left: 10px; line-height: 24px; width: 190px; .name { font-size: 16px; color: #666; } .desc { color: #999; } .price { font-size: 22px; color: @priceColor; i { font-size: 16px; } } } } } } } &:hover { .layer { display: block; } } ``` 1. 渲染逻辑:src/views/home/components/home-category.vue - 定义一个数据记录当前鼠标经过分类的ID,使用计算属性得到当前的分类推荐商品数据 ```vue
    ``` ## 22-首页主体-补充-vue动画 **目标:** 知道vue中如何使用动画,知道Transition组件使用。
    当vue中,显示隐藏,创建移除,一个元素或者一个组件的时候,可以通过transition实现动画。
    ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27272831/1658927951154-f819f1c7-19a6-459f-af19-f2a771ff7524.png#clientId=uaac27d05-8ce4-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u9220b12a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=318&originWidth=755&originalType=url&ratio=1&rotation=0&showTitle=false&size=42198&status=done&style=none&taskId=uc44c9640-d447-4105-a951-a5d23f5aa00&title=)
    如果元素或组件离开,完成一个淡出效果: ```vue

    100

    ``` ```less .fade-leave { opacity: 1 } .fade-leave-active { transition: all 1s; } .fade-leave-to { opcaity: 0 } ``` - 进入(显示,创建) - v-enter 进入前 (vue3.0 v-enter-from) - v-enter-active 进入中 - v-enter-to 进入后 - 离开(隐藏,移除) - v-leave 进入前 (vue3.0 v-leave-from) - v-leave-active 进入中 - v-leave-to 进入后 多个transition使用不同动画,可以添加name属性,name属性的值替换v即可。 ## 23-首页主体-面板骨架效果 **目的:** 加上面板的骨架加载效果
    定义一个骨架布局组件:
    src/views/home/components/home-skeleton.vue ```vue ``` 在 home-hot home-new 组件分别使用 ```vue +
    + +
    • 首页模块 - 图15

      {{item.title}}

      {{item.alt}}

    + +
    +
    ``` ```vue ``` 在 src/assets/styles/common.less 定义动画 ```less .fade{ &-leave { &-active { position: absolute; width: 100%; transition: opacity .5s .2s; z-index: 1; } &-to { opacity: 0; } } } ``` 注意: - 动画的父容器需要是定位,防止定位跑偏。 ## 24-首页主体-组件数据懒加载 **目的:** 实现当组件进入可视区域再加载数据。
    我们可以使用 @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
    // 省略。。。 ``` 3. 由于首页面板数据加载都需要实现懒数据加载,所以封装一个钩子函数,得到数据。 src/hooks/index.js ```javascript // hooks 封装逻辑,提供响应式数据。 import { useIntersectionObserver } from '@vueuse/core' import { ref } from 'vue' // 数据懒加载函数 export const useLazyData = (apiFn) => { // 需要 // 1. 被观察的对象 // 2. 不同的API函数 const target = ref(null) const result = ref([]) const { stop } = useIntersectionObserver( target, ([{ isIntersecting }], observerElement) => { if (isIntersecting) { stop() // 调用API获取数据 apiFn().then(data => { result.value = data.result }) } } ) // 返回--->数据(dom,后台数据) return { target, result } } ``` 4. 再次改造 home-new 组件:rc/views/home/components/home-new.vue ```javascript import { findNew } from '@/api/home' +import { useLazyData } from '@/hooks' export default { name: 'HomeNew', components: { HomePanel, HomeSkeleton }, setup () { + const { target, result } = useLazyData(findNew) + return { goods: result, target } } } ``` ```vue +
    ``` 5. 然后改造 home-hot 组件:src/views/home/components/home-hot.vue ```vue +
    import { findHot } from '@/api/home' import HomePanel from './home-panel' import HomeSkeleton from './home-skeleton' +import { useLazyData } from '@/hooks' export default { name: 'HomeHot', components: { HomePanel, HomeSkeleton }, setup () { + const { target, result } = useLazyData(findHot) + return { target, list: result } } } ``` ## 25-首页主体-热门品牌 **目的:** 实现品牌的展示,和切换品牌效果。
    **基本步骤:** - 准备基础布局组件 - 获取数据实现渲染,完成切换效果 - 加上骨架效果和数据懒加载 **落的代码:** 1. 基础结构:src/views/home/components/home-brand.vue ```vue
    
    - 使用组件:src/views/home/index.vue
    ```vue
     <!-- 人气推荐 -->
        <HomeHot />
        <!-- 热门品牌 -->
    +    <HomeBrand />
    
    1. 获取数据和切换效果:
    • 由于最后会使用到数据懒加载,那么我们也会使用组合API实现。
    • 业务上,只有两页数据切换,0—->1 或者 1—->0 的方式。 ```vue
    ``` 1. 加上数据懒加载和骨架效果 ```vue ``` **总结:** 注意下useLazyData传参的情况。 ## 26-首页主体-商品区块 **目的:** 完成商品区域展示。
    **大致步骤:** - 准备一个商品盒子组件 home-goods 展示单个商品 - 定义产品区块组件 home-product 使用 home-goods 完成基础布局 - 在首页中使用 home-product 组件 - 定义API函数,获取数据,进行渲染 - 处理板块需要进入可视区太多内容才能加载数据问题。 **落地代码:** 1. 单个商品组件:src/views/home/components/home-goods.vue ```vue
    
    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>
    
    1. 使用组件: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 }
      }
      
    1. 获取数据渲染:
    • 定义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">&yen;{{goods.price}}</p>
        <div class="extra">
          <RouterLink to="/">
            <span>找相似</span>
            <span>发现现多宝贝 &gt;</span>
          </RouterLink>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'HomeGoods',
    +  props: {
    +    goods: {
    +      type: Object,
    +      default: () => {}
    +    }
    +  }
    }
    </script>
    
    1. 处理问题:
    • 产品区域需要滚动比较多才能去加载数据。 ```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 ```vue

    使用组件: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

    ```

    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属性。