MoreMall商城
一、新建项目
1、配置环境
首先安装nvm对node.js版本进行管理(安装之前要先卸载已经安装的Node.js),安装教程:
https://blog.csdn.net/qq_22182989/article/details/125387145
2、创建项目
创建vue-cli项目:先安装脚手架,在cmd中输入:
npm i @vue/cli -g
安装完成后可以输入vue -V查看版本号,如果弹出版本号则说明安装成功,接下来创建项目,在cmd输入如下命令
vue create project
创建项目的时候vue版本建议选择vue3
创建vite项目(建议使用):
npm create vue@latest
3、清空项目目录
将components及views目录清空
二、完成系统首页
1、封装Header组件
在components目录下新建header.vue,Header组件主要是html+css,这里直接附上代码
<template>
<div class="container">
<div class="box">
<p>MoreMall商城</p>
<div>
<span>登录</span>
<span>注册</span>
</div>
</div>
</div>
</template>
<style scoped>
.container {
width: 100%;
height: 40px;
background-color: #333;
}
.box {
width: 1226px;
height: 40px;
margin: 0 auto;
font-size: 12px;
color: #b0b0b0;
display: flex;
justify-content: space-between;
align-items: center;
}
span {
margin-left: 10px;
cursor: pointer;
}
p:hover, span:hover {
color: #fff;
}
</style>
接下来对该组件进行注册,在main.js中将其注册为全局组件
import { createApp } from 'vue'
import App from './App.vue'
import Header from './components/Header.vue'
const app = createApp(App)
app.component('Header', Header)
app.mount('#app')
最后在App.vue组件中使用即可
<template>
<Header></Header>
</template>
由于导航栏及下面的Footer部分与Header类似,基本是由html和css组成,因此不再赘述,直接附上代码:
Nav.vue
<template>
<div class="container">
<div class="box">
<div class="logo">
<img src="@/assets/malllogo.png" alt="" />
</div>
<ul>
<li>手机</li>
<li>手环手表</li>
<li>平板电脑</li>
</ul>
<div class="search">
<input type="text" />
<button>🔍</button>
</div>
</div>
</div>
</template>
<style scoped>
.container {
width: 100%;
height: 100px;
border-bottom: 1px solid #e0e0e0;
}
.box {
width: 1226px;
margin: 0 auto;
height: 100px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
width: 204px;
}
.logo img {
width: 100%;
cursor: pointer;
}
ul {
color: #333;
width: 232px;
justify-content: space-around;
display: flex;
}
li {
cursor: pointer;
}
.search input {
width: 225px;
height: 48px;
padding: 0 10px;
outline: 0;
border: 1px solid #e0e0e0;
border-right: none;
}
.search input:focus,
.search input:focus + button {
border-color: orange;
}
.search button {
width: 52px;
height: 50px;
outline: 0;
border: none;
border: 1px solid #e0e0e0;
background-color: #fff;
font-size: 22px;
position: relative;
top: 4px;
cursor: pointer;
}
</style>
Footer.vue
<template>
<div class="container">
<ul class="footer-top">
<li>
<img
src="//yanxuan.nosdn.127.net/e6021a6fcd3ba0af3a10243b7a2fda0d.png"
/>
<span>30天无忧退换货</span>
</li>
<li>
<img
src="//yanxuan.nosdn.127.net/e09c44e4369232c7dd2f6495450439f1.png"
alt=""
/>
<span>满88元免邮费</span>
</li>
<li>
<img
src="//yanxuan.nosdn.127.net/e72ed4de906bd7ff4fec8fa90f2c63f1.png"
alt=""
/>
<span>XX品质保证</span>
</li>
</ul>
<div class="footer-bottom">
<ul>
<li>关于我们</li>
<li>帮助中心</li>
<li>售后服务</li>
<li>配送与验收</li>
<li>商务合作</li>
<li>企业采购</li>
<li>开放平台</li>
<li>搜索推荐</li>
<li>友情链接</li>
</ul>
<p>XX公司版权所有 © 1996-2018 食品经营许可证:XXXXXXXXXXXXXXXXX</p>
</div>
</div>
</template>
<style scoped>
.container {
width: 100%;
height: 230px;
background-color: #414141;
color: white;
overflow: hidden;
}
.footer-top {
width: 100%;
height: 60px;
padding: 32px 0;
border-bottom: 1px solid #4f4f4f;
display: flex;
justify-content: space-around;
}
.footer-top li {
display: flex;
width: 33%;
height: 60px;
justify-content: center;
align-items: center;
}
.footer-top li span {
vertical-align: middle;
font-size: 18px;
margin-left: 10px;
}
.footer-bottom {
color: #aaa;
font-size: 13px;
text-align: center;
margin-top: 30px;
}
.footer-bottom li {
display: inline-block;
cursor: pointer;
padding: 0 6px;
border-right: 2px solid #aaa;
}
.footer-bottom li:last-child {
border-right: none;
}
.footer-bottom p {
margin: 5px;
}
</style>
main.js
import { createApp } from 'vue'
import App from './App.vue'
import Header from './components/Header.vue'
import Nav from './components/Nav.vue'
import Footer from './components/Footer.vue'
const app = createApp(App)
app.component('Header', Header)
app.component('Nav', Nav)
app.component('Footer', Footer)
app.mount('#app')
2、完成商品列表组件基本结构
在components下新建product-list.vue作为商品列表组件,首先完成页面基本结构如下
<template>
<div class="box">
<div class="container">
<p>全部商品</p>
<ul>
<li>
<img src="" alt="" />
<div class="title"></div>
<div class="desc"></div>
<div class="price">
<span class="sale_price"></span>
<span class="original_price"></span>
</div>
<div class="imgs">
<img src="" alt="" />
</div>
</li>
</ul>
</div>
</div>
</template>
<style scoped>
.box {
width: 100%;
height: auto;
background-color: #f5f5f5;
}
.container {
width: 1226px;
height: auto;
margin: 0 auto;
}
.container p {
color: #333;
font-size: 22px;
font-weight: 200;
width: 88px;
height: 58px;
line-height: 58px;
}
ul {
width: 100%;
display: flex;
flex-wrap: wrap;
}
ul li {
width: 296px;
height: 383px;
background-color: #fff;
margin: 0 14px 14px 0;
padding-top: 47px;
font-size: 14px;
text-align: center;
transition: all 0.3s;
cursor: pointer;
}
ul li:nth-child(4n) {
margin-right: 0;
}
li:hover {
transform: translate(0, -2px);
box-shadow: 0px 5px 15px rgba(0, 0, 0, .2);
}
li > img {
width: 200px;
}
li .title {
color: #333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 3px 20px;
margin: 0 13px;
}
li .desc {
color: #b0b0b0;
font-size: 12px;
overflow: hidden;
padding: 3px 20px;
white-space: nowrap;
text-overflow: ellipsis;
}
li .sale_price {
color: #ff6700;
}
li .original_price {
color: #b0b0b0;
text-decoration: line-through;
margin-left: 5px;
}
li .price {
margin: 0 0 15px;
}
li .imgs img {
width: 34px;
border: 1px solid transparent;
margin-right: 3px;
}
li .imgs img:hover {
border-color: #ff6700;
}
</style>
3、获取商品列表数据
接下来需要从后台获取商品列表数据,然后渲染到页面中,注意在进行前后端交互时要保证后端服务器已启动,否则无法拿到数据
首先安装axios,在cmd中输入如下命令
npm i axios
安装完成后重启项目,然后在product-list组件中通过axios获取后台数据
<script>
import axios from 'axios'
export default {
data() {
return {
// 商品列表
productList: [],
};
},
//async和await是ES7的语法糖,可以针对返回值是Promise的操作用于获取其值,和then方法作用相似
async created() {
//解构赋值,将data属性从axios的返回值中拿出来并重命名为res,方便后续操作
const { data: res } = await axios.get("/product/list");
//将拿到的数据保存到data当中,方便后续对数据进行处理
this.productList = res.data;
console.log(this.productList);
},
}
</script>
打开浏览器开发者工具的控制台就能看到商品数据已经拿到,接下来就可以对数据进行遍历来渲染到页面
4、渲染商品数据到页面
现在已经拿到了商品列表的数据,可以发现其结构是一个数组,因此直接使用v-for对其进行遍历即可
<template>
<div class="box">
<div class="container">
<p>全部商品</p>
<ul>
<li v-for="product in productList" :key="product.id">
<img :src="product.details[0].image" alt="" />
<div class="title">
{{ product.name + " " + product.details[0].name }}
</div>
<div class="desc">{{ product.description }}</div>
<div class="price">
<span class="sale_price"
>¥{{ product.details[0].salePrice }}元</span
>
<span class="original_price"
>¥{{ product.details[0].price }}元</span
>
</div>
<div class="imgs">
<img
v-for="item in product.details"
:key="item.id"
:src="item.image"
alt=""
/>
</div>
</li>
</ul>
</div>
</div>
</template>
在对数据进行遍历之后,需要依次在相应位置插入商品名称、商品描述、商品价格及商品图片,但几乎所有商品数据都不是直接保存在每一个商品对象当中,而是保存在该对象的details属性中,该属性同样是一个数组,这时就需要考虑应该从该数组中取出哪一个值,因为每一个商品的details的长度实际上都是不一样的,这就很令人难以抉择,因此可以先默认每个商品均取出该数组的第一个值,所有可以看到上述代码中商品名称、商品价格等信息均通过product.details[0]来表示。
接下来就需要实现商品数据切换的效果,原网页中是通过鼠标移动到具体的小图片上实现商品切换,因此可以明确的是需要给每一个小图片加上一个鼠标经过的事件。至于切换商品图片及商品价格等信息的逻辑其实也很简单,因为前面在展示数据的时候是通过下下标0来从details中取数据,因此要实现数据的切换实际上只需要切换下标就可以了,这时我们会发现在每一个商品数据当中刚好有一个showIndex属性默认值为0,因此我们可以用product.showIndex来代替上述代码中的0进行页面数据的展示,并且我们在切换数据的时候直接将该属性的值改为对应的下标即可。具体代码如下:
<template>
<div class="box">
<div class="container">
<p>全部商品</p>
<ul>
<li v-for="product in productList" :key="product.id">
<img :src="product.details[product.showIndex].image" alt="" />
<div class="title">
{{ product.name + " " + product.details[product.showIndex].name }}
</div>
<div class="desc">{{ product.description }}</div>
<div class="price">
<span class="sale_price"
>¥{{ product.details[product.showIndex].salePrice }}元</span
>
<span class="original_price"
>¥{{ product.details[product.showIndex].price }}元</span
>
</div>
<div class="imgs">
<img
@mouseover="product.showIndex = index"
v-for="(item, index) in product.details"
:key="item.id"
:src="item.image"
alt=""
/>
</div>
</li>
</ul>
</div>
</div>
</template>
在这里我们给每一个小图片添加了一个mouseover的事件,当鼠标经过时获取每一个图片在details数组中的index,并将当前商品的showIndex属性改为其index即可实现数据的切换
三、完成商品详情页
1、安装并配置路由
因为涉及到多个页面,页面之间的切换一般通过vue-router实现,因此需要用到路由,在cmd中输入如下命令
npm i vue-router
安装完成后需要进行路由配置,在src下新建目录router,在其下新建文件index.js,其内容如下:
import { createRouter, createWebHashHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(),
routes: [
]
})
export default router
在该文件中对路由进行了简单配置,路由模式这里我选择的是hash模式,如果觉得#不美观也可以选择history模式
接下来需要在main.js中导入路由配置:
import { createApp } from 'vue'
import App from './App.vue'
//导入路由配置
import router from './router/index'
import Header from './components/Header.vue'
import List from './components/product-list.vue'
import Nav from './components/Nav.vue'
import Footer from './components/Footer.vue'
const app = createApp(App)
//使用路由
app.use(router)
app.component('Header', Header)
app.component('List', List)
app.component('Nav', Nav)
app.component('Footer', Footer)
app.mount('#app')
2、配置路由展示规则
因为之前的代码是将所有内容全部放到APP.vue中,这样不太方便后续的路由切换,因此在src下新建目录views用于放置路由切换的页面,然后新建index.vue作为项目首页,接下来将App.vue中的内容直接复制到其中即可。
index.vue
<template>
<div>
<Header />
<Nav />
<div class="carousel">
<img
:class="currentIndex === index ? 'active' : ''"
v-for="(image, index) in images"
:key="index"
:src="image"
alt="图片正在加载"
/>
<div class="last_btn" @click="lastImg"></div>
<div class="next_btn" @click="nextImg"></div>
</div>
<List />
<Footer />
</div>
</template>
<script>
export default {
data() {
return {
images: [
"https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/ed3db75d62d66994c219e5df3f7c648d.jpg?thumb=1&w=1226&h=460&f=webp&q=90",
"https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/9ebff5f5c1f52f2dbdd611897adbefd4.jpg?thumb=1&w=1226&h=460&f=webp&q=90",
"https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/1acdc8c5e49afede6d6b75ed32568b22.jpg?w=2452&h=920",
"https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/0ef4160c861b998239bce9adb82341e7.jpg?thumb=1&w=1226&h=460&f=webp&q=90",
"https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/ed12ea68287caf5fa7807449f9a4f5b7.jpg?thumb=1&w=1226&h=460&f=webp&q=90",
],
currentIndex: 0,
productList: [],
loginFlag: "",
};
},
created() {
setInterval(this.nextImg, 3000);
},
methods: {
lastImg() {
if (this.currentIndex <= 0) {
this.currentIndex = this.images.length - 1;
} else {
this.currentIndex--;
}
},
nextImg() {
if (this.currentIndex < this.images.length - 1) {
this.currentIndex++;
} else {
this.currentIndex = 0;
}
},
},
};
</script>
<style scoped>
.carousel {
width: 1226px;
height: 460px;
margin: 8px auto;
position: relative;
}
.carousel img {
width: 100%;
display: none;
}
.carousel .active {
display: block;
}
.carousel div {
width: 41px;
height: 69px;
position: absolute;
cursor: pointer;
}
.last_btn {
left: 0;
top: 50%;
transform: translate(0, -50%);
background: url(./assets/icon-slides.png) no-repeat -83px 0;
}
.next_btn {
right: 0;
top: 50%;
transform: translate(0, -50%);
background: url(./assets/icon-slides.png) no-repeat -123px 0;
}
.last_btn:hover {
background-position-x: 0;
}
.next_btn:hover {
background-position-x: -41px;
}
.login_container {
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: -100%;
left: 0;
transition: all 0.5s;
}
</style>
然后在App.vue中加上路由占位符,方便其它页面进行展示
App.vue
<template>
<router-view></router-view>
</template>
最后在router下的index.js中配置路由展示规则即可:
import { createRouter, createWebHistory, createWebHashHistory } from "vue-router";
import Index from '../views/Index.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Index },
]
})
export default router
现在再打开网页,页面和之前一样,但url地址发生了一些变化。
3、创建商品详情页并实现页面跳转
在views下新建details.vue文件,作为商品详情页,并配置该页面路由匹配规则
index.js
import { createRouter, createWebHistory, createWebHashHistory } from "vue-router";
import Index from '../views/Index.vue'
import Details from '@/views/details.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Index },
{ path: '/product/:id', component: Details, props: true }
]
})
export default router
这里的path值为’/product/:id’,使用了动态路由中的params传参,因为原网页在页面切换时其实也是通过这种方式实现页面跳转并查询商品详情,这里的id用于匹配页面跳转时所传递的商品id,然后在跳转到新页面时需要根据该id发送请求,获取该商品对应的详情。
至于页面切换可以通过链接式导航或编程式导航实现,两种方式任选其一即可。
(1)链接式导航
原网页用的也是这种方式,只需要在商品列表中的li里面放上一个路由链接router-link即可,具体代码如下:
<template>
<div class="box">
<div class="container">
<p>全部商品</p>
<ul>
<li v-for="product in productList" :key="product.id">
//路由链接
<router-link :to="/product/ + product.id" >
<img :src="product.details[product.showIndex].image" alt="" />
<div class="title">
{{ product.name + " " + product.details[product.showIndex].name }}
</div>
<div class="desc">{{ product.description }}</div>
<div class="price">
<span class="sale_price"
>¥{{ product.details[product.showIndex].salePrice }}元</span
>
<span class="original_price"
>¥{{ product.details[product.showIndex].price }}元</span
>
</div>
<div class="imgs">
<img
@mouseover="product.showIndex = index"
v-for="(item, index) in product.details"
:key="item.id"
:src="item.image"
alt=""
/>
</div>
</router-link>
</li>
</ul>
</div>
</div>
</template>
(2)编程式导航
编程式导航需要通过JS的API实现页面跳转,这里只需要给li加上一个点击事件即可
<template>
<div class="box">
<div class="container">
<p>全部商品</p>
<ul>
<li @click="$router.push('/product/' + product.id)" v-for="product in productList" :key="product.id">
<img :src="product.details[product.showIndex].image" alt="" />
<div class="title">
{{ product.name + " " + product.details[product.showIndex].name }}
</div>
<div class="desc">{{ product.description }}</div>
<div class="price">
<span class="sale_price"
>¥{{ product.details[product.showIndex].salePrice }}元</span
>
<span class="original_price"
>¥{{ product.details[product.showIndex].price }}元</span
>
</div>
<div class="imgs">
<img
@mouseover="product.showIndex = index"
v-for="(item, index) in product.details"
:key="item.id"
:src="item.image"
alt=""
/>
</div>
</li>
</ul>
</div>
</div>
</template>
这里通过$router.push方法实现页面跳转,并在跳转时传递id作为参数
4、完成商品详情页基本结构
跳转到商品详情页之后,首先需要获取该商品的相关数据,因此需要发送axios请求进行前后端交互
<script>
import axios from 'axios'
export default {
data() {
return {
product: {}
}
},
props: ["id"],
async created() {
const { data: res } = await axios.get("/product/content/" + this.id);
this.product = res.data
console.log(this.product);
},
};
</script>
其中的props是通过动态路由的params传参所传递的id,这里根据该id发送请求来查询相应商品详情并将拿到的数据保存到data中方便后续数据的渲染
商品详情页结构及样式较简单,这里不做过多赘述,大家自己参考原网页完成即可,以下代码仅供参考
details.vue
<template>
<Header></Header>
<Nav></Nav>
<div class="container">
<div class="name"><h2>{{ product.name }}</h2></div>
<div class="details">
<div class="product">
<img :src="product.details[product.showIndex].image" alt="">
<div class="info">
<div class="title">{{ product.name }}</div>
<div class="desc">{{ product.description }}</div>
<div class="price">{{ product.details[product.showIndex].salePrice }} 起</div>
<h2>选择规格</h2>
<ul>
<li v-for="item in product.details" :key="item.id">{{ item.name }}</li>
</ul>
<div class="number">
<span>购买数量 </span>
<button>-</button>
<input type="text">
<button>+</button>
</div>
<div class="btns">
<button class="buynow">立即购买</button>
<button class="addCart">加入购物车</button>
</div>
</div>
</div>
</div>
</div>
<Footer></Footer>
</template>
<script>
export default {
data() {
return {
product: {}
}
},
props: ["id"],
async created() {
const { data: res } = await this.$http.get("/product/content/" + this.id);
this.product = res.data
console.log(this.product);
},
};
</script>
<style scoped>
.container {
width: 100%;
height: 652px;
}
.details {
width: 1226px;
height: 652px;
margin: 0 auto;
}
.name {
width: 100%;
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.07);
}
.name>h2 {
color: #424242;
font-size: 18px;
font-weight: 400;
width: 1226px;
margin: 0 auto;
height: 60px;
line-height: 60px;
}
.product {
width: 1226px;
height: 560px;
padding: 32px 0 0;
display: flex;
justify-content: space-between;
}
.product img {
width: 560px;
}
.info {
width: 600px;
height: 560px;
}
.title {
color: #212121;
font-size: 18px;
}
.desc {
color: #b0b0b0;
font-size: 14px;
padding: 14px 0;
}
.price {
color: #ff6700;
font-size: 24px;
padding: 14px 0;
border-bottom: 1px solid #ccc;
}
.product h2 {
font-size: 18px;
font-weight: 400;
padding: 14px 0;
}
ul {
width: 600px;
height: auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
li {
width: 294px;
height: 44px;
margin: 0 0 10px;
border: 1px solid #ccc;
color: #757575;
text-align: center;
line-height: 44px;
cursor: pointer;
}
.number button {
width: 38px;
height: 38px;
background-color: #fff;
cursor: pointer;
border: 1px solid #ccc;
}
.number input {
width: 72px;
outline: 0;
border: 1px solid #ccc;
border-left: none;
border-right: none;
height: 36px;
}
.btns {
margin: 20px 0;
}
.btns button {
cursor: pointer;
border: none;
height: 52px;
color: #fff;
}
.btns .buynow {
width: 298px;
background-color: #ff6700;
margin: 0 10px 0 0;
}
.btns .addCart {
width: 140px;
background-color: #b0b0b0;
}
</style>
接下来需要通过选择商品规格来控制图片和价格的修改,这里的逻辑和商品列表的逻辑相似,直接将当前商品的showIndex属性修改为当前所选中商品的规格所对应下标即可:
<li @mouseover="product.showIndex = index" v-for="(item, index) in product.details" :key="item.id">
{{ item.name }}
</li>
5、修改商品列表组件
之前我们所完成的商品列表组件默认展示的是所有的商品,但实际上只有系统首页需要展示所有商品,其它页面(如商品分类、商品详情页等)都是按照所选商品分类进行商品的展示,即只需要所有商品中的一部分数据即可,查看接口之后会发现按照商品分类获取商品的接口和获取所有商品的接口是同一个接口,只需要单独传入一个商品分类id即可,因此我们可以在不同页面使用到商品列表组件时将该id传递给商品列表组件,而在首页要展示所有商品,只需要传递一个空的参数即可。修改商品列表组件的请求代码如下:
<script>
export default {
props: ['categoryId'],
data() {
return {
productList: [],
};
},
created() {
this.axios.get("/product/list", {
params: {
'categoryId': this.categoryId
}
}).then((res) => {
this.productList = res.data.data;
console.log(this.productList);
});
},
methods: {
goDetails(id) {
this.$router.push("/product/" + id);
},
},
};
</script>
在上述代码中我们增加了一个props用于接收父组件传递的商品分类id,然后将该id作为请求参数用于请求不同分类的商品,要注意该参数传递的格式。
在不同页面中要实现不同的展示效果,只需要控制传递的商品分类id即可,如下所示:
index.vue传递商品分类id:
<product-list :categoryId="''"></product-list>
因为首页要展示所有商品,因此这里传入的id为空
details.vue传递商品分类id:
<product-list :categoryId="product.categoryId"></product-list>
这里传递的是当前所选中的商品所对应的商品分类id
另外还需要对商品列表上方的文字标题进行修改,来达到不同的显示效果
<h2 v-if="categoryId === ''">全部商品</h2>
<h2 v-else-if="categoryId === 1">手机</h2>
<h2 v-else-if="categoryId === 2">手环手表</h2>
<h2 v-else-if="categoryId === 3">平板电脑</h2>
这里根据所传入的商品分类id来判断到底应该展示哪一个标题