首页界面
本项目是一个电商项目的简易Demo
启动项目DEMO
将项目目录中的sph放置本地
进入到目录sph,安装依赖模块
shellcnpm i
package.json
json{ "scripts":{ "start":"node server.js" }, "dependencies": { "connect-history-api-fallback": "^2.0.0", "express": "^4.18.2", "http-proxy-middleware": "^2.0.6" } }
启动:
shellnpm start
通过脚手架搭建项目
选择默认版本vue2
shellvue create first # 选择默认安装 vue2 -->Default ([Vue 2] babel, eslint)
整理项目
复制尚品汇图标至public->favicon.ico
启动项目如果站标未发生改变,清除缓存,然后重新打开浏览器。
public->index.html
html<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <div id="app"></div> </body> </html>
vue.config.js
jsconst {defineConfig} = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, // publicPath:"./",// 更改BASE_URL devServer:{ open:true, host:"zhangpeiyue.com", port:80 }, pages: { index: { // page 的入口 entry: 'src/index.js', // 设置网站标题 title:"尚品汇" } } })
官方文档:https://cli.vuejs.org/zh/config/#pages
javascriptmodule.exports = { pages: { index: { // page 的入口 entry: 'src/index/main.js', // 模板来源 template: 'public/index.html', // 在 dist/index.html 的输出 filename: 'index.html', // 当使用 title 选项时, // template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title> title: 'Index Page', // 在这个页面中包含的块,默认情况下会包含 // 提取出来的通用 chunk 和 vendor chunk。 chunks: ['chunk-vendors', 'chunk-common', 'index'] }, // 当使用只有入口的字符串格式时, // 模板会被推导为 `public/subpage.html` // 并且如果找不到的话,就回退到 `public/index.html`。 // 输出文件名会被推导为 `subpage.html`。 subpage: 'src/subpage/main.js' } }
解决esling问题
src->index.js
jsimport Vue from "vue"; import App from "@/App"; const vm = new Vue({ el:"#app", render:h=>h(App) })
解决一,配置忽略:package.json
json{ .... "rules": { // 不用的变量忽略检查 "no-unused-vars":0, // 组件命名多个字母 "vue/multi-word-component-names": 0 } ..... }
解决二:整体忽略lint检查 vue.config.js
jsconst {defineConfig} = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, // 忽略lint检查 lintOnSave:false, // publicPath:"./",// 更改BASE_URL devServer:{ open:true, host:"zhangpeiyue.com", port:80 }, pages: { index: { // page 的入口 entry: 'src/index.js', // 设置网站标题 title:"尚品汇" } } })
解决三:在esling配置文件中更改 .eslintrc.js
javascriptmodule.exports = { root: true, env: { node: true }, 'extends': [ 'plugin:vue/essential', 'eslint:recommended' ], parserOptions: { parser: '@babel/eslint-parser' }, rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', "vue/multi-word-component-names": "off", "vue/valid-v-slot":"off", } }
将首页放置到App.vue组件中
swiper.min.css放到public->css->swiper.min.css
在public->index.html中引入css
html<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="stylesheet" href="<%= BASE_URL >css/swiper.min.css"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <div id="app"></div> </body> </html>
把home的css放在src->pages->Home->index.vue中的style标签中
vue...
把重置样式表reset.css放在index.js文件中导入
jsimport "src/assets/css/reset.css"
安装less
javascriptnpm install less-loader less -D // -D 在开发环境中使用
把图片放置在 src->assets->images中
修改html中的img的src地址为正确的地址
头部与底部组件
src->components->Header->index.vue
javascript把头部组件Header.vue从APP.vue中抽离出来, 同时把组件内的图片单独放置在组件中的文件夹 src->components->Header->images
src->components->Footer->index.vue
javascript把尾部组件Footer.vue从APP.vue中抽离出来 同时把组件内的图片单独放置在组件中的文件夹 src->components->Footer->images
src->App.vue
javascript...
创建基本的路由
首页:------> /
登陆: --------> /login
注册:---------->/register
搜索:------------>/search
安装vue-router
javascriptcnpm install vue-router@3 // vue-router 3版本对vue2支持
在src目录下创建4个页面 src->pages->Home,src->pages->login,src->pages->register,src->pages->search
src->router->index.js
javascriptimport Vue from "vue"; import VueRouter from "vue-router"; import Home from "@/pages/Home"; import Login from "@/pages/Login"; import Register from "@/pages/Register"; import Search from "@/pages/Search"; Vue.use(VueRouter); const routes = [ { path:"/", component:Home }, { path:"/login", component:Login }, { path:"/Register", component:Register }, { path:"/search", component:Search } ]; const router = new VueRouter({ mode:"history", routes }); export default router;
把首页Home.vue从App.vue中抽离出来
javascripthtml中的图片的src修改图片为 ../../assets/images/home/
src->App.vue
vue<template> <!-- 项目的最外层 --> <div> <!-- 头部 --> <Header></Header> <router-view/> <!-- 底部 --> <Footer></Footer> </div> </template> <script> import Home from "@/pages/Home"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; export default { name: "App", components: {Footer, Header, Home} } </script> <style lang="less" scoped> </style>
src->index.js
javascriptimport Vue from "vue"; import App from "@/App"; import router from "@/router"; import "@/assets/css/reset.css"; new Vue({ el:"#app", router, render:h=>h(App) })
实现导航切换
src->components->Header->index.vue
javascript<!--1--> <router-link to="/login">登录</router-link> <!--2--> <router-link to="/register" class="register">免费注册</router-link> <!--3--> <router-link to="/" class="logo" title="尚品汇"> <img src="./images/logo.png" alt=""> </router-link> <!--4--> <button @click="$router.push('/search')" class="sui-btn btn-xlarge btn-danger" type="button">搜索</button>
src->App.vue
javascript<!-- 匹配的路由出口 --> <router-view></router-view>
Push方法重写
- 解决的问题是:如果通过编程式导航,切换到当前路由时,那么会有异常。
原因:由于vue-route3之后对路由地址进行了判断,且采用了promise形式(如果通过编程式导航切换的地址和原来的地址相同,会返回一个失败的promise对象)。
解决的办法:
1 使用vue-router3.1之前的
2 处理异常:
// 方式1
this.$router.push('/search').then(value=>{
// 成功
},reason=>{
// 失败
})
// 方式2
this.$router.push('/search').catch(()=>{})
// 方式3:使用push的回调函数处理异常,push,replace接收的第一参数是地址,第二个参数是 成功回调,第三个参数是失败回调,
this.$router.push('/search',onComplete,onAbort)
src->router->index.js(方式1)
javascript// 1- 先将要加强的方法备份 const nativePush = VueRouter.prototype.push; const nativeReplace = VueRouter.prototype.replace; // 2- 重写push方法,并且使用push的回调函数onAbort,对失败的情况进行空处理,就不会报错。 VueRouter.prototype.push = function(location, onComplete, onAbort){ return nativePush.call(this,location, onComplete, ()=>{}) } VueRouter.prototype.replace = function(location, onComplete, onAbort){ return nativeReplace.call(this,location, onComplete, ()=>{}) }
js可选的在 router.push 或 router.replace 中提供 onComplete 和 onAbort 回调作为第二个和第三个参数。这些回调将会在导航成功完成 (在所有的异步钩子被解析之后) 或终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候进行相应的调用。
src->router->index.js(方式2)
javascript新建一个文件夹 utils/tools 用于存放自定义的工具 mkdir src->utils->pushReWrite.js
设置分类条组件
方式1:把typeNav设置为全局组件
// 1 src->components->TypeNav->index.vue
....从Home.vue中抽离出来
// 2 src->index.js
// 导入全局组件
import TypeNav from "@/components/TypeNav";
Vue.component("TypeNav", TypeNav);
...
// 3 src->pages->Home->index.vue
<TypeNav></TypeNav>
方式2:将typeNav放在Header中,由路由控制显示和隐藏
// 1 src->components->Header->TypeNav->index.vue
....把typeNav放在Header组件中
// 2 src->components->Header->index.vue
<template>
<!-- 商品分类栏 -->
<TypeNav v-if="$route.meta.isTypeNav"></TypeNav>
</template>
import TypeNav from "@/components/Header/TypeNav";
components: { TypeNav },
使用axios获取分类条数据
- 在 src->components->TypeNav->index.vue 组件中使用axios获取数据
npm install axios
// src->components->TypeNav->index.vue
<script>
export default {
name: "TypeNav",
}
</script>
- vue.config.js 配置代理服务器,注意重启服务
const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 忽略lint检查
lintOnSave:false,
devServer:{
open:true,
host:"zhangpeiyue.com",
port:80,
// 1
proxy:{
// 访问"/api"的路径,统一拼接为"http://sph-h5-api.atguigu.cn/api"
"/api":{
target:"http://sph-h5-api.atguigu.cn",
changeOrigin:true
}
}
},
pages: {
index: {
// page 的入口
entry: 'src/index.js',
// 设置网站标题
title:"尚品汇"
}
}
})
使用 nprogress 插件
使用 nprogress 插件,渲染axios加载进度条
下载nprogress https://www.npmjs.com/package/nprogress
javascriptnpm install nprogress
src->components->Header->TypeNav->index.vue
javascript<script> import axios from "axios"; import nprogress from "nprogress"; import "nprogress/nprogress.css" export default { name: "TypeNav", mounted() { // 请求地址:http://sph-h5-api.atguigu.cn/api/product/getBaseCategoryList // 请求方式:get nprogress.start();// 开始加载 axios.get("/api/product/getBaseCategoryList") .then(({data}) => { console.log(data); // 加载完成 nprogress.done(); },err=>{ console.log(err); nprogress.done; }) }, } </script>
nprogress的使用
javascriptAdd nprogress.js and nprogress.css to your project //添加nprogress.js and nprogress.css import nprogress from "nprogress"; // 在node_modules/nprogress/component.json中的main中指定了入口文件为nprogress.js import "nprogress/nprogress.css" // 使用 NProgress.start(); //加载 NProgress.done(); // 加载完成
axios封装
- 新建一个sphRequest.js文件(发送axios请求),src->request->sphRequest.js
import axios from "axios";
import nprogress from "nprogress";
import "nprogress/nprogress.css";
// 创建一个实例
const sphRequest = axios.create({
baseURL: "/api",
timeout: 5000
});
// 请求拦截器
sphRequest.interceptors.request.use(config => {
nprogress.start();// 开启进度条
return config;
});
// 响应拦截器
sphRequest.interceptors.response.use(response => {
nprogress.done();// 结束进度条
return response.data;// 返回响应体
},err=>{
nprogress.done();// 结束进度条
alert(err);//提示错误信息
return new Promise(() => { });//中断Promise
})
// 暴漏数据
export default sphRequest;
- 新建一个入口文件,src->request->index.js
// 引入请求
import sphRequest from "@/request/sphRequest";
// 暴漏数据
export {
sphRequest
}
- 在组件中使用 src->components->Header->TypeNav->index.vue
<script>
import {sphRequest} from "@/request"
export default {
name: "TypeNav",
mounted() {
sphRequest.get("/product/getBaseCategoryList")
.then(res=>{
console.log("res",res);
})
},
}
</script>
API封装
建议:按照后端提供的API进行分类
新建一个api文件夹,src->api->product.js
// 导入axios
import { sphRequest } from "@/request";
// 暴漏数据
export const getBaseCategoryList = function(){
return sphRequest("/product/getBaseCategoryList");
}
在页面中调用api方法
src->components->Header->typeNav->index.vue
...
<script>
// 导入api接口方法
import { getBaseCategoryList } from '@/api/product';
export default {
name: "TypeNav",
mounted() {
getBaseCategoryList()
.then(res=>{
console.log("res",res);
})
},
}
</script>
...
渲染TypeNav分类
src->components->Header->typeNav->index.vue
...
<!-- 全部商品分类 -->
<div v-for="c1 in categoryList" :key="c1.categoryId" class="item">
<h3>
<a>{{ c1.categoryName }}</a>
</h3>
<div class="item-list clearfix">
<div v-for="c2 in c1.categoryChild" :key="c2.categoryId" class="subitem">
<dl class="fore">
<dt>
<a href="">{{ c2.categoryName }}</a>
</dt>
<dd>
<em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
<a href="">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
...
<script>
// 导入api接口方法
import { getBaseCategoryList } from '@/api/product';
export default {
name: "TypeNav",
data(){
return{
// 分类数据
categoryList:[],
}
},
mounted() {
getBaseCategoryList()
.then(res=>{
this.categoryList = res.data.splice(0,15);
})
},
}
</script>
...
优化渲染内存
由于分类较多,每个分类均需要跳转,如果跳转使用组件router-link,会占用大量内存!
需要优化:使用事件委托,把router-link委托给父元素
src->components->Header->typeNav->index.vue
<div @click="$router.push('/search')" class="all-sort-list2"></div>
搜索页面默认隐藏商品分类
src->components->Header->TypeNav->index.vue
...
<!-- 在父元素设置离开 categroyConstricted 判断是否是首页-->
<div @mouseleave="categroyConstricted" class="nav-left">
<h2 @mouseenter="isShowCategory=true" class="all">全部商品分类</h2>
<div class="sort" v-show="isShowCategory">
...
</div>
</div>
...
<script>
export default {
name: "TypeNav",
data(){
return{
...
// 是否展开分类页面,当路径为/展开,默认展开
isShowCategory:this.$route.path === "/",
}
},
...
methods:{
// 分类展开方法,如果路径不是/ 不展开分类
categroyConstricted(){
if (this.$route.path !== '/'){
this.isShowCategory = false;
}
}
},
// 监听数据变化
watch:{
// 当路由路径发生变化,更新isShowCategory状态
"$route.path":{
handler(){
this.isShowCategory = this.$route.path === '/';
}
}
}
}
</script>
使用VueX保存数据
- 安装vueX
npm install vuex@3 //安装vuex大版本为3
- 新建store入口文件 src->store->index.js
src->store->index.js
import Vue from "vue";
import Vuex from "vuex";
//导入模块
import product from "@/store/product";
Vue.use(Vuex);
const store = new Vuex.Store({
modules:{
product
}
});
export default store;
- 新建product模块文件:src->store->product.js
// 导入api
import { getBaseCategoryList } from '@/api/product';
// 定义商品的数据状态
const state = {
// 首页分类列表
categoryList:[],
}
// 定义mutations
const mutations = {
// 修改state中的首页分类列表
UP_CATEGORY_LIST(state,categoryList){
state.categoryList = categoryList
}
}
// 定义actions
const actions = {
// 使用api接口获取数据
async getBaseCategoryListAsync({commit},num=1){
const { data } = await getBaseCategoryList();
commit("UP_CATEGORY_LIST",data.splice(0,num));
}
}
// 暴漏数据,导出模块
export default {
namespaced:true,
state,
mutations,
actions,
}
- 在vue入口文件中导入,并挂载
import Vue from "vue";
import App from "@/App";
import router from "@/router";
import store from "@/store";
import "@/assets/css/reset.css";
// 设置为 false 以阻止 vue 在启动时生成生产提示
Vue.config.productionTip = false;
new Vue({
el:"#app",
router,
store,
render:h=>h(App)
})
底部分别渲染
登陆,注册页面与其它的底部不同(只是其它页面中的一部分)
把版权作为子组件抽离出来:src->components->CopyRight->index.vue
// src->components->CopyRight->index.vue
<template lang="">
...
</template>
<script>
export default {
name:"CopyRight",
}
</script>
<style lang="less" scoped>
....
}
</style>
Footer父组件:src->components->Footer->index.vue
<template lang="">
<div v-if="!$route.meta.isHideFooterList" class="footer">
...
<CopyRight></CopyRight>
</div>
<CopyRight v-else></CopyRight>
...
</template>
<script>
import CopyRight from '@/components/Footer/CopyRight';
export default {
name: "Footer",
components: {
CopyRight,
},
}
</script>
<style lang="less" scoped>
....
}
</style>
路由组件:src->router->index.js
// 定义路由
const routes = [
{
path:'/',
component:Home,
meta:{
isTypeNav:true,
}
},
{
path: '/login',
component: Login,
meta:{
// 隐藏页脚中的列表信息
isHideFooterList:true,
}
},
{
path: '/register',
component: Register,
meta: {
// 隐藏页脚中的列表信息
isHideFooterList: true,
}
},
{
path: '/search',
component: Search,
meta: {
// 是否使用导航
isTypeNav: true
}
},
]
使用swiper实现轮播图
* vue资源地址:https://github.com/vuejs/awesome-vue
* awesome仓库:https://github.com/surmon-china/vue-awesome-swiper
* 示例:https://v1.github.surmon.me/vue-awesome-swiper/
- 下载模块:使用5版本的swiper与4版本的vue-awesome
npm install swiper@5 vue-awesome-swiper@4
- 新建一个组件作为Home的子组件MainAdv
src->pages->Home->MainAdv->index.vue
<template lang="">
<div>
<swiper class="swiper" :options="swiperOption">
<swiper-slide><img src="" /></swiper-slide>
<swiper-slide><img src="" /></swiper-slide>
<swiper-slide><img src="" /></swiper-slide>
<swiper-slide><img src="" /></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:"MainAdv",
components:{
Swiper,
SwiperSlide
},
data() {
return {
swiperOption: {
slidesPerView: 1,
spaceBetween: 30,
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
}
}
</script>
<style lang="less">
.swiper {
height: 100%;
img {
width: 100%;
}
}
</style>
复制的swiper模版源码
<template>
<swiper class="swiper" :options="swiperOption">
<swiper-slide>Slide 1</swiper-slide>
<swiper-slide>Slide 2</swiper-slide>
<swiper-slide>Slide 3</swiper-slide>
<swiper-slide>Slide 4</swiper-slide>
<swiper-slide>Slide 5</swiper-slide>
<swiper-slide>Slide 6</swiper-slide>
<swiper-slide>Slide 7</swiper-slide>
<swiper-slide>Slide 8</swiper-slide>
<swiper-slide>Slide 9</swiper-slide>
<swiper-slide>Slide 10</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'
import 'swiper/css/swiper.css'
export default {
name: 'swiper-example-loop',
title: 'Loop mode / Infinite loop',
components: {
Swiper,
SwiperSlide
},
data() {
return {
swiperOption: {
slidesPerView: 1,
spaceBetween: 30,
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
}
}
</script>
<style lang="scss" scoped>
@import './base.scss';
</style>
- Home父组件
<div class="center">
<!--banner轮播-->
<MainAdv></MainAdv>
</div>
<script>
import MainAdv from '@/pages/Home/MainAdv'
export default {
name:"Home",
components:{
MainAdv,
}
}
</script>
mock的使用
作用:mockjs可以帮助前端程序员快速创建API接口用于模拟数据。
- 下载模块
npm install mockjs
mock_npm源
https://www.npmjs.com/package/mockjs
mock官方文档
http://mockjs.com/
- 新建mook目录,提供自定义接口
src->mock->index.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
})
- 在项目入口文件中引入mock->index.js
src->index.js
import "@/mock";
- 在vue页面中发送axios请求,请求mock中的数据
src->views->Home->MainAdv->index.vue
mounted(){
axios.get("/my").then(value=>{
console.log(value.data);
})
}
封装mockRequest
- 将图片放置在public->images中
- 设置mock拦截
src->mock->index.js
import Mock from "mockjs";
import focusList from "./focus.json";
// 获取首页中轮播图的图片列表
// 请求方式:get
// 请求地址:
Mock.mock("http://hanser.com/adv/focus","get",{
ok:200,
data:focusList
})
- mock中的data数据 src->mock->focus.json
[
{
"id":"1",
"imgUrl":"/images/banner1.jpg"
},
{
"id":"2",
"imgUrl":"/images/banner2.jpg"
},
{
"id":"3",
"imgUrl":"/images/banner3.jpg"
},
{
"id":"4",
"imgUrl":"/images/banner4.jpg"
}
]
- mock中的data数据 src->mock->focus.json
[
{
"id": "001",
"name": "家用电器",
"keywords": ["节能补贴", "4K电视", "空气净化器", "IH电饭煲", "滚筒洗衣机", "电热水器"],
"imgUrl": "/images/floor-1-1.png",
"navList": [
{
"url": "http://www.atguigu.com",
"text": "热门"
},
{
"url": "http://www.atguigu.com",
"text": "大家电"
},
{
"url": "http://www.atguigu.com",
"text": "生活电器"
},
{
"url": "http://www.atguigu.com",
"text": "厨房电器"
},
{
"url": "http://www.atguigu.com",
"text": "应季电器"
},
{
"url": "http://www.atguigu.com",
"text": "空气/净水"
},
{
"url": "http://www.atguigu.com",
"text": "高端电器"
}
],
"carouselList": [
{
"id": "0011",
"imgUrl": "/images/floor-1-b01.png"
},
{
"id": "0012",
"imgUrl": "/images/floor-1-b02.png"
},
{
"id": "0013",
"imgUrl": "/images/floor-1-b03.png"
}
],
"recommendList": [
"/images/floor-1-2.png",
"/images/floor-1-3.png",
"/images/floor-1-5.png",
"/images/floor-1-6.png"
],
"bigImg": "/images/floor-1-4.png"
},
{
"id": "002",
"name": "手机通讯",
"keywords": ["高通骁龙", "16核处理器", "超高性价比", "鸿蒙系统", "以旧换新", "免费升级"],
"imgUrl": "/images/1_floor-1-1.png",
"navList": [
{
"url": "http://www.atguigu.com",
"text": "安卓机皇"
},
{
"url": "http://www.atguigu.com",
"text": "热销爆品"
},
{
"url": "http://www.atguigu.com",
"text": "性价比之王"
},
{
"url": "http://www.atguigu.com",
"text": "华为钜惠"
},
{
"url": "http://www.atguigu.com",
"text": "小米黑科技"
},
{
"url": "http://www.atguigu.com",
"text": "老人机"
},
{
"url": "http://www.atguigu.com",
"text": "极致奢华"
}
],
"carouselList": [
{
"id": "0011",
"imgUrl": "/images/1_floor-1-b01.png"
},
{
"id": "0012",
"imgUrl": "/images/1_floor-1-b02.png"
},
{
"id": "0013",
"imgUrl": "/images/1_floor-1-b03.png"
}
],
"recommendList": [
"/images/1_floor-1-2.png",
"/images/1_floor-1-3.png",
"/images/1_floor-1-5.png",
"/images/1_floor-1-6.png"
],
"bigImg": "/images/1_floor-1-4.png"
}
]
- 封装mockRequest axios请求 src->request->mockRequest.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;
- 把mockRequest请求引入request的入口文件 src->request->index.js
import sphRequest from "@/request/sphRequest";
import mockRequest from "@/request/mockRequest";
export {
// 调用尚品汇相关接口
sphRequest,
mockRequest
}
- 在页面中使用mockRequest请求 src->pages->Home->FocusAdv->index.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 src->index.js
// 导入mock
import "@/mock";
封装API
- 封装getfocusList api:src->api->adv.js
// 导入mockRequest请求
import { mockRequest } from "@/request";
const getFocusList = function(){
return mockRequest("/adv/focus");
}
// 暴漏数据
export {
getFocusList
}
- 新建一个adv 的store 数据仓库 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
}
- 导入到store中入口文件中 src->store->index
import Vue from 'vue';
import Vuex from 'vuex';
// 导入product模块
import product from "@/store/product/index.js";
import adv from '@/store/adv/index';
// 挂载
Vue.use(Vuex);
// 定义store对象
const store = new Vuex.Store({
modules:{
product,
adv
}
});
// 导出store对象
export default store;
- 在页面中使用vuex store中的数据 src->Home->MainAdv->index.vue,并渲染。
<template lang="">
<div>
<swiper class="swiper" :options="swiperOption">
<swiper-slide v-for="item in 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';
import { mapState } from 'vuex';
export default {
name:"MainAdv",
mounted(){
this.$store.dispatch('adv/getFocusListAsync');
},
computed:{
...mapState("adv", ["focusList"])
},
}
</script>
<style lang="">
</style>
增加advStore模块并渲染
- 新建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
}
- 在入口文件引入adv模块
import Vue from 'vue';
import Vuex from 'vuex';
// 导入product模块
import product from "@/store/product/index.js";
import adv from '@/store/adv/index';
import todaysell from './todaysell';
// 挂载
Vue.use(Vuex);
// 定义store对象
const store = new Vuex.Store({
modules:{
product,
adv,
todaysell
}
});
// 导出store对象
export default store;
- 在页面中使用store中的数据:src->pages->Home->MainAdv->index.vue
<template lang="">
<div>
<swiper class="swiper" :options="swiperOption">
<swiper-slide v-for="item in 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';
import { mapState } from 'vuex';
export default {
name:"MainAdv",
components:{
Swiper,
SwiperSlide
},
data() {
return {
swiperOption: {
slidesPerView: 1,
spaceBetween: 30,
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
},
mounted(){
this.$store.dispatch('adv/getFocusListAsync');
},
computed:{
...mapState("adv", ["focusList"])
},
}
</script>
<style lang="less">
.swiper {
height: 100%;
img {
width: 100%;
}
}
</style>
今日推荐
- 设置mock拦截数据:mock->data->todaysell.json
[
{
"id": "1",
"imgUrl": "https://kano-1303231448.cos.ap-nanjing.myqcloud.com/hanser/20240718_073546.webp"
},
{
"id": "2",
"imgUrl": "https://kano-1303231448.cos.ap-nanjing.myqcloud.com/hanser/20240706-DSC03655.webp"
},
{
"id": "3",
"imgUrl": "https://kano-1303231448.cos.ap-nanjing.myqcloud.com/hanser/20240706-DSC03760.webp"
},
{
"id": "4",
"imgUrl": "https://kano-1303231448.cos.ap-nanjing.myqcloud.com/hanser/20240706-DSC03707.webp"
}
]
- 设置mock拦截的接口:src->mock->index.js
import Mock from "mockjs";
import focusList from "@/mock/data/focus.json";
import todaysell from '@/mock/data/todaysell.json';
import floorList from '@/mock/data/floorList.json';
// 首页轮播图后台数据
Mock.mock("http://127.0.0.1:9090/adv/focus", "get", {
ok: 200,
data: focusList
})
// 今日推荐数据
Mock.mock("http://127.0.0.1:9090/today/sell","get",{
ok: 200,
data: todaysell
})
// 楼层的数据
Mock.mock("http://127.0.0.1:9090/floorList", "get", {
ok: 200,
data: floorList
})
- 设置今日推荐store数据:src->store->todaysell->index.js
import { getTodaySell } from "@/api/todaysell";
const state = {
// 今日推荐
topTodayList: [],
};
const mutations = {
SAVE_TOP_TODAY_LIST(state, topTodayList) {
state.topTodayList = topTodayList;
}
};
const actions = {
async getTodaySellAsync({ commit }) {
const { data } = await getTodaySell();
commit("SAVE_TOP_TODAY_LIST", data);
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
- 在入口文件中导入:src->store->index.js
import todaysell from './todaysell';
// 挂载
Vue.use(Vuex);
// 定义store对象
const store = new Vuex.Store({
modules:{
product,
adv,
todaysell
}
});
- 在今日推荐的组件页面中使用store数据:src-pages->Home->todaysell->index.vue
<template lang="">
<!--今日推荐-->
<div class="today-recommend">
<div class="py-container">
<ul class="recommend">
<li class="clock">
<div class="time">
<img src="../../../assets/images/home/clock.png" />
<h3>今日推荐</h3>
</div>
</li>
<li v-for="item in topTodayList" :key="item.id" class="banner">
<img :src="item.imgUrl" />
</li>
</ul>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
name:"TodaySell",
mounted() {
this.$store.dispatch("todaysell/getTodaySellAsync")
},
computed:{
...mapState("todaysell", ["topTodayList"])
}
}
</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>
- 在父组件中引入子组件 src->pages->Home->index.vue
<template lang="">
<div>
...
<!-- 今日推荐 -->
<TodaySell></TodaySell>
...
</div>
</template>
<script>
export default {
components:{
MainAdv,
TodaySell,
Floor
},
}
</script>
<style lang="">
</style>
楼层渲染
- 设置mock数据 src->mock->data->floorList.json
[
{
"id": "001",
"name": "家用电器",
"keywords": [
"节能补贴",
"4K电视",
"空气净化器",
"IH电饭煲",
"滚筒洗衣机",
"电热水器"
],
"imgUrl": "/images/floor-1-1.png",
"navList": [
{
"url": "https://yuluochenxiao.top",
"text": "热门"
},
{
"url": "https://yuluochenxiao.top",
"text": "大家电"
},
{
"url": "https://yuluochenxiao.top",
"text": "生活电器"
},
{
"url": "https://yuluochenxiao.top",
"text": "厨房电器"
},
{
"url": "https://yuluochenxiao.top",
"text": "应季电器"
},
{
"url": "https://yuluochenxiao.top",
"text": "空气/净水"
},
{
"url": "https://yuluochenxiao.top",
"text": "高端电器"
}
],
"carouselList": [
{
"id": "0011",
"imgUrl": "/images/floor-1-b01.png"
},
{
"id": "0012",
"imgUrl": "/images/floor-1-b02.png"
},
{
"id": "0013",
"imgUrl": "/images/floor-1-b03.png"
}
],
"recommendList": [
"/images/floor-1-2.png",
"/images/floor-1-3.png",
"/images/floor-1-5.png",
"/images/floor-1-6.png"
],
"bigImg": "/images/floor-1-4.png"
},
{
"id": "002",
"name": "手机通讯",
"keywords": [
"高通骁龙",
"16核处理器",
"超高性价比",
"鸿蒙系统",
"以旧换新",
"免费升级"
],
"imgUrl": "/images/floor-1-1.png",
"navList": [
{
"url": "https://yuluochenxiao.top",
"text": "安卓机皇"
},
{
"url": "https://yuluochenxiao.top",
"text": "热销爆品"
},
{
"url": "https://yuluochenxiao.top",
"text": "性价比之王"
},
{
"url": "https://yuluochenxiao.top",
"text": "华为钜惠"
},
{
"url": "https://yuluochenxiao.top",
"text": "小米黑科技"
},
{
"url": "https://yuluochenxiao.top",
"text": "老人机"
},
{
"url": "https://yuluochenxiao.top",
"text": "极致奢华"
}
],
"carouselList": [
{
"id": "0011",
"imgUrl": "/images/floor-1-b01.png"
},
{
"id": "0012",
"imgUrl": "/images/floor-1-b02.png"
},
{
"id": "0013",
"imgUrl": "/images/floor-1-b03.png"
}
],
"recommendList": [
"/images/floor-1-2.png",
"/images/floor-1-3.png",
"/images/floor-1-5.png",
"/images/floor-1-6.png"
],
"bigImg": "/images/floor-1-4.png"
}
]
- 封装楼层api接口getFloorList src->api->product.js
// 导入axios
import { sphRequest, mockRequest} from "@/request";
// 获取分类列表数据
const getBaseCategoryList = function(){
return sphRequest("/product/getBaseCategoryList");
}
// 获取楼层数据
const getFloorList = ()=>{
return mockRequest("/floorList")
}
// 暴漏数据
export {
getBaseCategoryList,
getFloorList
}
- 新建楼层的store数据仓库 src->store->product.js
// 定义商品的数据状态
const state = {
// 首页分类列表
categoryList:[],
// 楼层数据状态
floorList:[],
}
// 定义mutations
const mutations = {
// 修改state中的首页分类列表
UP_CATEGORY_LIST(state,categoryList){
state.categoryList = categoryList
},
// 修改state中的楼层列表
SAVE_FLOOR_LIST(state, floorList) {
state.floorList = floorList;
}
}
// 定义actions
const actions = {
// 使用api接口获取数据
async getBaseCategoryListAsync({commit},num=1){
const { data } = await getBaseCategoryList();
commit("UP_CATEGORY_LIST",data.splice(0,num));
},
// FloorList获取数据
async getFloorListAsync({ commit }) {
const { data } = await getFloorList();
commit("SAVE_FLOOR_LIST", data);
}
}
// 暴漏数据,导出模块
export default {
namespaced:true,
state,
mutations,
actions,
}
- 新建楼层组件 src->pages->Home->Floor->index.vue
<template lang="">
<div class="floor">
<div class="py-container">
<div class="title clearfix">
<h3 class="fl">{{floorInfo.name}}</h3>
<div class="fr">
<ul class="nav-tabs clearfix">
<li class="{active:index===0}" v-for="(item,index) in floorInfo.navList" :key="index">
<a :href="item.url" data-toggle="tab">{{item.text}}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<div class="tab-pane">
<div class="floor-1">
<div class="blockgary">
<ul class="jd-list">
<li v-for="(item,index) in floorInfo.keywords" :key="index" >{{item}}</li>
</ul>
<img :src="floorInfo.imgUrl" />
</div>
<!-- swiper轮播图 -->
<div class="floorBanner">
<swiper class="swiper" :options="swiperOption">
<swiper-slide v-for="item in floorInfo.carouselList" :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>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[0]" />
</div>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[1]" />
</div>
</div>
<div class="split center">
<img :src="floorInfo.bigImg" />
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[2]" />
</div>
<div class="floor-conver-pit">
<img :src="floorInfo.recommendList[3]" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
name:"Floor",
// 父向子传值
props: ["floorInfo"],
computed:{
},
components: {
Swiper,
SwiperSlide
},
data() {
return {
swiperOption: {
slidesPerView: 1,
spaceBetween: 30,
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
}
}
}
</script>
<style lang="less" scoped>
</style>
- Home页面中引入子组件 Floor
<template lang="">
<div>
<!--楼层-->
<Floor v-for="item in floorList" :key="item.id" :floorInfo="item"></Floor>
</div>
</template>
<script>
import Floor from '@/pages/Home/Floor';
export default {
components:{
MainAdv,
TodaySell,
Floor
},
}
</script>
<style lang="">
</style>
商品排行
接口一次性将所有排行的数据进行返回
- 新建mock数据 src->mock->data->rank.json
[...]
- 新增mock拦截请求 src->mock->index.js
import rankList from '@/mock/data/rank.json';
// 排行的数据
Mock.mock("http://127.0.0.1:9090/rankList", "get", {
ok: 200,
data: rankList
})
- 封装排行api getRankList :src->api->product.js
// 获取rank数据
const getRankList = () =>{
return mockRequest("/rankList")
}
// 暴漏数据
export {
getBaseCategoryList,
getFloorList,
getRankList
}
- 封装 rankList store数据仓库 src->store->product.js
// 导入api
import { getBaseCategoryList, getFloorList, getRankList } from '@/api/product';
// 定义商品的数据状态
const state = {
// 首页分类列表
categoryList:[],
// 楼层数据状态
floorList:[],
// rank的数据仓库
rankList:[],
}
// 定义mutations
const mutations = {
// 修改state中的首页分类列表
UP_CATEGORY_LIST(state,categoryList){
state.categoryList = categoryList
},
// 修改state中的楼层列表
SAVE_FLOOR_LIST(state, floorList) {
state.floorList = floorList;
},
// 修改rank中的数据
SAVE_RANK_LIST(state, rankList) {
state.rankList = rankList;
}
}
// 定义actions
const actions = {
// 使用api接口获取数据
async getBaseCategoryListAsync({commit},num=1){
const { data } = await getBaseCategoryList();
commit("UP_CATEGORY_LIST",data.splice(0,num));
},
// FloorList获取数据
async getFloorListAsync({ commit }) {
const { data } = await getFloorList();
commit("SAVE_FLOOR_LIST", data);
},
// 使用api获取rankList说几句
async getRankListAsync({ commit }) {
const { data } = await getRankList();
commit("SAVE_RANK_LIST", data);
}
}
// 暴漏数据,导出模块
export default {
namespaced:true,
state,
mutations,
actions,
}
- 新建rank组件:src->pages->Home->RankList->index.vue
<template lang="">
<div>
<div class="rank">
<div class="tab">
<div class="tab-tit clearfix">
<a v-for="(item,index) in rankList"
:key="item.id"
@click="activeIndex=index"
href="javascript:;"
:class="{on:activeIndex===index}">
<p class="img">
<i></i>
</p>
<p class="text">{{item.typeName}}</p>
</a>
</div>
</div>
<div class="content" v-for="(item,index) in rankList" :key="item.id" v-show="activeIndex===index">
<ul>
<li>
<div v-for="info in item.productList" :key="info.id" class="img-item">
<p class="tab-pic">
<a href="#">
<img :src="info.imgUrl" />
</a>
</p>
<div class="tab-info">
<div class="info-title">
<a href="#">{{info.name}}</a>
</div>
<p class="info-price">定金:{{info.price}}</p>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name:'RankList',
data(){
return {
activeIndex: 0
}
},
computed:{
...mapState("product",["rankList"])
},
mounted() {
this.$store.dispatch("product/getRankListAsync");
}
}
</script>
<style lang="less" scoped>
</style>
- 在父组件中引入子组件 src->Home->index.vue
<template lang="">
<div>
</div>
</template>
<script>
import RankList from '@/pages/Home/RankList';
export default {
components:{
MainAdv,
TodaySell,
Floor,
RankList
},
}
</script>
<style lang="">
</style>
商品排行方式2:使用不同的接口获取不同的排行
1. 构建mock数据:src->mock->rank->tejia.json,src->mock->rank->xinpin.json,src->mock->rank->remai.json
2. 创建mock拦截请求:src->mock->index.js
// 热卖排行
Mock.mock("http://mock.com/product/remai","get",{
ok:200,
data:remai
});
// 新品排行
Mock.mock("http://mock.com/product/xinpin","get",{
ok:200,
data:xinpin
});
// 特价排行
Mock.mock("http://mock.com/product/tejia","get",{
ok:200,
data:tejia
});
3. 创建api接口:getRemaiList getTeJiaList getXinPinList
export const getRemaiList = ()=>mockRequest("/product/remai");
export const getTeJiaList = ()=>mockRequest("/product/tejia");
export const getXinPinList = ()=>mockRequest("/product/xinpin");
4. 创建store数据仓库 src->store->product.js
import {getBaseCategoryList, getFloorList, getRankList, getRemaiList, getTeJiaList, getXinPinList} from "@/api/product";
const state = {
rankList2:[],// 第二种方案
}
const mutations = {
SAVE_RANK_LIST2(state,rankList){
state.rankList2 = rankList;
}
}
const actions = {
async getRankList2Async({commit},type){
// type:0-remai 1- tejia 2-xinpin
let data;
if(type === 0)
({data} = await getRemaiList());
else if(type === 1)
({data} = await getTeJiaList());
else
({data} = await getXinPinList());
commit("SAVE_RANK_LIST2",data);
},
}
export default {
namespaced:true,
state,
mutations,
actions
}
5. 创建rankList.vue组件
<template lang="">
<div>
...
<div class="tab-tit clearfix">
<a @click="type=0" href="javascript:;" :class="{on:type===0}">
<p class="img">
<i></i>
</p>
<p class="text">热卖排行</p>
</a>
<a @click="type=1" href="javascript:;" :class="{on:type===1}">
<p class="img">
<i></i>
</p>
<p class="text">特价排行</p>
</a>
<a @click="type=2" href="javascript:;" :class="{on:type===2}">
<p class="img">
<i></i>
</p>
<p class="text">新品排行</p>
</a>
</div>
...
</div>
</template>
<script>
export default {
name: "RankList",
data(){
return {
type:0
}
},
watch:{
type:{
handler(){
this.$store.dispatch("product/getRankList2Async",this.type)
},
immediate:true
}
}
}
</script>
<style lang="">
</style>
6. 父组件引入子组件