Skip to content

完成底部渲染与首页轮播图功能实现(Lesson15 1/2) ​

本页对应 Lesson15,共 6 段视频,约 1 小时 39 分。

学习目标 ​

  • 能够根据当前路由配置控制底部组件的显示与隐藏,使登录、注册页面仅渲染底部局部内容,其他页面渲染完整底部内容。
  • 能够使用 Swiper/Vue-Awesome-Swiper 在首页完成轮播图功能,并配置自动播放、循环、分页点、左右切换及图片数据渲染。
  • 能够安装并配置 Mock.js,在入口文件中引入 Mock 配置,通过指定请求地址与请求方式拦截 Ajax 请求并返回模拟响应数据。
  • 能够封装独立的 mockRequest 请求实例,配置基础地址与请求拦截/响应处理,并在业务组件中调用封装后的请求获取模拟数据。
  • 能够按业务类型封装 API 模块,将轮播图接口统一管理并在组件中通过导入 API 方法发起请求。
  • 能够为 Vuex 增加 adv 模块,定义 state、mutations、actions,开启命名空间,并通过 dispatch 获取数据、commit 更新状态后渲染轮播图。

知识点 ​

底部渲染的需求与判断思路 ​

为什么需要:登录、注册页面与其它页面的底部不一样。其它页面显示“购物指南、配送方式、支付方式、售后服务、特色服务、帮助中心”等完整底部;登录、注册页面只显示最下面的版权信息。
怎么想:不要把“登录/注册”写死在 Footer 里判断路径,否则以后新增类似页面还要改组件。更稳的做法是:在路由上标记“是否隐藏完整底部”,Footer 只读这个标记。
怎么做:先抽出版权部分为 CopyRight,再在 Footer 中用 v-if 控制完整底部,登录/注册路由的 meta 设置 isHideFooter: true。
易错点:meta 字段名要统一;v-if 要取反,因为字段表示“隐藏”;v-else 必须紧挨 v-if。

为什么需要:版权部分在完整底部和登录/注册页都会出现,抽成组件可复用,避免复制两份。
怎么想:把原 Footer 中最下面的地址、备案号、帮助链接单独放到 src/components/CopyRight/index.vue,Footer 再引入。
怎么做:新建组件并保留原结构;图片标签补中文 alt。
易错点:组件 name、引入路径、样式 scoped 要对应;笔记中 CopyRight 路径与 Footer 引入路径存在不一致,落地时按实际目录调整。

vue
<template>
    <div class="copyright">
        <ul class="helpLink">
            <li>关于我们
                <span class="space"></span>
            </li>
            <li>联系我们
                <span class="space"></span>
            </li>
            <li>关于我们
                <span class="space"></span>
            </li>
            <li>商家入驻
                <span class="space"></span>
            </li>
            <li>营销中心
                <span class="space"></span>
            </li>
            <li>友情链接
                <span class="space"></span>
            </li>
            <li>关于我们
                <span class="space"></span>
            </li>
            <li>营销中心
                <span class="space"></span>
            </li>
            <li>友情链接
                <span class="space"></span>
            </li>
            <li>关于我们</li>
        </ul>
        <p>地址:北京市昌平区宏福科技园综合楼6层</p>
        <p>京ICP备19006430号</p>
    </div>
</template>

<script>
export default {
    name: "CopyRight"
}
</script>

<style lang="less" scoped>
.copyright {
    padding: 20px;

    .helpLink {
        text-align: center;

        li {
            display: inline;

            .space {
                border-left: 1px solid #666;
                width: 1px;
                height: 13px;
                background: #666;
                margin: 8px 10px;
            }
        }
    }

    p {
        margin: 10px 0;
        text-align: center;
    }
}
</style>

为什么需要:完整底部不是所有页面都要显示,登录/注册只要版权部分。
怎么想:Footer 里放两套结构:完整底部 + 版权;用路由 meta.isHideFooter 判断。字段为 true 时隐藏完整底部,只留 CopyRight。
怎么做:完整底部外层写 v-if="!$route.meta.isHideFooter",下面写 <CopyRight v-else></CopyRight>。
易错点:不要漏掉取反 !;v-else 必须紧邻;CopyRight 要注册;图片补中文 alt。

vue
<template>
    <!-- 底部 -->
    <div v-if="!$route.meta.isHideFooter" class="footer">
        <div class="footer-container">
            <div class="footerList">
                <div class="footerItem">
                    <h4>购物指南</h4>
                    <ul class="footerItemCon">
                        <li>购物流程</li>
                        <li>会员介绍</li>
                        <li>生活旅行/团购</li>
                        <li>常见问题</li>
                        <li>购物指南</li>
                    </ul>

                </div>
                <div class="footerItem">
                    <h4>配送方式</h4>
                    <ul class="footerItemCon">
                        <li>上门自提</li>
                        <li>211限时达</li>
                        <li>配送服务查询</li>
                        <li>配送费收取标准</li>
                        <li>海外配送</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>支付方式</h4>
                    <ul class="footerItemCon">
                        <li>货到付款</li>
                        <li>在线支付</li>
                        <li>分期付款</li>
                        <li>邮局汇款</li>
                        <li>公司转账</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>售后服务</h4>
                    <ul class="footerItemCon">
                        <li>售后政策</li>
                        <li>价格保护</li>
                        <li>退款说明</li>
                        <li>返修/退换货</li>
                        <li>取消订单</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>特色服务</h4>
                    <ul class="footerItemCon">
                        <li>夺宝岛</li>
                        <li>DIY装机</li>
                        <li>延保服务</li>
                        <li>尚品汇E卡</li>
                        <li>尚品汇通信</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>帮助中心</h4>
                    <img src="./images/wx_cz.jpg" alt="微信二维码">
                </div>
            </div>
            <CopyRight></CopyRight>
        </div>
    </div>
    <CopyRight v-else></CopyRight>
</template>

<script>
import CopyRight from "@/components/Footer/CopyRight";
export default {
    name: "Footer",
    components: {CopyRight}
}
</script>

在路由 meta 中标记隐藏底部 ​

为什么需要:登录、注册只是少数页面,改路由比在 Footer 里判断多个路径更灵活。
怎么想:默认不写 isHideFooter 就是显示完整底部;只有登录、注册设置 true。
怎么做:在 src/router/index.js 对应路由加 meta。
易错点:meta 是路由配置字段,不是组件 data;$route.meta.isHideFooter 读取当前路由;新增类似页面时只加路由标记即可。

js
	{
		path:"/login",
		component:Login,
		meta:{
			// 是否隐藏底部信息
			isHideFooter:true
		}
	},
	{
		path:"/Register",
		component:Register,
		meta:{
			// 是否隐藏底部信息
			isHideFooter:true
		}
	},

调试与易错点 ​

先在首页看完整底部是否正常,再点登录、注册,预期只显示版权部分。若没变化,按顺序查:路由是否写了 meta.isHideFooter: true;Footer 是否取反;CopyRight 是否注册成功;引入路径是否与实际目录一致。转写稿里反复强调“为 true 要隐藏,所以要取反”,这是最容易写反的地方。另一个坑是 CopyRight 被写了两份,导致登录页出现重复版权;用 v-if / v-else 后只保留一个分支即可。

为什么用 Swiper 而不是手写轮播图 ​

为什么需要:真实项目里手写轮播图效率低,兼容性、触摸滑动、分页和导航都要自己测,容易出问题。怎么想:用成熟插件 Swiper,再通过 vue-awesome-swiper 在 Vue2 中快速注册组件。怎么做:安装时指定版本,Vue2 对应 vue-awesome-swiper@4,它配套 swiper@5。易错点:不要直接装最新版,最新版多面向 Vue3;版本不匹配会出现组件注册或样式异常。

shell
cnpm install swiper@5 vue-awesome-swiper@4

资源入口可参考:Vue 资源列表 https://github.com/vuejs/awesome-vue,awesome 仓库 https://github.com/surmon-china/vue-awesome-swiper,示例 https://v1.github.surmon.me/vue-awesome-swiper/。

先定位并抽出 FocusAdv 组件 ​

为什么需要:首页内容多,轮播图应独立成组件,便于维护和数据接入。怎么想:转写中先在 Home 里搜索 banner1 等图片,定位到 class="center" 的区域,再把这一块抽出去。怎么做:新建 src/pages/Home/FocusAdv/index.vue,先在 Home 中引入并占位,确认图片能显示后再替换成 Swiper。易错点:图片相对路径层级要数清楚;抽组件后路径可能变化,先占位调试最稳。

轮播图组件代码 ​

为什么需要:模板负责结构,脚本负责注册组件和配置,样式负责尺寸。怎么想:Swiper、SwiperSlide 由插件提供,必须注册到当前组件;分页和左右按钮通过 slot 放进 Swiper。怎么做:保留下面代码,图片 alt 已改为中文。易错点:忘记注册组件会报“未知组件”;忘记引入 swiper/css/swiper.css 会导致分页、箭头样式错乱。

vue
<template>
    <div class="center">
        <swiper class="swiper" :options="swiperOption">
            <swiper-slide><img src="../../../assets/images/home/banner1.jpg" alt="首页轮播图一" /></swiper-slide>
            <swiper-slide><img src="../../../assets/images/home/banner2.jpg" alt="首页轮播图二" /></swiper-slide>
            <swiper-slide><img src="../../../assets/images/home/banner3.jpg" alt="首页轮播图三" /></swiper-slide>
            <swiper-slide><img src="../../../assets/images/home/banner4.jpg" alt="首页轮播图四" /></swiper-slide>
            <div class="swiper-pagination" slot="pagination"></div>
            <div class="swiper-button-prev" slot="button-prev"></div>
            <div class="swiper-button-next" slot="button-next"></div>
        </swiper>
    </div>
</template>

<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
    name: "FocusAdv",
    components: {
        Swiper,
        SwiperSlide
    },
    data() {
        return {
            swiperOption: {
                slidesPerView: 1,
                spaceBetween: 1,
                loop: false,
                pagination: {
                    el: '.swiper-pagination',
                    clickable: true
                },
                autoplay: {
                    delay: 3000,
                    stopOnLastSlide: false,
                    disableOnInteraction: true,
                },
                navigation: {
                    nextEl: '.swiper-button-next',
                    prevEl: '.swiper-button-prev'
                }
            }
        }
    }
}
</script>

<style lang="less" scoped>
.center {
    box-sizing: border-box;
    width: 740px;
    height: 100%;
    padding: 5px;
    float: left;
    .swiper{
        height:100%;
        img{
            width:100%;
        }
    }
}
</style>

配置项逐项调试 ​

为什么需要:配置项决定轮播行为,不能只复制不看效果。怎么想:转写中通过改值观察效果来理解配置。怎么做:slidesPerView:1 表示一屏一张;spaceBetween 是屏间距;loop:false 到最后不能继续右走,改 true 才循环;pagination.clickable:true 让小圆点可点击切换;autoplay.delay:3000 是 3 秒切换;disableOnInteraction:true 表示用户触碰、拖动、点分页后停止自动播放,改 false 则操作后重新启动。易错点:分页 el 的类名必须和模板一致;左右按钮不想要就删对应节点和配置;stopOnLastSlide 只在特定场景有意义,别和 loop 混淆。

图片与样式调整 ​

为什么需要:轮播图容器高度、图片宽度不统一会出现空隙。怎么想:转写中先让 .swiper 高度 100%,再让 img 宽度 100%,发现右侧或高度不满再调容器。怎么做:检查 .center 宽高、.swiper 高度、img 宽度,必要时给图片设 display:block 或高度。易错点:scoped 下样式选择器要落在当前组件内;不要只改图片而忽略 Swiper 容器高度。

数据接入:mock、request、api、vuex ​

为什么需要:接口未出时不能干等,先用 mock 造数据。怎么想:转写中把轮播图图片从 assets 移到 public,让 mock 数据里的 /images/... 能直接访问;再封装 mockRequest、api/adv.js,最后在 Vuex 增加 adv 模块。怎么做:入口文件引入 mock,mock 中按“地址+请求方式”拦截并返回响应体;api 里暴露获取轮播图列表的方法;Vuex 中写 state、mutation、action,组件 dispatch 后渲染 list。易错点:mock 不在入口引入不生效;被 mock 拦截的请求在网络面板里看不到;取数据时注意是响应体的 data;Vuex 模块要注册,开启命名空间后调用要带模块名。

为什么需要 mock ​

前端开发经常遇到“页面先做、接口没来”的情况。转写稿里说得很直白:UI、后端、前端是并行的,不能一直等接口;没有接口时可以先写虚拟数据,等真接口出来再替换。mockjs 的作用就是帮助前端程序员快速创建 API 接口用于模拟数据。为什么需要?因为要保证页面渲染和请求流程能先跑通。怎么想?先和接口约定好地址、请求方式、响应体结构,再用 mock 顶上去。怎么做?先下载模块:

shell
cnpm install mockjs

易错点:mock 只是开发阶段的模拟,不要把它当成正式后端;接口就绪后要能快速替换。

先想清楚 mock 拦截什么 ​

Mock.mock 的第一个参数是地址,第二个参数是请求方式,第三个参数是响应体。只有请求地址和请求方式同时匹配,才会被拦截。转写稿特别提醒:被拦截下来的 ajax 请求(xhr、fetch)在网络面板中是看不到的,但控制台可以打印出返回内容。怎么想?组件里写 axios.get("/my"),mock 里就必须写 "/my" 和 "get"。怎么做?把地址、方法、响应体三者对齐。易错点:组件用 get、mock 写 post,不会命中;地址少写斜杠或路径不一致,也不会命中;网络面板看不到请求不代表 mock 失败,要去看 console.log 的返回值。

新建 mock 目录并编写接口 ​

为什么需要单独建目录?因为模拟接口要集中管理,不能散落在组件里。怎么想?在 src 下新建 mock 目录,入口文件叫 index.js。怎么做?新建 src/mock/index.js:

js
import Mock from "mockjs";
import focusList from "./focus.json";
// 执行之后,那么在你发送ajax请求时会被拦截。
// 拦截的条件:
// 1- 请求地址为/my
// 2- 请求方式为get
// 注意:
// 1- mock接收的第三个参数是响应体,第一个参数是地址,第二个参数是请求方式
// 2- 被拦截下来的ajax请求(xhr,fetch)在网络中是无法查看的。
Mock.mock("/my","get",{
	ok:1,
	msg:"my->get->success",
	data:focusList
})

这里 focusList 来自 json 文件,最终作为响应体里的 data 返回。易错点:json 路径要写对;第三个参数是响应体,不要误以为它是配置项;Mock.mock 要被执行到,不能只定义不引入。

在项目入口引入 mock ​

为什么需要入口引入?因为 Mock.mock(...) 只有执行后才会注册拦截规则。怎么想?入口文件是项目启动必经之路,把 mock 放这里最稳。怎么做?在 src/index.js 中写:

js
import "@/mock";

易错点:忘记引入时,mock 文件等于没执行,组件请求会走真实网络或直接失败;路径别名 @ 要确认项目已配置。

在组件里发请求验证 ​

为什么要在组件里验证?因为最终要确认“请求地址 + 请求方式 + 响应体”能串起来。怎么想?在 mounted 里发请求,把返回内容打印出来。怎么做?在 src/views/Home/FocusAdv/index.vue 中:

js
mounted(){
    axios.get("/my").then(value=>{
        console.log(value.data);
    })
}

刷新后控制台能看到 { ok:1, msg:"my->get->success", data:... },说明请求被 mock 拦截并返回了模拟响应体。易错点:axios 要先引入;请求方法必须是 get;如果网络面板看不到 /my,不要慌,这是被 mock 拦截后的正常现象,重点看控制台输出。

调试过程与后续扩展 ​

转写稿里的调试思路是:先看控制台有没有响应体,再看请求地址和方法是否与 Mock.mock 一致;如果接口多,可以进一步封装 mockRequest,统一 baseURL,再按业务建 api/adv.js,最后在 vuex 的 action 里调用接口、提交 mutation 渲染轮播图。为什么需要封装?避免每个组件都写完整地址。怎么想?mock 地址可以先写成完整域名,后续替换真接口时只改一处。怎么做?先保证本节基本用法跑通,再封装请求实例和 API 文件。易错点:封装后 baseURL 与 mock 地址不一致;从响应体取数据时忘记取 data 里的业务字段,而不是整个响应对象。

为什么要封装 mockRequest ​

为什么需要:首页轮播图属于广告位,真实接口还没就绪时不能干等;用 mockjs 先模拟接口,再封装一个独立的 axios 实例,后续换真实接口只改地址和请求模块。
怎么想:mock 负责“造接口”,mockRequest 负责“发请求”,request/index.js 负责“统一出口”,组件只关心调用。
怎么做:先把图片放到 public/images,再写 mock 接口、封装请求、统一导出、组件调用。
易错点:只写 mock 文件但入口没引入,接口不会生效;baseURL 与 Mock.mock 地址不一致也拦不到。

准备图片与 mock 接口 ​

按笔记要求,将轮播图图片放置在 public/images 中,接口里用 /images/xxx 这类路径访问。
为什么需要:mock 返回的数据里包含图片地址,图片必须放在 public 下才能被直接访问。
怎么想:mock 数据是“响应体”,结构要和组件渲染所需结构一致。
怎么做:新建 src/mock/index.js:

js
import Mock from "mockjs";
import focusList from "./focus.json";
// 获取首页中轮播图的图片列表
// 请求方式:get
// 请求地址:
Mock.mock("http://mock.com/adv/focus","get",{
	ok:200,
	data:focusList
})

易错点:Mock.mock 第一个参数是完整地址,第二个是请求方式,第三个才是响应体;请求方式要和组件里的 get 对上。按转写稿强调,还要在入口文件引入 mock,例如 import "@/mock";,否则 mock 代码不执行。

创建 mockRequest 实例 ​

为什么需要:把 mock 请求单独封装,统一 baseURL、超时、进度条和响应体处理,不和真实请求混在一起。
怎么想:baseURL 设为 http://mock.com,组件里只写 /adv/focus,拼接后正好匹配 mock 地址。
怎么做:新建 src/request/mockRequest.js:

js
import axios from 'axios';
import nprogress from "nprogress";
import "nprogress/nprogress.css";
const mockRequest = axios.create({
	baseURL:"http://mock.com",
	timeout:5000
});
// 请求拦截
mockRequest.interceptors.request.use(config=>{
	nprogress.start();// 开启进度条
	return config;
});
// 响应拦截
mockRequest.interceptors.response.use(response=>{
	nprogress.done();// 结束进度条
	return response.data;// 返回响应体
},(err)=>{
	nprogress.done();// 结束进度条
	alert(err);
	return new Promise(()=>{});// 返回一个状态为pending的Promise链
});
export default mockRequest;

易错点:响应拦截器已经 return response.data,组件里再取 value.data 时,拿到的是 mock 响应体里的 data 字段;错误处理返回 pending Promise 是为了避免未捕获错误继续往下抛。

统一导出请求模块 ​

为什么需要:组件不应该到处记具体请求文件路径,统一从 @/request 导入。
怎么想:真实请求 sphRequest 和模拟请求 mockRequest 并列导出。
怎么做:src/request/index.js:

js
import sphRequest from "@/request/sphRequest";
import mockRequest from "@/request/mockRequest";
export {
	// 调用尚品汇相关接口
	sphRequest,
	mockRequest
}

易错点:导出名和组件导入名要一致;路径别名 @ 要能正常解析。

组件中调用并验证 ​

为什么需要:验证 mock 接口、请求封装、响应体层级是否全链路正确。
怎么想:组件只调用 mockRequest.get("/adv/focus"),打印 value.data 看轮播图数组。
怎么做:在 src/pages/Home/FocusAdv/index.vue 中:

vue
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import {mockRequest} from "@/request";
import 'swiper/css/swiper.css'
export default {
    name: "FocusAdv",
    components: {
        // 将vue-awesome-swiper提供的组件注册为当前组件
        Swiper,
        SwiperSlide
    },
    data() {
        return {
            // 配置项
            swiperOption: {
                // 显示多少屏
                slidesPerView: 1,
                // 屏之间的间隔
                spaceBetween: 1,
                // 是否循环
                loop: false,
                pagination: {
                    // 分页
                    el: '.swiper-pagination',
                    // 点击小圆点是否进行切换
                    clickable: true
                },
                autoplay: {
                    // 切换的间隔时间为3秒
                    delay: 3000,
                    // 如果设置为true,当切换到最后一个slide时停止自动切换
                    stopOnLastSlide: false,
                    // 用户操作swiper之后,是否禁止autoplay。默认为true:停止。
                    // 如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
                    // 操作包括触碰(touch),拖动,点击pagination等。
                    disableOnInteraction: true,
                },
                navigation: {
                    nextEl: '.swiper-button-next',
                    prevEl: '.swiper-button-prev'
                }
            }
        }
    },
    mounted(){
        mockRequest.get("/adv/focus").then(value=>{
            console.log(value.data);
        })
    }
}
</script>

易错点:控制台能打印出数组,说明 mock 拦截成功;被 mock 拦截的 Ajax 在浏览器 Network 面板里通常看不到。若请求失败,依次检查:入口是否引入 mock、baseURL 是否为 http://mock.com、Mock.mock 地址是否为 http://mock.com/adv/focus、请求方式是否为 get、图片是否已放到 public/images。

为什么要把广告接口单独封装 ​

为什么需要:首页轮播图属于广告位,后面还可能继续出现“品牌广告”“商品推广广告”等接口。如果每个组件里都直接写 mockRequest("/adv/focus"),地址一改就要满项目搜;而且组件会同时知道“请求库”和“接口地址”,职责变重。

怎么想:转写稿里反复强调,页面里大量工作就是“调接口、渲染数据”。组件只应该关心“我要广告轮播数据”,不关心这个数据是 mock 来的还是真实后端来的。所以把广告相关请求集中到 src/api/adv.js,组件只导入函数。

怎么做:在 src/api/adv.js 中从 @/request 引入 mockRequest,导出一个获取首页轮播图列表的函数:

js
// 提供与广告相关的接口
import {mockRequest} from "@/request";
// 获取首页中轮播图的图片列表
export const  getFocusList = ()=> mockRequest("/adv/focus") ;

易错点:@/request 的 @ 是项目别名,不要写成相对路径却少层级;导出用 export const,组件导入时必须用 { getFocusList };函数体是返回 mockRequest(...) 的结果,不要漏 return 或把大括号写成普通函数体却不返回。

先想清楚:组件只拿数据,不拼接口地址 ​

为什么需要:转写稿中讲到,真实项目里 UI、后端、前端并行推进,接口没出来时前端不能干等,可以用 mock 数据先联调。封装 mockRequest 后,组件调用 /adv/focus,等真实接口确定,只改请求层或 API 层,组件尽量不动。

怎么想:api/adv.js 是“广告接口目录”,只放和广告相关的接口;商品、搜索等不要混进来。getFocusList 命名表达“获取焦点图列表”,返回 Promise。

怎么做:组件里通过 import {getFocusList} from "@/api/adv"; 引入,在 mounted 中调用,用 .then 接收结果。转写稿调试时先 console.log(value.data),确认 mock 拦截成功、响应体里的 data 能取到,再继续渲染。

易错点:不要误以为 value 就是数组;笔记里打印的是 value.data,说明响应体还有一层。地址 /adv/focus 必须和 mock 拦截地址一致,否则请求不会被拦截,控制台会报错或拿不到假数据。

在 FocusAdv 中接入 Swiper 并调用接口 ​

为什么需要:轮播图不自己从零写,转写稿明确说真实项目一般用第三方插件,自己写兼容性和效率都差。这里使用 vue-awesome-swiper,它把 Swiper 封装成 Vue 组件,注册后即可在模板中使用。

怎么想:先注册 Swiper、SwiperSlide,再引入 Swiper 样式;swiperOption 是配置对象,控制显示屏数、间距、分页、自动播放和左右按钮。接口调用放在 mounted,因为此时组件已挂载,适合发请求。

怎么做:保留笔记代码:

vue
  
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import {getFocusList} from "@/api/adv";
import 'swiper/css/swiper.css'
export default {
    name: "FocusAdv",
    components: {
        // 将vue-awesome-swiper提供的组件注册为当前组件
        Swiper,
        SwiperSlide
    },
    data() {
        return {
            // 配置项
            swiperOption: {
                // 显示多少屏
                slidesPerView: 1,
                // 屏之间的间隔
                spaceBetween: 1,
                // 是否循环
                loop: false,
                pagination: {
                    // 分页
                    el: '.swiper-pagination',
                    // 点击小圆点是否进行切换
                    clickable: true
                },
                autoplay: {
                    // 切换的间隔时间为3秒
                    delay: 3000,
                    // 如果设置为true,当切换到最后一个slide时停止自动切换
                    stopOnLastSlide: false,
                    // 用户操作swiper之后,是否禁止autoplay。默认为true:停止。
                    // 如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
                    // 操作包括触碰(touch),拖动,点击pagination等。
                    disableOnInteraction: true,
                },
                navigation: {
                    nextEl: '.swiper-button-next',
                    prevEl: '.swiper-button-prev'
                }
            }
        }
    },
    mounted(){
        getFocusList().then(value=>{
            console.log(value.data);
        })
    }
}
</script>

易错点:Swiper、SwiperSlide 没注册会报组件不存在;样式路径要按当前依赖版本写;pagination.el 和 navigation 的选择器必须在模板中存在对应元素;delay: 3000 单位是毫秒;disableOnInteraction: true 表示用户操作后停止自动播放。转写稿调试时先打印数据,不要急着渲染,确认接口层通了再往下做。

这一节要解决什么问题 ​

首页轮播图属于广告位,数据不应写死在组件里。我们需要把“广告数据”放进 Vuex 的独立模块 adv,由 action 调接口、mutation 改状态,组件只负责渲染。这样轮播图数据和组件解耦,后面接真实接口时只改 API 层,不动组件结构。

为什么需要独立模块:首页除了商品数据,还有广告、品牌等数据,全塞进一个 store 会乱。怎么想:广告相关状态、修改方法、异步请求集中到 store/adv.js,再在 store/index.js 注册。怎么做:先写模块,再注册,再在组件里 dispatch。易错点:模块开启 namespaced: true 后,调用 action 必须带模块名,如 adv/getFocusListAsync,不能只写 getFocusListAsync。

定义 adv 模块 ​

adv.js 中 state.focusList 初始为数组,避免组件渲染时取到 undefined。mutations.SAVE_FOCUS_LIST 只做同步赋值;actions.getFocusListAsync 负责异步请求,拿到响应体中的 data 后 commit。注意这里 const { data } = await getFocusList() 解构的是接口返回对象里的 data,不是整个响应。

js
import {getFocusList} from "@/api/adv";

const state = {
	// 首页轮播图信息
	focusList: []
};
const mutations = {
	SAVE_FOCUS_LIST(state,focusList){
		state.focusList = focusList;
	}
};
const actions = {
	async getFocusListAsync({commit}){
		const {data} = await getFocusList();
		commit("SAVE_FOCUS_LIST",data);
	}
}
export default {
	namespaced:true,
	state,
	mutations,
	actions
}

易错点:模块内部 commit("SAVE_FOCUS_LIST") 不用写 adv/;只有组件 dispatch 时才带命名空间。mutation 名要和 commit 名一致,大小写别写错。

在 store/index.js 注册模块 ​

为什么需要注册:adv.js 只是导出对象,不注册不会进入 store。怎么想:和已有 product 模块并列。怎么做:import adv from "@/store/adv",放进 modules。易错点:路径、文件名大小写要一致;注册后可在 Vue Devtools 的 Vuex 面板看到 adv/focusList。

js
import Vue from "vue";
import Vuex from "vuex";
import product from "@/store/product";
import adv from "@/store/adv";
Vue.use(Vuex);
const store = new Vuex.Store({
	modules:{
		product,
		adv
	}
});
export default store;

组件中渲染轮播图数据 ​

为什么用 vue-awesome-swiper:转写稿强调真实项目一般不手写轮播图,兼容性和效率都差。Vue2 项目要选对应版本,安装时注意 vue-awesome-swiper 4.x 搭配 swiper 5.x,不要直接装最新版导致只支持 Vue3。组件里注册 Swiper、SwiperSlide,引入 swiper 样式;模板用 v-for 遍历 $store.state.adv.focusList,key 用 item.id,图片地址用 item.imgUrl。

vue
<template>
    <div class="center">
        <swiper class="swiper" :options="swiperOption">
            <swiper-slide v-for="item in $store.state.adv.focusList" :key="item.id">
                <img :src="item.imgUrl" />
            </swiper-slide>
            <div class="swiper-pagination" slot="pagination"></div>
            <div class="swiper-button-prev" slot="button-prev"></div>
            <div class="swiper-button-next" slot="button-next"></div>
        </swiper>
    </div>
</template>

<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
    name: "FocusAdv",
    components: {
        // 将vue-awesome-swiper提供的组件注册为当前组件
        Swiper,
        SwiperSlide
    },
    data() {
        return {
            // 配置项
            swiperOption: {
                // 显示多少屏
                slidesPerView: 1,
                // 屏之间的间隔
                spaceBetween: 1,
                // 是否循环
                loop: false,
                pagination: {
                    // 分页
                    el: '.swiper-pagination',
                    // 点击小圆点是否进行切换
                    clickable: true
                },
                autoplay: {
                    // 切换的间隔时间为3秒
                    delay: 3000,
                    // 如果设置为true,当切换到最后一个slide时停止自动切换
                    stopOnLastSlide: false,
                    // 用户操作swiper之后,是否禁止autoplay。默认为true:停止。
                    // 如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
                    // 操作包括触碰(touch),拖动,点击pagination等。
                    disableOnInteraction: true,
                },
                navigation: {
                    nextEl: '.swiper-button-next',
                    prevEl: '.swiper-button-prev'
                }
            }
        }
    },
    mounted(){
        this.$store.dispatch("adv/getFocusListAsync");
    }
}
</script>

怎么触发:mounted 中 dispatch("adv/getFocusListAsync"),action 请求成功后 commit,state 更新,模板自动重渲染。易错点:没注册 Swiper/SwiperSlide 会报未知组件;没引入 swiper CSS 分页和箭头样式会丢;容器没有宽高时图片可能不显示;focusList 为空时先检查 Vuex 面板和接口返回;图片 404 要检查 imgUrl 路径。转写稿还提到 mock 阶段请求可能被拦截,在网络面板看不到,可用 console 看返回;mock 入口要在项目入口引入,否则代码不执行。最后在 Devtools 看到 adv.focusList 有数据,再回到页面确认轮播图正常切换即可。

今日推荐的数据接口:先用 Mock 造出广告位数据 ​

为什么需要:今日推荐是轮播图底部的广告推荐商品区,结构是“时钟图标 + 4 张商品图”。真实接口没就绪时,不能干等后台;先用 Mock 造出与最终接口一致的数据,页面就能先渲染、先联调样式。

怎么想:这个区域本质还是“调接口 → 拿数组 → v-for 渲染图片”。接口地址定为 /adv/topToday,请求方式 GET,响应体里给 ok:200 和 data 数组,每个元素包含 id、imgUrl。转写稿里反复强调:Mock 的拦截条件是“请求地址 + 请求方式”都匹配,所以 Mock 地址、api 封装地址、请求方法要一致。

怎么做:在 src/mock/index.js 中写:

js
// 获取轮播图底部的广告推荐商品
Mock.mock("http://mock.com/adv/topToday","get",{
	ok:200,
	data:[
		{
			id:1,
			imgUrl:"https://img14.360buyimg.com/n7/jfs/t1/160926/36/39154/42521/64cb95fbF17e9f5e2/46825eec5d36a9ac.jpg"
		},
		{
			id:2,
			imgUrl:"https://img12.360buyimg.com/n7/jfs/t1/114451/20/35763/328474/64cc52aeF75e02e83/a679d35a86500c90.png"
		},
		{
			id:3,
			imgUrl:"https://img10.360buyimg.com/n7/jfs/t1/118684/21/39853/31096/649182eaF26d07904/02445a2475df50e1.jpg"
		},{
			id:4,
			imgUrl:"https://img11.360buyimg.com/n7/jfs/t1/131081/19/39374/78410/64c77f21F7c291b6a/cb18f952320466fd.jpg"
		}
		
	]
})

易错点:Mock 方法名写 "get",不要写成 "GET";Mock 文件必须在项目入口引入执行,否则不会拦截;请求地址要和 mockRequest 的 baseURL 拼出来一致,否则网络面板里看不到请求,页面也拿不到数据。

api 层:把“今日推荐”请求收口 ​

为什么需要:组件里不要直接写请求地址和请求库。转写稿的推导是:接口按业务分文件,广告相关接口放 api/adv.js,以后换真实域名只改封装层。

怎么想:getTopToday 只负责“发请求”,不负责存数据、不负责渲染。它返回 Promise,store 的 action 里 await 它。

怎么做:在 src/api/adv.js 中:

js
// 获取轮播图底部的今日推荐
export const getTopToday = ()=> mockRequest("/adv/topToday");

易错点:笔记省略了 mockRequest 的导入,实际要按你项目封装位置引入;如果 mockRequest 的 baseURL 是 http://mock.com,这里写 /adv/topToday 才能匹配 Mock。

Vuex 的 adv 模块:存列表、改列表、发请求 ​

为什么需要:今日推荐列表是跨组件共享的广告数据,放进 Vuex 后,组件只负责 dispatch 和渲染,数据流清晰。

怎么想:state 存 topTodayList;mutation 只做同步赋值;action 发异步请求,拿到响应体中的 data 后 commit。转写稿特别提醒:const {data} = await getTopToday() 里的 data 是“响应体里的 data”,不是整个响应对象。

怎么做:在 src/store/adv.js 中:

js
import {getFocusList, getTopToday} from "@/api/adv";

const state = {
	// 今日推荐
	topTodayList:[],
};
const mutations = {
	SAVE_TOP_TODAY_LIST(state,topTodayList){
		state.topTodayList = topTodayList;
	}
};
const actions = {
	async getTopTodayAsync({commit}){
		const {data} = await getTopToday();
		commit("SAVE_TOP_TODAY_LIST",data);
	}
}
export default {
	namespaced:true,
	state,
	mutations,
	actions
}

易错点:namespaced:true 后,组件里必须写 adv/getTopTodayAsync;还要在 store/index.js 的 modules 里注册 adv,否则 dispatch 报 unknown action;mutation 名要和 commit 一致。

组件渲染:mounted 触发请求,v-for 渲染图片 ​

为什么需要:今日推荐区域要固定显示时钟标题,再动态渲染商品图。用 v-for 渲染 topTodayList,以后接口换图不用改模板。

怎么想:mounted 是组件挂载后触发一次请求;模板里第一个 li 是时钟,后面的 li.banner 循环列表;key 用 item.id,图片地址绑定 item.imgUrl。

怎么做:src/pages/Home/TodayRecomment.vue:

vue
<template>
    <div class="today-recommend">
        <div class="py-container">
            <ul class="recommend">
                <li class="clock">
                    <div class="time">
                        <img src="../images/clock.png" alt="时钟图标" />
                        <h3>今日推荐</h3>
                    </div>
                </li>
                <li class="banner" v-for="item in $store.state.adv.topTodayList" :key="item.id">
                    <img :src="item.imgUrl" alt="今日推荐商品图片" />
                </li>
            </ul>
        </div>
    </div>
</template>

<script>
export default {
    name: "TodayRecommend",
    mounted(){
        this.$store.dispatch("adv/getTopTodayAsync")
    }
}
</script>

<style lang="less" scoped>
.today-recommend {
    .py-container {
        width: 1200px;
        margin: 0 auto;

        .recommend {
            height: 165px;
            background-color: #eaeaea;
            margin: 10px 0;
            display: flex;

            .clock {
                width: 16.67%;
                background-color: #5c5251;
                color: #fff;
                font-size: 18px;
                text-align: center;

                .time {
                    padding: 30px 0;
                }

                h3 {
                    margin: 9px 0;
                    font-weight: 700;
                    font-size: 18px;
                    line-height: 30.06px;
                }
            }

            .banner {
                width: 20.83%;

                img {
                    width: 100%;
                    height: 100%;
                    transition: all 400ms;

                    &:hover {
                        opacity: 0.8;
                    }
                }
            }
        }
    }
}
</style>

易错点:mounted 里 dispatch 的命名空间不能漏;v-for 必须加 :key;imgUrl 是绝对地址,直接绑定即可;组件名 TodayRecommend 和文件名 TodayRecomment.vue 不完全一致时,引入/注册以实际文件路径为准,不要因为拼写差异找不到组件。

本节目标与整体拆分 ​

这一节要完成“底部品牌广告”相关功能:一是底部品牌区的按页面显示控制,二是首页轮播广告的数据渲染。先带做底部和轮播图,再把 mock、请求封装、Vuex 串起来。为什么需要:真实项目中不能等接口和 UI 全齐再写;怎么想:先用静态结构和假数据跑通,再替换成接口数据;怎么做:拆组件、配路由 meta、用 Swiper、造 mock、封 api、进 Vuex;易错点:不要把登录/注册逻辑写死在 Footer 里,不要手写轮播图。

底部品牌广告:用路由 meta 控制显示 ​

为什么需要:登录、注册页只显示底部一小部分品牌信息,其他页面显示完整底部;如果每个页面写死,新增页面就要改 Footer。怎么想:登录/注册是少数页面,改少数页面更划算,所以在路由上标记。怎么做:给登录、注册路由加 meta.hideBrand,Footer 里用 v-if 取反控制完整品牌区,v-else 显示简版。易错点:meta 默认是 undefined,判断时要取反;不要把完整底部复制两份后忘记拆子组件。

js
// router/index.js
{
  path: '/login',
  component: Login,
  meta: { hideBrand: true }
}
vue
<!-- Footer.vue -->
<template>
  <div class="footer">
    <BrandFull v-if="!$route.meta.hideBrand" />
    <BrandMini v-else />
  </div>
</template>

轮播图:用 vue-awesome-swiper 而不是手写 ​

为什么需要:手写轮播图要处理自动播放、循环、分页、左右箭头、兼容性,效率低且易出问题。怎么想:Vue2 项目用 vue-awesome-swiper 4.x,搭配 Swiper 5.x;不要直接装最新版,最新版可能只支持 Vue3。怎么做:安装指定版本,注册 Swiper、SwiperSlide,配置 slidesPerView、spaceBetween、loop、autoplay、pagination、navigation。易错点:组件没注册会报“未知组件”;loop 为 false 时最后一张不能继续右切;autoplay.disableOnInteraction 为 true 时用户操作后停止自动播放。

vue
<template>
  <swiper :options="swiperOption">
    <swiper-slide v-for="item in bannerList" :key="item.id">
      <img :src="item.imageUrl" alt="轮播广告图" />
    </swiper-slide>
    <div class="swiper-pagination" slot="pagination"></div>
    <div class="swiper-button-prev" slot="button-prev"></div>
    <div class="swiper-button-next" slot="button-next"></div>
  </swiper>
</template>

<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
export default {
  components: { Swiper, SwiperSlide },
  data() {
    return {
      swiperOption: {
        slidesPerView: 1,
        spaceBetween: 30,
        loop: true,
        autoplay: { delay: 3000, disableOnInteraction: false },
        pagination: { el: '.swiper-pagination', clickable: true },
        navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' }
      }
    }
  }
}
</script>

mock 的基本使用:先造数据不空等 ​

为什么需要:接口没出来时不能干等,否则页面无法推进。怎么想:用 Mock.js 拦截指定地址和请求方式的 XHR,返回假数据。怎么做:建 mock/index.js,用 Mock.mock(url, method, response);在入口 main.js 引入 ./mock。易错点:地址和请求方式必须完全匹配;mock 请求在 Network 里看不到;忘记在入口引入等于代码没执行。

js
// mock/index.js
import Mock from 'mockjs'
import bannerList from './bannerList.json'
Mock.mock('http://mock.com/adv/banner', 'get', {
  code: 200,
  data: bannerList
})
js
// main.js
import './mock'

封装 mockRequest 与 api 层 ​

为什么需要:每个组件直接写完整地址会散乱,后期换真实域名难改。怎么想:用 axios 实例统一 baseURL,api 文件按业务分类。怎么做:建 utils/mockRequest.js,再建 api/adv.js 暴露 getBannerList。易错点:baseURL 和 mock 地址拼接要一致;不要在 api 里重复写 http://mock.com。

js
// utils/mockRequest.js
import axios from 'axios'
export default axios.create({ baseURL: 'http://mock.com' })
js
// api/adv.js
import mockRequest from '@/utils/mockRequest'
export const getBannerList = () => mockRequest.get('/adv/banner')

Vuex adv 模块与组件渲染 ​

为什么需要:轮播数据要被组件消费,也方便后续扩展广告业务。怎么想:按模块管理,adv 模块存 bannerList,action 调 api,mutation 改状态。怎么做:store/modules/adv.js 写 state/mutations/actions,store/index.js 注册并开启 namespaced;组件 dispatch('adv/getBannerList'),用 mapState('adv', ['bannerList']) 渲染。易错点:开启命名空间后 dispatch 必须带模块名;mock 返回结构是 res.data.data,别少取一层;组件挂载时再发请求。

js
// store/modules/adv.js
import { getBannerList } from '@/api/adv'
export default {
  namespaced: true,
  state: { bannerList: [] },
  mutations: {
    SET_BANNER_LIST(state, list) { state.bannerList = list }
  },
  actions: {
    async getBannerList({ commit }) {
      const res = await getBannerList()
      commit('SET_BANNER_LIST', res.data.data)
    }
  }
}
js
computed: {
  ...mapState('adv', ['bannerList'])
},
mounted() {
  this.$store.dispatch('adv/getBannerList')
}

调试顺序与易错点汇总 ​

先静态结构,再抽 FocusAdv 组件;替换 Swiper 后如果报错,先查组件注册;页面无图先查图片路径和 mock 地址;Vuex 无数据先查模块是否注册、是否开启命名空间、dispatch 是否带 adv/。反复强调:这类页面 80% 是“调接口、渲染数据”,不要害怕,按“结构→假数据→接口→状态管理”的顺序推进即可。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

vue
<template>
    <div class="copyright">
        <ul class="helpLink">
            <li>关于我们
                <span class="space"></span>
            </li>
            <li>联系我们
                <span class="space"></span>
            </li>
            <li>关于我们
                <span class="space"></span>
            </li>
            <li>商家入驻
                <span class="space"></span>
            </li>
            <li>营销中心
                <span class="space"></span>
            </li>
            <li>友情链接
                <span class="space"></span>
            </li>
            <li>关于我们
                <span class="space"></span>
            </li>
            <li>营销中心
                <span class="space"></span>
            </li>
            <li>友情链接
                <span class="space"></span>
            </li>
            <li>关于我们</li>
        </ul>
        <p>地址:北京市昌平区宏福科技园综合楼6层</p>
        <p>京ICP备19006430号</p>
    </div>
</template>

<script>
export default {
    name: "CopyRight"
}
</script>

<style lang="less" scoped>
.copyright {
    padding: 20px;

    .helpLink {
        text-align: center;

        li {
            display: inline;

            .space {
                border-left: 1px solid #666;
                width: 1px;
                height: 13px;
                background: #666;
                margin: 8px 10px;
            }
        }
    }

    p {
        margin: 10px 0;
        text-align: center;
    }
}
</style>

要点解释与易错点

  • 这个组件是「登录/注册页」和「其它页面」共用的那一小部分底部,所以必须单独抽成组件,不能在 Footer 里复制两份 DOM。
  • 最后一个 <li> 后面不能再跟 <span class="space">,否则会出现多余竖线;这也是笔记里最后一个 关于我们 单独写的原因。
  • .space 是行内元素,width/height 对行内元素不生效,实际竖线是靠 border-left 撑出来的,别指望改宽高调间距,调 margin 即可。
  • 目录名要和引入路径一致:笔记中 Footer 里写的是 import CopyRight from "@/components/Footer/CopyRight",组件就应放在 src/components/Footer/CopyRight/index.vue,路径写错会直接报「找不到模块」。

vue
<template>
    <!-- 底部 -->
    <div v-if="!$route.meta.isHideFooter" class="footer">
        <div class="footer-container">
            <div class="footerList">
                <div class="footerItem">
                    <h4>购物指南</h4>
                    <ul class="footerItemCon">
                        <li>购物流程</li>
                        <li>会员介绍</li>
                        <li>生活旅行/团购</li>
                        <li>常见问题</li>
                        <li>购物指南</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>配送方式</h4>
                    <ul class="footerItemCon">
                        <li>上门自提</li>
                        <li>211限时达</li>
                        <li>配送服务查询</li>
                        <li>配送费收取标准</li>
                        <li>海外配送</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>支付方式</h4>
                    <ul class="footerItemCon">
                        <li>货到付款</li>
                        <li>在线支付</li>
                        <li>分期付款</li>
                        <li>邮局汇款</li>
                        <li>公司转账</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>售后服务</h4>
                    <ul class="footerItemCon">
                        <li>售后政策</li>
                        <li>价格保护</li>
                        <li>退款说明</li>
                        <li>返修/退换货</li>
                        <li>取消订单</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>特色服务</h4>
                    <ul class="footerItemCon">
                        <li>夺宝岛</li>
                        <li>DIY装机</li>
                        <li>延保服务</li>
                        <li>尚品汇E卡</li>
                        <li>尚品汇通信</li>
                    </ul>
                </div>
                <div class="footerItem">
                    <h4>帮助中心</h4>
                    <img src="./images/wx_cz.jpg">
                </div>
            </div>
            <CopyRight></CopyRight>
        </div>
    </div>
    <CopyRight v-else></CopyRight>
</template>

<script>
import CopyRight from "@/components/Footer/CopyRight";
export default {
    name: "Footer",
    components: {CopyRight}
}
</script>

配套的路由元信息(src/router/index.js,登录/注册两条路由):

js
{
    path: "/login",
    component: Login,
    meta: {
        // 是否隐藏底部信息
        isHideFooter: true
    }
},
{
    path: "/Register",
    component: Register,
    meta: {
        // 是否隐藏底部信息
        isHideFooter: true
    }
}

要点解释与易错点

  • 核心是 v-if="!$route.meta.isHideFooter":取反是关键。isHideFooter 为 true 表示隐藏完整底部,只留 CopyRight。
  • 为什么用 isHideFooter 而不是 isShowFooter?因为「显示完整底部」的页面是多数,隐藏的只有登录/注册两个;默认 undefined 即 false(不隐藏),其它页面一行都不用改。如果反过来设计,所有页面都得加 meta,极易漏写。
  • v-if / v-else 必须紧挨着,中间不能插其它元素或注释,否则 v-else 会失效。
  • $route 是全局注入的,不用 import;但如果你要监听路由变化做逻辑,需改用 watch: { $route() {} }。
  • 样式(.footer、.footerList、.footerItem)笔记中未给出,沿用项目里已有的底部样式表即可;<img src="./images/wx_cz.jpg"> 用的是相对路径,图片必须放在本组件同级 images 目录下,否则编译报错。

示例 3:首页轮播图 src/pages/Home/FocusAdv/index.vue ​

vue
<template>
    <div class="center">
        <swiper class="swiper" :options="swiperOption">
            <swiper-slide v-for="item in $store.state.adv.focusList" :key="item.id">
                <img :src="item.imgUrl" />
            </swiper-slide>
            <div class="swiper-pagination" slot="pagination"></div>
            <div class="swiper-button-prev" slot="button-prev"></div>
            <div class="swiper-button-next" slot="button-next"></div>
        </swiper>
    </div>
</template>

<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
    name: "FocusAdv",
    components: {
        // 将vue-awesome-swiper提供的组件注册为当前组件
        Swiper,
        SwiperSlide
    },
    data() {
        return {
            // 配置项
            swiperOption: {
                // 显示多少屏
                slidesPerView: 1,
                // 屏之间的间隔
                spaceBetween: 1,
                // 是否循环
                loop: false,
                pagination: {
                    // 分页
                    el: '.swiper-pagination',
                    // 点击小圆点是否进行切换
                    clickable: true
                },
                autoplay: {
                    // 切换的间隔时间为3秒
                    delay: 3000,
                    // 如果设置为true,当切换到最后一个slide时停止自动切换
                    stopOnLastSlide: false,
                    // 用户操作swiper之后,是否禁止autoplay。默认为true:停止。
                    // 如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
                    // 操作包括触碰(touch),拖动,点击pagination等。
                    disableOnInteraction: true,
                },
                navigation: {
                    nextEl: '.swiper-button-next',
                    prevEl: '.swiper-button-prev'
                }
            }
        }
    },
    mounted(){
        this.$store.dispatch("adv/getFocusListAsync");
    }
}
</script>

<style lang="less" scoped>
.center {
    box-sizing: border-box;
    width: 740px;
    height: 100%;
    padding: 5px;
    float: left;
    .swiper{
        height:100%;
        img{
            width:100%;
        }
    }
}
</style>

配套的 Vuex 模块 src/store/adv.js 与接口 src/api/adv.js:

js
// src/api/adv.js
import {mockRequest} from "@/request";
export const getFocusList = () => mockRequest("/adv/focus");

// src/store/adv.js
import {getFocusList} from "@/api/adv";
const state = {
    // 首页轮播图信息
    focusList: []
};
const mutations = {
    SAVE_FOCUS_LIST(state, focusList){
        state.focusList = focusList;
    }
};
const actions = {
    async getFocusListAsync({commit}){
        const {data} = await getFocusList();
        commit("SAVE_FOCUS_LIST", data);
    }
}
export default {
    namespaced: true,
    state,
    mutations,
    actions
}

要点解释与易错点

  • 版本必须锁死:cnpm install swiper@5 vue-awesome-swiper@4。直接 install 装到的是支持 Vue3 的最新版,在 Vue2 项目里会报错或组件不渲染。
  • Swiper / SwiperSlide 必须先在 components 里注册,否则模板里的 <swiper> 会被当成未知标签,控制台报「did you register the component correctly」。
  • slot="pagination"、slot="button-prev"、slot="button-next" 这三个 slot 名是固定写法,同时 pagination.el: '.swiper-pagination'、navigation.nextEl/prevEl 的类名必须与模板中的 class 完全一致,写错就不显示小圆点或箭头。
  • autoplay 写成对象形式({delay: 3000}),不要写成 autoplay: true;disableOnInteraction: true 表示用户手动拖动后停止自动播放,改成 false 才会操作完继续轮播。
  • 渲染数据的根路径是 $store.state.adv.focusList,adv 就是模块的 namespaced: true 加在 store/index.js 的 modules 里注册的键名;dispatch("adv/getFocusListAsync") 同理,命名空间前缀不能漏,否则 action 找不到。
  • const {data} = await getFocusList() 中的 data 是 mockRequest 响应拦截器 return response.data 之后响应体里的 data,不是 axios 的响应体本身,解构层级容易写错导致 focusList 变成 undefined。
  • item.imgUrl 用的是 /images/xxx.jpg 这种绝对路径时,图片要放到 public/images 下,而不是 src/assets;放错位置会 404。

常见错误 ​

  • 登录/注册页仍显示完整底部 → 原因:meta.isHideFooter 未设置、字段名不一致,或 Footer 中 v-if 判断取反写错 → 修法:在 /login、/Register 的 meta 中设 isHideFooter:true,并核对 v-if="!$route.meta.isHideFooter" 与 v-else 的 CopyRight 分支。
  • 轮播图报 Unknown custom element: swiper/swiper-slide 或样式错乱 → 原因:vue-awesome-swiper 与 swiper 版本不匹配、组件未注册、CSS 未引入,或容器无宽高 → 修法:使用 swiper@5 + vue-awesome-swiper@4,局部注册 Swiper/SwiperSlide,引入 swiper/css/swiper.css,给 .swiper 和图片设置宽高。
  • mock 接口不生效,仍请求真实地址或 404 → 原因:入口未 import "@/mock",或 Mock.mock 的地址/方法与 mockRequest 的 baseURL + 路径 不一致 → 修法:入口引入 mock,保证 Mock.mock("http://mock.com/adv/focus","get",...) 与请求层完全匹配。
  • Network 面板看不到 mock 请求,误判接口失败 → 原因:mock 拦截的 XHR/fetch 不会在网络面板中显示 → 修法:用响应数据、console.log 或 Vuex 状态验证,不要只依赖 Network。
  • 轮播图图片 404 → 原因:mock 返回的 imgUrl 以 / 开头,但图片未放到 public/images 或路径不一致 → 修法:将图片放入 public/images,按实际 public 路径返回,如 /images/home/banner1.jpg。
  • Vuex 中 adv 数据不渲染 → 原因:模块未注册、未开启 namespaced 却按 adv/xxx 调用、mutation 名或字段名不一致、组件未 dispatch → 修法:在 store/index.js 注册 adv,设置 namespaced:true,核对 dispatch("adv/getFocusListAsync")、mutation 与模板字段。

练习 ​

  1. 实现登录/注册页只显示 CopyRight,其他页面显示完整 Footer;再新增一个“仅显示 CopyRight”的页面,要求用最少改动完成。
  2. 用 mock 提供 /adv/focus 与 /adv/topToday 两个接口,封装 mockRequest 和 api/adv.js,要求 baseURL 可统一切换。
  3. 将首页轮播图数据接入 Vuex 的 adv 模块,要求开启 namespaced,组件只 dispatch,不直接写 axios。
  4. 调整 Swiper 配置:自动播放 3 秒、循环、用户操作后继续自动播放、隐藏左右箭头但保留分页。
  5. 排查题:轮播图空白且控制台提示 Unknown custom element,请写出排查步骤与修复方案。

原视频对照 ​

#视频时长
1完成底部渲染16:22
2首页轮播图功能实现38:27
3mock的基本使用17:47
4封装mockRequest10:28
5封装api04:56
6vuex增加adv模块并渲染轮播图数据11:01

小结 ​

  • 底部差异显示应由路由 meta 控制,Footer 内部用条件分支切换完整版与精简版。
  • 轮播图优先使用成熟插件,Vue2 项目必须严格匹配 swiper 与 vue-awesome-swiper 版本。
  • mock 生效依赖入口引入、地址匹配、请求方式匹配,三者缺一不可。
  • 请求层应统一封装 baseURL、拦截器、进度条和响应体返回,页面不要散写 axios。
  • API 层按业务域拆分,组件通过 API 或 Vuex 取数,便于后续替换真实接口。
  • Vuex 模块必须注册并注意 namespaced 调用格式,action 调 API、mutation 改 state。
  • 静态资源放 public 时要使用匹配的绝对路径,避免图片 404。
  • 调试 mock 不要只看 Network,应结合响应数据、控制台与 Vuex 状态定位问题。