Vue3微商城开发文档

1
8 章 “微商城”前台开发文档
8.1 准备工作
8.1.1 导入项目
1 )创建 D:\vue\chapter08 目录。
2 )从配套源代码中,将项目模板“ my-shop-template ”文件夹复制到 chapter08 目录,
并将其重命名为“ my-shop ”。
3 )使用命令提示符打开 D:\vue\chapter08\my-shop 目录,安装依赖。
yarn
yarn add axios@1.2.2 --save
yarn add less@4.1.3 --save
yarn add pinia@2.0.27 --save
yarn add pinia-plugin-persist@1.0 --save
yarn add vue-router@4 --save
yarn add vant@4.0 --save
4 )打开 index.html 修改标题。
<title> 微商城 </title>
5 )禁止双击缩放。
<meta name="viewport" content="width=device-width, initial-scale=1.0 , us
er-scalable=no " />
说明:本文档中标注红的代码为当前步骤新增或修改的代码。
8.1.2 定义路由
1 )创建 src\router\index.js ,具体代码如下。
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/', 2
redirect: '/home',
meta: { title: ' 首页 ' }
},
{
path: '/home',
component: () => import('../pages/Home.vue'),
name: 'home',
meta: { title: ' 首页 ', name: 'home', isTab: true }
},
{
path: '/category',
component: () => import('../pages/Category.vue'),
name: 'category',
meta: { title: ' 分类 ', isTab: true, isShowNav: true }
},
{
path: '/message',
component: () => import('../pages/Message.vue'),
name: 'message',
meta: { title: ' 消息 ', isTab: true, isShowNav: true }
},
{
path: '/cart',
component: () => import('../pages/Cart.vue'),
name: 'cart',
meta: { title: ' 购物车 ', isTab: true, isShowNav: true, isShowBack:
true }
},
{
path: '/user',
component: () => import('../pages/User.vue'),
name: 'user',
meta: { title: ' 我的 ', isTab: true }
},
]
})
router.beforeEach((to, from, next) => {
const title = to.meta && to.meta.title 3
if (title) {
document.title = title + ' - 微商城 '
}
next()
})
export default router
解释
isTab :是否显示底部 Tabbar 标签栏, true 表示显示, false 表示不显示。
isShowNav :是否显示 NavBar 导航栏, true 表示显示, false 表示不显示。
isShowBack : 是否显示导航栏中的返回箭头, true 表示显示, false 表示不显示。
2 )创建 src\pages\Home.vue ,具体代码如下。
<template>
Home
</template>
3 )创建 src\pages\Category.vue ,具体代码如下。
<template>
Category
</template>
4 )创建 src\pages\Message.vue ,具体代码如下。
<template>
Message
</template>
5 )创建 src\pages\Cart.vue ,具体代码如下。
<template>
Cart
</template>
6 )创建 src\pages\User.vue ,具体代码如下。
<template>
User
</template>
7 )修改 src\App.vue 中的模板部分,具体代码如下。
<template>
<router-view></router-view>
</template>
8 )修改 src\main.js 中的所有内容,具体代码如下。
import { createApp } from 'vue'
import App from './App.vue' 4
import Vant from 'vant'
import 'vant/lib/index.css'
import router from './router'
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(piniaPluginPersist)
const app = createApp(App)
app.use(Vant)
app.use(pinia)
app.use(router)
app.mount('#app')
9 )启动项目,具体命令如下。
yarn dev
8.1.3 实现底部导航栏
1 )创建 src\components\TabBar.vue ,具体代码如下。
<template>
<van-tabbar route fixed placeholder border>
<van-tabbar-item replace :to="{ name: 'home' }" icon="home-o"> 首页 </va
n-tabbar-item>
<van-tabbar-item replace :to="{ name: 'category'}" icon="apps-o"> 分类
</van-tabbar-item>
<van-tabbar-item replace :to="{ name: 'message'}" icon="chat-o" badge
="4"> 消息 </van-tabbar-item>
<van-tabbar-item replace :to="{ name: 'cart'}" icon="shopping-cart-o
"> 购物车 </van-tabbar-item>
<van-tabbar-item replace :to="{ name: 'user'}" icon="user-o"> 我的 </va
n-tabbar-item> 5
</van-tabbar>
</template>
<style scoped>
.van-tabbar-item {
--van-tabbar-item-active-color: #FF8000;
}
</style>
2 )修改 src\App.vue 中的所有内容,具体代码如下。
<template>
<router-view></router-view>
<tab-bar v-if="isShowTabbar"></tab-bar>
</template>
<script setup>
import TabBar from './components/TabBar.vue'
import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'
const route = useRoute()
const isShowTabbar = ref(true)
// 监听路由中的 isTab 是否为 true ,如果为 true ,展示底部 TabBar
watch(
() => route.meta,
val => {
isShowTabbar.value = val.isTab
}
)
</script>
<style>
#app {
color: #2c3e50;
line-height: 24px;
}
</style>
3 )访问测试,此时能看到页面底部的导航栏,效果如下。 6
8.2 首页开发
8.2.1 实现首页搜索框
1 )创建 src\pages\Home.vue ,具体代码如下。
<template>
<van-search
shape="round"
v-model="value"
placeholder=" 请输入搜索关键词 "
@search="onSearch"
@cancel="onCancel"
>
</van-search>
</template>
<script setup>
import { ref } from 'vue'
import { showToast } from 'vant'
const value = ref('')
const onSearch = val=> showToast(val)
const onCancel = () => showToast(' 取消 ')
</script>
2 )访问测试,搜索框的效果如下。
8.2.2 实现首页轮播图
1 )创建 src\components\HomeSwiper.vue ,具体代码如下。
<template> 7
<div class="home-swiper">
<van-swipe :autoplay="3000" lazy-render indicator-color="#FF8000">
<van-swipe-item v-for="item in banner" :key="item">
<img :src="item">
</van-swipe-item>
</van-swipe>
</div>
</template>
<script setup>
const banner = [
'/images/banner1.jpg',
'/images/banner2.jpg',
]
</script>
<style lang="less" scoped>
.home-swiper {
width: 100%;
img {
width: 100%;
}
}
</style>
2 )修改 src\pages\Home.vue ,导入组件,具体代码如下。
<template>
……(原有代码)
<!-- 轮播图 -->
<home-swiper></home-swiper>
</template>
<script setup>
import HomeSwiper from '../components/HomeSwiper.vue'
……(原有代码)
<script>
3 )查看轮播图效果,具体如下。 8
8.2.3 实现首页功能按钮区
1 )创建 src\components\HomeGrid.vue ,具体代码如下。
<template>
<div class="home-grid">
<van-grid :column-num="5" square :gutter="5">
<van-grid-item v-for="list in menulist" :key="list">
<van-image :src="list.url" />
<span>{{ list.text }}</span>
</van-grid-item>
</van-grid>
</div>
</template>
<script setup>
import menu1 from '../assets/images/menu1.png'
import menu2 from '../assets/images/menu2.png'
import menu3 from '../assets/images/menu3.png'
import menu4 from '../assets/images/menu4.png'
import menu5 from '../assets/images/menu5.png'
import menu6 from '../assets/images/menu6.png'
import menu7 from '../assets/images/menu7.png'
import menu8 from '../assets/images/menu8.png'
import menu9 from '../assets/images/menu9.png'
import menu10 from '../assets/images/menu10.png'
const menulist = [
{ text: ' 今日爆款 ', url: menu1 },
{ text: ' 好物分享 ', url: menu2 },
{ text: ' 推荐购买 ', url: menu3 }, 9
{ text: ' 购物心得 ', url: menu4 },
{ text: ' 直播专区 ', url: menu5 },
{ text: ' 签到中心 ', url: menu6 },
{ text: ' 值得购买 ', url: menu7 },
{ text: ' 每日优惠 ', url: menu8 },
{ text: ' 充值中心 ', url: menu9 },
{ text: ' 我的客服 ', url: menu10 }
]
</script>
<style lang="less" scoped>
.home-grid {
.van-image {
width: 55%;
}
span {
font-size: 12px;
}
}
</style>
2 )修改 src\pages\Home.vue ,具体代码如下。
<template>
……(原有代码)
<!-- 功能按钮区 -->
<home-grid></home-grid>
</template>
<script setup>
import HomeSwiper from '../components/HomeSwiper.vue'
import HomeGrid from '../components/HomeGrid.vue'
……(原有代码)
<script>
3 )查看功能按钮区效果,具体如下。 10
8.2.4 实现首页商品信息展示区
1 )创建 src\components\HomeProduct.vue ,具体代码如下。
<template>
<div class="home-product">
<ul>
<li v-for="item in brandList" :key="item.id">
<img :src="item.pic_url" alt="">
<h4>{{ item.name }}</h4>
</li>
</ul>
</div>
</template>
<script setup>
const brandList = [
{ id: 1, name: ' 直播 ', pic_url: '/images/product1.png' },
{ id: 2, name: ' 推荐 ', pic_url: '/images/product2.png' },
{ id: 3, name: ' 补贴 ', pic_url: '/images/product3.png' },
{ id: 4, name: ' 分享 ', pic_url: '/images/product4.png' }
]
</script>
<style lang="less" scoped>
.home-product > ul {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
li {
width: 49.5%; 11
position: relative;
img {
width: 100%;
}
h4 {
font-size: 14px;
position: absolute;
left: 2px;
top: -13px;
background-color: red;
color: #fff;
border-radius: 10%;
padding: 1px 3px;
}
}
}
</style>
2 )修改 src\pages\Home.vue ,具体代码如下。
<template>
……(原有代码)
<!-- 商品信息展示区 -->
<home-product></home-product>
</template>
<script setup>
import HomeSwiper from '../components/HomeSwiper.vue'
import HomeGrid from '../components/HomeGrid.vue'
import HomeProduct from '../components/HomeProduct.vue'
……(原有代码)
<script>
3 )查看商品信息展示区效果,具体如下。 12
8.2.5 实现每周上新
1 )创建 src\components\HomeNew.vue ,具体代码如下。
<template>
<div class="home-new">
<div class="home-new-title">
<h3> 每周上新 </h3>
</div>
<ul>
<li v-for="item in newList" :key="item.id">
<img :src="item.list_pic_url" alt="" />
<p>{{ item.name }}</p>
<p><span> </span>{{ item.retail_price }}</p>
</li>
</ul>
</div>
</template>
<script setup>
const newList = [
{ name: ' 懒人小沙发 ', list_pic_url: '/images/new1.jpg', retail_price: '1
28.00' }, 13
{ name: ' 减压弹力球 ', list_pic_url: '/images/new2.jpg', retail_price: '8
9.00' },
{ name: ' 简约一字夹发夹 ', list_pic_url: '/images/new3.jpg', retail_price:
'12.8' },
{ name: ' 毛线小兔子耳朵发夹 ', list_pic_url: '/images/new4.jpg', retail_pr
ice: '9.9' }
]
</script>
<style lang="less" scoped>
.home-new {
.home-new-title {
text-align: center;
font-size: 16px;
margin-top: 1.6rem;
height: 50px;
h3 {
width: 50%;
border-top: 2px solid #ccc;
padding-top: 8px;
margin: 0 auto;
}
}
ul {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
padding: 1rem 0 0;
background-color: #f9f9f9;
li {
width: 49.5%;
img {
width: 100%;
}
p {
text-align: center;
margin: 0.5rem 0;
} 14
span {
color: #FF8000;
font-size: 12px;
}
}
}
}
</style>
2 )修改 src\pages\Home.vue ,具体代码如下。
<template>
……(原有代码)
<!-- 每周上新 -->
<home-new></home-new>
</template>
<script setup>
import HomeSwiper from '../components/HomeSwiper.vue'
import HomeGrid from '../components/HomeGrid.vue'
import HomeProduct from '../components/HomeProduct.vue'
import HomeNew from '../components/HomeNew.vue'
……(原有代码)
<script>
3 )查看每周上新效果,具体如下。 15
8.2.6 实现人气推荐
1 )创建 src\components\HomeTop.vue ,具体代码如下。
<template>
<div class="home-top">
<h3> 人气推荐 </h3>
<div class="content">
<van-card
v-for="item in goodsList"
:key="item.id"
:tag="item.tag"
:price="item.retail_price"
:origin-price="item.origin_price"
:desc="item.goods_brief"
:title="item.name"
:thumb="item.list_pic_url"
>
</van-card>
</div> 16
</div>
</template>
<script setup>
const goodsList = [
{
retail_price: '299.00',
name: ' 蚕丝被 正品桑蚕丝 ',
goods_brief: ' 一级桑蚕丝,轻盈、透气、柔软 ',
list_pic_url: '/images/top1.jpg',
tag: 'TOP1'
},
{
retail_price: '88.00',
origin_price: '98.00',
name: ' 儿童摇摇马 ',
goods_brief: ' 安全、不会侧翻、爸妈放心 ',
list_pic_url: '/images/top2.jpg',
tag: 'TOP2'
},
{
retail_price: '128.00',
origin_price: '168.00',
name: ' 可躺可睡休闲懒人沙发 ',
goods_brief: ' 轻松看书、社交、办公、舒适放松 ',
list_pic_url: '/images/top3.jpg',
tag: 'TOP3'
},
{
retail_price: '199.00',
origin_price: '205.00',
name: ' 儿童积木 拼装玩具 ',
goods_brief: ' 大颗粒 家长更放心 不易吞咽、安全性高 ',
list_pic_url: '/images/top4.jpg',
tag: 'TOP4'
},
{
retail_price: '89.00', 17
origin_price: '99.00',
name: ' 扭扭车 1 —— 3 岁男女宝宝 ',
goods_brief: ' 儿童扭扭车万向轮 防侧翻大人新款摇摆扭扭车 ',
list_pic_url: '/images/top5.jpg',
tag: 'TOP5'
}
]
</script>
<style lang="less" scoped>
.home-top {
h3 {
font-size: 22px;
line-height: 30px;
text-align: center;
margin: 0.5rem 0;
}
.content {
--van-tag-primary-color: #FF8000;
--van-card-font-size: 16px;
--van-card-background: #f9f9f9;
background: var(--van-card-background);
:deep(.van-card) {
margin-top: 0;
.van-card__title {
padding: 10px 0 5px;
}
.van-card__price-currency {
font-size: var(--van-card-font-size);
}
}
&::after {
content: '';
display: block;
height: 3rem;
}
}
} 18
</style>
2 )修改 src\pages\Home.vue ,具体代码如下。
<template>
……(原有代码)
<!-- 人气推荐 -->
<home-top></home-top>
</template>
<script setup>
import HomeSwiper from '../components/HomeSwiper.vue'
import HomeGrid from '../components/HomeGrid.vue'
import HomeProduct from '../components/HomeProduct.vue'
import HomeNew from '../components/HomeNew.vue'
import HomeTop from '../components/HomeTop.vue'
……(原有代码)
<script>
3 )查看人气推荐效果,具体如下。
8.3 消息页开发
1 )先实现页面顶部的导航栏,修改 src\App.vue ,具体代码如下。
<template>
<van-nav-bar
:title="$route.meta.title"
v-show="$route.meta.isShowNav" 19
@click-left="onClickLeft"
:left-arrow="$route.meta.isShowBack"
fixed
placeholder
style="height: 46px"
/>
……(原有代码)
</template>
<script setup>
import TabBar from './components/TabBar.vue'
import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const route = useRoute()
const isShowTabbar = ref(true)
const router = useRouter()
const onClickLeft = () => {
if (history.length > 1) {
router.back()
} else {
router.push({ name: 'home' })
}
}
……(原有代码)
</script>
<style>
#app {
color: #2c3e50;
line-height: 24px;
--van-nav-bar-background: #ff8000;
--van-nav-bar-title-text-color: #fff;
--van-nav-bar-icon-color: #fff;
}
</style> 20
页面顶部的导航栏效果,具体如下。
2 )修改 src\pages\Message.vue ,具体代码如下。
<template>
<van-cell-group v-for="item in lists" :key="item">
<van-cell center :icon="item.img" :title="item.title" :value="item.va
lue" :label="item.label" />
</van-cell-group>
</template>
<script setup>
const lists = [
{
img: '/images/avatar1.jpg',
title: ' 食品旗舰店 ',
value: ' 星期一 ',
label: " 您有一条店铺消息 "
},
{
img: '/images/avatar2.jpg',
title: ' 水果旗舰店 ',
value: ' 星期二 ',
label: " 亲爱的果粉: "
},
{
img: '/images/avatar3.png',
title: ' 订阅号消息 ',
value: ' 星期日 ',
label: " 水果旗舰店:【新到水果新品————粑粑柑、砂糖橘】 "
},
{
img: '/images/avatar4.png',
title: ' 消息号内容 ',
value: ' 星期一 ',
label: " 食品旗舰店:大量新品到货,速来选购 "
}
] 21
</script>
<style lang="less" scoped>
:deep(.van-cell) {
.van-cell__left-icon {
width: 40px;
height: 40px;
.van-icon__image {
width: 100%;
height: 100%;
}
}
.van-cell__title {
.van-cell__label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 165px;
}
}
}
</style>
3 )消息页面的效果,具体如下。 22
8.4 用户登录和注册
8.4.1 实现登录页面
1 )修改 src\router\index.js ,添加登录页面的路由,具体代码如下。
routes: [
……(原有代码)
{
path: '/login',
component: () => import('../pages/Login.vue'),
name: 'login',
meta: { title: ' 登录 ', isTab: true, isShowNav: true, isShowBack: true
}
},
]
2 )创建 src\pages\Login.vue ,具体代码如下。
<template>
<van-form @submit="submitForm" @failed="onFailed" ref="ruleFormRef" :m
odel="form"> 23
<van-cell-group>
<van-field
v-model="form.username"
label=" 账号: "
placeholder=" 请输入账号 "
clearable
name="username"
:rules="usernameRules"
></van-field>
</van-cell-group>
<van-cell-group>
<van-field
v-model="form.password"
label=" 密码: "
placeholder=" 请输入密码 "
name="password"
clearable
type="password"
:rules="passwordRules"
></van-field>
</van-cell-group>
<van-cell-group>
<van-button block round type="primary" native-type="submit"> 登录 </va
n-button>
</van-cell-group>
</van-form>
</template>
<script setup>
import { ref, reactive } from 'vue'
const form = reactive({
username: 'demo1',
password: '123456'
})
const ruleFormRef = ref()
// 定义验证规则
const usernameRules = ref([
{ required: true, message: ' 用户名不能为空 ' }, 24
{ pattern: /^\w{3,16}$/, message: ' 用户名长度为 3-16 个字符 ' }
])
const passwordRules = ref([
{ required: true, message: ' 密码不能为空 ' },
{ pattern: /^\w{6,24}$/, message: ' 密码必须为 6-24 位英文字母或数字 ' }
])
// 表单提交函数
const submitForm = async values => {
console.log(values)
}
const onFailed = errorInfo => {
console.log('failed', errorInfo)
}
</script>
<style lang="less" scoped>
button {
position: fixed;
top: 200px;
}
</style>
3 )查看登录页效果,具体如下。
8.4.2 封装网络请求
1 )服务器端 API ,创建 src\config.js ,具体代码如下。
export default { 25
baseURL: 'http://127.0.0.1:8360'
}
2 )存储服务器返回的 token ,创建 src\stores\token.js ,具体代码如下。
import { defineStore } from 'pinia'
import { ref } from 'vue'
const useToken = defineStore('token', () => {
const token = ref(null)
const updateToken = val => token.value = val
const removeToken = () => token.value = null
return { token, updateToken, removeToken }
}, {
persist: {
enabled: true,
strategies: [
{
key: 'token',
storage: localStorage
}
]
}
})
export default useToken
3 )创建 src\utils\request.js ,具体代码如下。
import axios from 'axios';
import useToken from '../stores/token'
import config from '../config'
import router from '../router'
import { showLoadingToast, showToast, closeToast} from 'vant'
const baseURL = config.baseURL
const service = axios.create({ baseURL })
// 请求拦截器
service.interceptors.request.use(config => {
const { token } = useToken()
showLoadingToast({
message: ' 加载中 ...', 26
forbidClick: true,
loadingType: 'spinner'
})
if (token) {
config.headers.jwt = token
}
return config
})
// 响应拦截器
service.interceptors.response.use(
response => {
closeToast()
const { errno, data, errmsg } = response.data
if (errno === 0) {
if (errmsg !== '') {
showToast({
message: errmsg,
type: 'success'
})
}
return data || true
}
showToast({
message: errmsg,
type: 'error'
})
if (errno === 2) {
router.push({ name: 'login' })
}
return false
},
error => {
closeToast()
showToast ({
message: ' 请求失败 ',
type: 'fail'
}) 27
console.log(error)
}
)
export default service
4 )封装 API ,创建 src\api\index.js ,具体代码如下。
import request from '../utils/request'
// 登录接口
export function login(data) {
return request.post('/home/login', data)
}
8.4.3 实现登录功能
1 )修改 src\pages\Login.vue ,具体代码如下。
<script setup>
import { ref, reactive } from 'vue'
import { login } from '../api'
import useToken from '../stores/token'
const { updateToken } = useToken()
……(原有代码)
</script>
2 )定义表单提交函数,具体代码如下。
const submitForm = async values => {
const data = await login(values)
if (data) {
updateToken(data.token)
}
}
3 )存储用户信息。
修改 src\api\index.js ,调用用户信息接口,具体代码如下。
export function getUser() {
return request.get('/home/user')
}
创建 src\stores\user.js ,存储用户数据,具体代码如下。
import { defineStore } from 'pinia' 28
import { reactive } from 'vue'
const useUser = defineStore('user', () => {
const defaultUser = {
isLogin: false,
username: '',
avatar: ''
}
const user = reactive(Object.assign({}, defaultUser))
const updateUser = options => {
Object.assign(user, options)
return user
}
const removeUser = () => {
Object.assign(user, defaultUser)
return user
}
return { user, updateUser, removeUser }
}, {
persist: {
enabled: true,
strategies: [
{
key: 'user',
storage: localStorage
}
]
}
})
export default useUser
4 )更新用户信息
修改 src\pages\Login.vue ,具体代码如下。
import { login , getUser } from '../api'
import useToken from '../stores/token'
import useUser from '../stores/user'
const { updateToken } = useToken()
const { updateUser } = useUser() 29
if (data) {
updateToken(data.token)
const user = await getUser()
updateUser({
isLogin: true,
username: user.username,
avatar: user.avatar
})
}
5 )登录成功后,跳转到“我的”页面,具体代码如下。
import { useRouter } from 'vue-router'
const { updateToken } = useToken()
const { updateUser } = useUser()
const router = useRouter()
if (data) {
……(原有代码)
router.push({ name: 'user' })
}
6 )查看登录页面效果,具体如下。 30
8.4.4 检查登录状态
修改 src\App.vue ,具体代码如下。
import { ref, watch, onMounted } from 'vue'
import { getUser } from './api'
import useUser from './stores/user'
const { user, updateUser } = useUser()
onMounted(() => {
if (user.isLogin) {
loadUser()
}
})
const loadUser = async () => {
const data = await getUser()
updateUser({
isLogin: true,
username: data.username,
avatar: data.avatar
})
}
8.4.5 实现注册功能
1 )修改 src\router\index.js ,添加注册页面的路由,具体代码如下。
routes: [
……(原有代码)
{
path: '/register',
component: () => import('../pages/Register.vue'),
name: 'register',
meta: { title: ' 注册 ', isTab: true, isShowNav: true, isShowBack: true
} 31
},
]
2 )修改 src\api\index.js ,调用注册接口,具体代码如下。
export function register(data) {
return request.post('/home/register', data)
}
3 )创建 src\pages\Register.vue ,具体代码如下。
<template>
<van-form @submit="submitForm" @failed="onFailed" ref="ruleFormRef" :m
odel="form">
<van-cell-group>
<van-field
v-model="form.username"
label=" 账号: "
placeholder=" 请输入账号 "
clearable
name="username"
:rules="usernameRules"
></van-field>
</van-cell-group>
<van-cell-group>
<van-field
v-model="form.password"
label=" 密码: "
placeholder=" 请输入密码 "
clearable
type="password"
name="password"
:rules="passwordRules"
></van-field>
</van-cell-group>
<van-cell-group>
<van-field
v-model="form.confirmPassword"
label=" 确认密码: "
placeholder=" 请再次输入密码 "
clearable
type="password" 32
name="confirmPassword"
:rules="comfirmPasswordRules"
></van-field>
</van-cell-group>
<van-cell-group>
<van-button block round type="primary" native-type="submit"> 注册 </va
n-button>
</van-cell-group>
</van-form>
<div class="tip"> 注册成功后的用户可用于登录 </div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { getUser, register } from '../api'
import useToken from '../stores/token'
import useUser from '../stores/user'
import { useRouter } from 'vue-router'
const { updateToken} = useToken()
const { updateUser } = useUser()
const form = reactive({
username: '',
password: '',
confirmPassword: ''
})
const ruleFormRef = ref()
const router = useRouter()
// 定义验证规则
const usernameRules = ref([
{ required: true, message: ' 用户名不能为空 ', trigger: 'onBlur' },
{ pattern: /^\w{3,16}$/, message: ' 用户名长度为 3-16 个字符 ', trigger: 'onB
lur' }
])
const passwordRules = ref([
{ required: true, message: ' 密码不能为空 ', trigger: 'onBlur' }, 33
{ pattern: /^\w{6,24}$/, message: ' 密码必须为 6-24 位英文字母或数字 ', trigge
r: 'onBlur' }
])
const comfirmPasswordRules = ref([
{ required: true, message: ' 密码不能为空 ', trigger: 'onBlur' },
{ pattern: /^\w{6,24}$/, message: ' 密码必须为 6-24 位英文字母或数字 ', trigge
r: 'onBlur' },
{ validator: value => {
if (value !== form.password) {
return ' 两次输入的密码不一致 '
}
return true
}
}
])
// 表单提交函数
const submitForm = async () => {
const data = await register(form)
if (data) {
updateToken(data.token)
const user = await getUser()
updateUser({
isLogin: true,
username: user.username,
avatar: user.avatar !== '' ? config.baseURL + '/' + user.avatar : ''
})
router.push({ name: 'user' })
}
}
const onFailed = errorInfo => {
console.log('failed', errorInfo)
}
</script>
<style lang="less" scoped>
button {
position: fixed; 34
top: 270px;
}
.tip {
position: fixed;
top: 330px;
text-align: center;
width: 100%;
font-size: 14px;
color: #666;
}
</style>
4 )查看注册页面效果,具体如下。
8.5 “我的”页开发
1 )创建 src\pages\User.vue ,具体代码如下。
<template>
<!-- 已登录 -->
<van-row v-if="user.isLogin" class="user-info">
<van-image v-if="user.avatar" round width="100" height="100" src="{{
user.avatar }}" />
<van-image v-else round width="100" height="100" :src="avatar_default
" />
<span class="user-info-name">{{ user.username }}</span>
<van-button plain type="danger" size="mini" @click="onLogout"> 退出 </v
an-button> 35
</van-row>
<!-- 未登录 -->
<van-row v-else class="user-info">
<van-image round width="100" height="100" :src="avatar_default" />
<router-link :to="{ name: 'login' }">
<span class="user-info-name"> 登录   |</span>
</router-link>
<router-link :to="{ name: 'register' }">
<span class="user-info-name"> 注册 </span>
</router-link>
</van-row>
<van-row class="user-links">
<van-col span="6">
<van-icon name="pending-payment" />
待付款
</van-col>
<van-col span="6">
<van-icon name="records" :badge="user.isLogin ? '7' : ''" />
待收货
</van-col>
<van-col span="6">
<van-icon name="tosend" :badge="user.isLogin ? '40' : ''" />
待评价
</van-col>
<van-col span="6">
<van-icon name="logistics" :badge="user.isLogin ? '1' : ''" />
退换 / 售后
</van-col>
</van-row>
<van-cell-group class="user-group my-title">
<van-cell icon="records" title=" 全部订单 " is-link />
</van-cell-group>
<van-cell-group class="my-title">
<van-cell icon="points" title=" 我的积分 " is-link />
<van-cell icon="gold-coin-o" title=" 我的优惠券 " is-link />
<van-cell icon="gift-o" title=" 我的红包 " is-link />
</van-cell-group>
</template> 36
<script setup>
import avatar_default from '../assets/images/avatar_default.png'
import router from '../router/index'
import useToken from '../stores/token'
import useUser from '../stores/user'
import { showToast } from 'vant'
const { removeToken } = useToken()
const { user, removeUser } = useUser()
// 退出登录
const onLogout = async () => {
removeToken()
removeUser()
router.push({ name: 'user' })
showToast({
message: ' 退出成功 ',
type: 'success'
})
}
</script>
<style lang="less" scoped>
.user-info {
padding: 15px;
background: url(../assets/images/user_head_bg.png) no-repeat;
background-size: 100%;
}
.user-info button {
margin: 40px 0 0 10px;
}
.user-info-name {
display: inline-block;
color: #fff;
padding: 40px 0 0 10px;
font-size: 20px;
} 37
:deep(.van-badge--top-right) {
top: 4px;
right: 35px;
transform: translate(50%, -50%);
}
.user {
&-group {
margin-bottom: 15px;
}
&-links {
padding: 15px 0;
font-size: 12px;
text-align: center;
.van-icon {
display: block;
font-size: 24px;
}
}
}
</style>
2 )查看“我的”页面效果,具体如下。
3 )单击“登录”链接,进入登录页面,登录成功后页面效果,具体如下。 38
4 )退出登录后页面效果,具体如下。 39
5 )单击“注册”链接,跳转到注册页面,输入信息后,单击“注册”按钮,注册成
功则直接登录,具体如下。
8.6 分类页开发
8.6.1 加载分类数据
1 )修改 src\api\index.js ,调用分类接口,具体代码如下。
export function getCategoryList() {
return request.get('/home/category/list')
}
2 )修改 src\pages\Category.vue ,具体代码如下。
<script setup>
import { onMounted } from 'vue'
import { getCategoryList } from '../api'
onMounted(() => {
loadCategoryList()
})
// 获取分类数据
const loadCategoryList = async () => {
let data = await getCategoryList()
console.log(data) 40
}
</script>
3 )在控制台打印接口返回的数据,具体如下。
8.6.2 数组格式转换
1 )修改 src\pages\Category.vue ,具体代码如下。
<script setup>
……(原有代码)
// 将一维数组转换成树形结构的方法
const convertToTree = data => {
const treeData = []
const map = {}
// 遍历一维数组数据,建立节点映射表
for (const item of data) {
map[item.id] = { ...item, children: [] }
}
// 遍历映射表,将节点添加到父节点的 children
for (const item of data) { 41
const node = map[item.id]
if (item.pid === 0) {
treeData.push(node)
} else {
const parent = map[item.pid]
parent.children.push(node)
}
}
return treeData
}
</script>
2 )将数据进行转换,具体代码如下。
const loadCategoryList = async () => {
let data = await getCategoryList()
// 将一维数组数据转换为树形结构
const treeData = convertToTree(data)
console.log(treeData)
}
3 )在控制台打印转换后的数据,具体如下。
8.6.3 显示分类数据
1 )修改 src\pages\Category.vue ,具体代码如下。
<script setup>
import { onMounted , ref } from 'vue'
import { getCategoryList } from '../api'
const menus = ref([]) 42
2 )将转换后的数据赋值给 menus 数组,具体代码如下。
const loadCategoryList = async () => {
let data = await getCategoryList()
// 将一维数组数据转换为树形结构
const treeData = convertToTree(data)
// 将转换后的数据赋值给 menus
menus.value = treeData
}
3 )遍历 menus 数组,渲染页面,具体代码如下。
<template>
<div class="menu">
<div class="menu-left">
<ul>
<li class="menu-item" v-for="(menu, index) in menus" :key="index">
<p class="text">{{ menu.name }}</p>
</li>
</ul>
</div>
<div class="menu-right">
<!-- 显示二级分类 -->
<ul>
<li class="cate" v-for="(menu, index1) in menus" :key="index1">
<h4 class="cate-title">{{ menu.name }}</h4>
<ul class="cate-item">
<li v-for="(item, index2) in menu.children" :key="index2">
<router-link class="cate-item-wrapper" to="">
<div class="cate-item-img">
<img :src="item.picture" alt="">
</div>
<span>{{ item.name }}</span>
</router-link>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template> 43
<style lang="less" scoped>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.menu {
display: flex;
position: absolute;
text-align: center;
top: 46px;
bottom: 50px;
width: 100%;
overflow: hidden;
.menu-left {
flex: 0 0 80px;
width: 80px;
background: #f3f5f7;
line-height: 54px;
.menu-item {
height: 54px;
width: 100%;
border-bottom: 1px solid #e1e1e1;
.text {
width: 100%;
margin: 0;
}
}
.current {
width: 100%;
background: #fff;
.text {
color: red;
}
}
} 44
.menu-right {
flex: 1;
background: #fff;
.cate {
height: 100%;
.cate-title {
margin: 0;
text-align: left;
font-size: 14px;
color: #333;
font-weight: bold;
padding: 10px;
}
.cate-item {
padding: 7px 10px 10px;
display: flex;
overflow: hidden;
flex-flow: row wrap;
li {
width: 33.3%;
.cate-item-wrapper {
.cate-item-img {
width: 100%;
img {
width: 70px;
height: 70px;
}
}
span {
display: inline-block;
font-size: 14px;
color: #333;
}
}
}
}
}
} 45
}
</style>
4 )查看数据渲染页面后的分类页,具体如下。
8.6.4 单击左侧菜单项获取右侧菜单对应位置
1 )修改 src\pages\Category.vue ,给左侧菜单项添加一个单击事件,具体代码如下。
<li class="menu-item" v-for="(menu, index) in menus" :key="index" @click
="clickList(index)" >
2 )获取单击当前菜单项的索引值,具体代码如下。
<script setup>
……(原有代码)
// 单击左侧菜单项 46
const clickList = index => {
console.log(index)
}
</script>
3 )定义一个 rightLiTops 数组,用于存储所有分类头部位置,具体代码如下。
const menus = ref([])
const rightLiTops = ref([])
4 )导入 watch nextTick ,调用 initRightHeight() 监听右侧菜单 列表高度, 具体代码
如下。
import { onMounted , watch, nextTick , ref } from 'vue'
onMounted(() => {
loadCategoryList()
})
// 监听
watch(menus, () => {
nextTick(() => {
initRightHeight()
})
})
5 )为右侧列表添加一个 ref 属性,属性值为 itemList ,具体代码如下。
const menus = ref([])
const rightLiTops = ref([])
const itemList = ref()
为元素绑定一个 ref 属性,且属性值为 itemList ,具体代码如下。
<div class="menu-right" ref="itemList" >
6 )定义 initRightHeight() 方法,具体代码如下。
<script setup>
……(原有代码)
// 初始化右边菜单的高度
const initRightHeight = () => {
const itemArray = []
let top = 0
itemArray.push(top)
const allList = itemList.value.getElementsByClassName('cate')
Array.prototype.slice.call(allList).forEach(li => {
top += li.clientHeight
itemArray.push(top) 47
})
rightLiTops.value = itemArray
}
</script>
7 )单击当前菜单项时,通过 index 索引会得到右侧菜单每一块 <li> 标签的高度,具
体代码如下。
// 单击左侧菜单项
const clickList = index => {
console.log(rightLiTops.value[index])
}
8 )在控制台打印,当单击左侧菜单项时输出对应的右侧菜单的高度,例如, 0 表示
单击左侧第一个菜单项时,对应的右侧菜单的高度,依次类推,具体如下。
8.6.5 初始化 better-scroll
1 )安装插件 better-scroll 插件,具体命令如下。
yarn add @better-scroll/core@2.5 --save
2 )在 Category.vue 中导入 better-scroll 插件,具体代码如下。
import BScroll from '@better-scroll/core'
3 )调用 initBScroll() ,监听左右菜单的滚动,具体代码如下。
const rightLiTops = ref([])
let leftBscroll = null
let rightBscroll = null
// 监听
watch(menus, () => {
nextTick(() => { 48
initBScroll()
initRightHeight()
})
})
4 )定义 initBScroll() ,初始化左菜单和右菜单,具体代码如下。
<script setup>
……(原有代码)
// 初始化 BScroll
const initBScroll = () => {
// 初始化左菜单
leftBscroll = new BScroll('.menu-left', {
click: true,
mouseWheel: true
})
// 初始化右菜单
rightBscroll = new BScroll('.menu-right', {
click: true,
mouseWheel: true,
probeType: 3 // 实时派发 scroll 事件
})
}
</script>
5 )监听右侧滚动事件,当右侧菜单滚动的时候计算出滚动的距离,具体代码如下。
const scrollY = ref(0) // 右侧列表滑动的 y 轴坐标
let rightBscroll = null
// 初始化 BScroll
const initBScroll = () => {
……(原有代码)
rightBscroll.on('scroll', pos => {
scrollY.value = Math.abs(pos.y)
})
}
6 )单击左侧菜单项,右侧菜单滚动到相应位置,具体代码如下。
// 单击左侧菜单项
const clickList = index => { 49
scrollY.value = rightLiTops.value[index]
rightBscroll.scrollTo(0, -scrollY.value)
}
7 )单击左侧菜单项,右侧菜单滚动到相应位置,具体如下。
8.6.6 修复单击左侧底部菜单项页面跳转问题
1 )添加一个 <li> 标签,设置元素的高度,具体代码如下。
<script setup>
……(原有代码)
const RightHeightFix = () => {
let bottom = itemList.value.getElementsByClassName('cate-bottom')[0]
bottom.style.height = itemList.value.clientHeight / 1.2 + 'px'
}
</script>
<div class="menu-right" ref="itemList">
<!-- 显示二级分类 -->
<ul> 50
<li class="cate" v-for="(menu, index1) in menus" :key="index1">
<h4 class="cate-title">{{ menu.name }}</h4>
<ul class="cate-item">
<li v-for="(item, index2) in menu.children" :key="index2">
<router-link class="cate-item-wrapper" to="">
<div class="cate-item-img">
<img :src="item.picture" alt="">
</div>
<span>{{ item.name }}</span>
</router-link>
</li>
</ul>
</li>
<li class="cate-bottom"></li>
</ul>
</div>
2 组件 挂载 后立即执行 RightHeightFix() ,具 体代码如下。
onMounted(() => {
loadCategoryList()
RightHeightFix()
})
3 )单击左侧菜单的最后一项时,右侧菜单可以滚动到相应的位置,具体如下。 51
8.6.7 右侧菜单滚动激活左侧菜单项
1 )右菜单滚动时,左菜单联动,具体代码如下。
const initLeftScroll = index => {
const menu = menuList.value
let el = menu[index]
leftBscroll.scrollToElement(el, 300, 0, -100)
}
参数: scrollToElement(el, time, offsetX, offsetY, easing) ,用于 滚动到指定的目标元素。
easing 缓动函数,一般不建议修改。
el :目标元素
300 滚动动画执行的时长,单位毫秒
0 相对于目标元素的横轴偏移量
-100 相对于目标元素的纵轴轴偏移量
2 )动态绑定 class 样式,激活左侧菜单项,具体代码如下。
const rightLiTops = ref([])
const menuList = ref()
<li class="menu-item" v-for="(menu, index) in menus" :key="index" :class
="{ current: index === currentIndex }" @click="clickList(index)" ref="menuLi
st" >
3 )使用 computed 计算 currentIndex ,具体代码如下。
import { onMounted, watch, nextTick, ref , computed } from 'vue'
<script setup>
……(原有代码)
const currentIndex = computed(() => {
return rightLiTops.value.findIndex((top, index) => {
if (index === rightLiTops.value.length - 2) {
return true
}
if (scrollY.value >= top && scrollY.value < rightLiTops.value[index +
1]) {
initLeftScroll(index)
return true
}
}) 52
})
</script>
8.7 商品列表页开发
8.7.1 单击分类跳转到商品列表页
1 )修改 src\router\index.js ,添加商品列表页面的路由,具体代码如下。
routes: [
……(原有代码)
{
path: '/goodslist/:category_id',
component: () => import('../pages/GoodsList.vue'),
props: true,
name: 'goodslist',
meta: { title: ' 商品列表 ', isTab: true, isShowNav: true, isShowBack: tr
ue }
},
]
2 )创建 src\pages\GoodsList.vue ,具体代码如下。
<template>
GoodsList
</template>
<script setup>
import { onMounted } from 'vue'
const props = defineProps({
category_id: String
})
onMounted(() => {
loadGoodList()
})
const loadGoodList = async () => { 53
console.log(props.category_id)
}
</script>
3 )修改 src\pages\Category.vue ,具体代码如下。
<router-link class="cate-item-wrapper" :to="{ name: 'goodslist', params:
{ category_id: item.id } }" >
4 )测试能否接收到参数,具体如下。
8.7.2 加载商品列表数据
1 )修改 src\api\index.js ,调用商品列表接口,具体代码如下。
// 商品列表接口
export function getGoodsList(params) {
return request.get('/home/goods/list', { params })
}
2 )修改 src\pages\GoodsList.vue ,具体代码如下。
import { onMounted , ref } from 'vue'
import { getGoodsList } from '../api'
const goodsList = ref([]) 54
let last_id = '0'
3 )请求接口返回的数据,具体代码如下。
const loadGoodList = async () => {
let params = {
last_id,
category_id: props.category_id,
pagesize: 4
}
const data = await getGoodsList(params)
if (data.length > 0) {
goodsList.value = goodsList.value.concat(data)
last_id = data[data.length - 1].id
} else if (goodsList.value.length > 0) {
// 已经到达底部
} else {
// 列表为空
}
}
8.7.3 显示商品列表数据
1 )在页面中渲染数据,具体代码如下。
<template>
<div class="goods-list">
<div class="goods-item" v-for="item in goodsList" :key="item.id">
<router-link to="">
<van-image
width="150"
height="150"
:src="item.picture"
/>
<h1 class="title">{{ item.name }}<span class="small">{{ item.spec
}}</span></h1>
<p class="info">
<span class="price">¥{{ item.price }}</span>
<span class="sell"> 剩余 {{ item.stock }} </span> 55
</p>
</router-link>
</div>
</div>
</template>
2 )编写样式,具体代码如下。
<style lang="less" scoped>
.goods-list {
display: flex;
flex-wrap: wrap;
padding-left: 10px;
clear: both;
.goods-item {
width: calc(calc(100% / 2) - 12px);
margin: 10px 10px 0 0;
display: flex;
flex-direction: column;
justify-content: space-between;
border-radius: 10px;
border: 1px solid #efeff4;
padding: 10px 0;
clear: both;
.title {
text-align: left;
font-size: 14px;
color: #333;
margin: 10px 0 0;
padding: 0 5px;
.small {
font-size: 12px;
padding-left: 2px;
color: #999;
}
}
.info {
display: flex;
justify-content: space-between;
margin-bottom: 0; 56
padding: 0 5px;
.price {
color: red;
font-weight: bold;
font-size: 16px;
}
.sell {
font-size: 13px;
color: #999;
}
}
}
.more {
margin: 40px 20px 40px 10px;
font-size: 14px;
}
}
</style>
3 )查看商品列表,具体如下。
8.7.4 实现加载更多
1 )定义 is_last 默认为 false ,具体代码如下。
const goodsList = ref([])
const is_last = ref(false)
2 )添加“加载更多”按钮,具体代码如下。
<div class="goods-list"> 57
……(原有代码)
<van-button class="more" :disabled="is_last" v-if="goodsList.length !=
= 0" size="large" type="primary" plain hairline @click="getMore"> 加载更多 </va
n-button>
</div>
3 )定义 getMore 事件,具体代码如下。
<script setup>
……(原有代码)
const getMore = () => {
loadGoodList()
}
</script>
4 )查看加载更多效果,具体如下。
5 )导入 showToast 组件,具体代码如下。
import { showToast } from 'vant'
6 )根据数据的长度进行条件判断,给出不同的提示,具体代码如下。
} else if (goodsList.value.length > 0) {
showToast({
message: ' 已经到达底部 ',
type: 'fail'
})
is_last.value = true
} else {
showToast({
message: ' 列表为空 ', 58
type: 'fail'
})
}
7 )查看已经到达顶部的效果,具体如下。
8 )查看列表为空的效果,具体如下。
8.8 商品详情页开发
8.8.1 单击商品跳转到商品详情页
1 )修改 src\router\index.js ,添加商品详情页的路由,具体代码如下。
routes: [
……(原有代码) 59
{
path: '/goodsDetail/:id',
component: () => import('../pages/GoodsDetail.vue'),
props: true,
name: 'goodsDetail',
meta: { title: ' 商品详情 ', isTab: false, isShowNav: true, isShowBack: t
rue }
},
}
2 )创建 src\pages\GoodsDetail.vue ,具体代码如下。
<template>
GoodsDetail
</template>
<script setup>
import { onMounted } from 'vue'
const props = defineProps({
id: String
})
onMounted(() => {
loadGoodsDetail()
})
// 加载商品详情
const loadGoodsDetail = async () => {
console.log(props.id)
}
</script>
3 )修改 src\pages\GoodsList.vue ,具体代码如下。
<router-link :to="{ name: 'goodsDetail', params: { id: item.id } }" >
4 )测试能否接收到参数,单击商品,可以传递过去参数,具体如下。 60
8.8.2 加载商品详情数据
1 )修改 src\api\index.js ,具体代码如下。
// 商品相册接口
export function getGoodsAlbum(params) {
return request.get('/home/goods/album', { params })
}
// 商品详情接口
export function getGoodsDetail(params) {
return request.get('/home/goods', { params })
}
2 )修改 src\pages\GoodsDetail.vue ,具体代码如下。
import { reactive, ref, onMounted } from 'vue'
import { getGoodsAlbum, getGoodsDetail } from '../api'
const goods = reactive({})
const album = ref([])
const isNotFound = ref(false)
3 )加载商品详情数据和轮播图数据,具体代码如下。 61
// 加载商品详情
const loadGoodsDetail = async () => {
const data1 = await getGoodsDetail({ id: props.id })
if (!data1.id) {
isNotFound.value = true
return
}
const data2 = await getGoodsAlbum({ goods_id: props.id })
if (data2.length === 0 && data1.picture !== '') {
data2.push({ picture: data1.picture })
}
Object.assign(goods, data1)
album.value = data2
}
8.8.3 显示商品详情数据
1 )修改 src\pages\GoodsDetail.vue ,具体代码如下。
<template>
<div class="goods" v-if="!isNotFound">
<van-swipe class="goods-swipe" :autoplay="3000">
<van-swipe-item v-for="item in album" :key="item.id">
<img :src="item.picture" height="414">
</van-swipe-item>
</van-swipe>
<van-cell-group>
<van-cell>
<template #title>
<span class="goods-top"> 新品 </span>
<div class="goods-price">
<span class="small"> </span>
{{ goods.price }}
<span class="spec">{{ goods.spec }}</span>
</div>
<div class="goods-title">
<span class="small"> {{ goods.name }}</span>
</div> 62
</template>
</van-cell>
<van-cell class="goods-express">
<template #title>
<van-col span="10"> 运费: 10</van-col>
<van-col span="14"> 剩余: {{ goods.stock }}</van-col>
</template>
</van-cell>
</van-cell-group>
<van-cell-group class="goods-cell-group">
<van-cell>
<template #title>
<span class="van-cell-text"> 发货 陕西宝鸡 </span>
</template>
</van-cell>
<van-cell>
<template #title>
<span class="van-cell-text"> 保障 坏单包赔·假一赔四·极速退款 </span>
</template>
</van-cell>
<van-cell>
<template #title>
<span class="van-cell-text"> 参数 品牌:枝纯 价格: 100-200</span>
</template>
</van-cell>
</van-cell-group>
<div class="goods-cell-title">
—— 宝贝详情 ——
</div>
<div class="goods-description" v-html="goods.description"></div>
<!-- 底部按钮 -->
<van-action-bar>
<van-action-bar-icon icon="chat-o" text=" 客服 " />
<van-action-bar-icon icon="cart-o" text=" 购物车 " />
<van-action-bar-button type="warning" text=" 加入购物车 " />
<van-action-bar-button type="danger" text=" 立即购买 " />
</van-action-bar>
</div> 63
<div class="goods-not-found" v-else> 商品不存在 </div>
</template>
2 )编写样式,具体代码如下。
<style lang="less" scoped>
.goods {
text-align: center;
padding-bottom: 50px;
.goods-swipe {
img {
width: 100%;
display: block;
}
}
.goods-top {
display: block;
width: 30px;
padding: 0 5px;
border-radius: 10px;
color: #fff;
background-color: #f44;
}
.goods-title {
text-align: left;
.small {
font-size: 14px;
}
}
.goods-price {
color: #f44;
text-align: left;
font-size: 20px;
.small {
font-size: 12px;
}
.spec {
font-size: 12px;
color: #999;
} 64
}
.goods-cell-title {
padding: 20px 0;
}
.goods-description {
width: 95%;
margin: 0 auto;
padding-bottom: 20px;
font-size: 14px;
:deep(img) {
max-width: 100%;
height: auto;
display: block;
}
}
&-express {
color: #999;
font-size: 12px;
padding: 5px 15px;
:deep(.van-col) {
float: left;
}
:deep(.van-col--14){
width: 58%;
}
}
.goods-cell-group {
:deep(.van-cell__title span){
float: left;
}
}
:deep(.van-cell:after) {
border: none;
}
}
.goods-not-found {
padding-top: 48px;
text-align: center; 65
font-size: 28px;
}
</style>
3 )查看商品详情页,具体如下。
8.8.4 完成底部按钮功能
1 )修改 src\pages\GoodsDetail.vue ,添加底部按钮区域,具体代码如下。
<van-action-bar-icon icon="chat-o" text=" 客服 " @click="sorry" />
<van-action-bar-icon icon="cart-o" text=" 购物车 " @click="onClickCart" />
<van-action-bar-button type="warning" text=" 加入购物车 " @click="addCart"
/>
<van-action-bar-button type="danger" text=" 立即购买 " @click="sorry" />
2 )导入路由和 showToast ,具体代码如下。
import { showToast } from 'vant'
import { useRouter } from 'vue-router'
const router = useRouter();
3 )处理逻辑,具体代码如下。
<script setup>
……(原有代码)
const sorry = () => {
showToast(' 暂无后续逻辑 ~') 66
}
const onClickCart = () => {
router.push({ name: 'cart' })
}
const addCart = () => {
showToast(' 暂无后续逻辑 ~')
}
</script>
4 )单击“购物车”跳转到购物车页面,具体如下。
8.9 购物车页开发
8.9.1 添加到购物车
1 )创建 src\store\cart.js ,具体代码如下。
import { defineStore } from 'pinia'
import { ref } from 'vue'
const useCart = defineStore('cart', () => {
const cart = ref([])
const addToCart = goods => {
const item = cart.value.find(item => goods.id == item.id)
if (item) {
item.num += goods.num
} else {
cart.value.push(goods) 67
}
}
const removeCart = id => {
cart.value.forEach((item, index) => {
if (item.id == id) {
cart.value.splice(index, 1)
}
})
}
const cartCount = () => {
let sum = 0
cart.value.forEach(item => {
sum += item.num
})
return sum || undefined
}
return { cart, addToCart, removeCart, cartCount }
}, {
persist: {
enabled: true,
strategies: [
{
key: 'cart',
storage: localStorage
}
]
}
})
export default useCart
2 )底部导航栏显示购物车中的商品数量
修改 src\components\TabBar.vue ,具体代码如下。
<van-tabbar-item replace :to="{ name: 'cart' }" icon="shopping-cart-o" :
badge="cartCount()" > 购物车 </van-tabbar-item>
<script setup>
import useCart from '../stores/cart'
const { cartCount } = useCart() 68
</script>
底部导航栏的购物车中商品数量效果,具体如下。
3 )商品详情页显示购物车中的商品数量
修改 src\pages\GoodsDetail.vue ,具体代码如下。
import useCart from '../stores/cart'
const { cartCount, addToCart } = useCart()
绑定 cartCount() ,具体代码如下。
<van-action-bar-icon icon="cart-o" :badge="cartCount()" @click="onClickC
art" text=" 购物车 " />
4 )添加到购物车,具体代码如下。
const addCart = () => {
addToCart({ id: props.id, num: 1, checked: true })
showToast({
message: ' 添加成功 '
})
}
5 )查看添加购物车效果,具体如下。 69
8.9.2 加载购物车数据
1 )修改 src\api\index.js ,具体代码如下。
// 购物车接口
export function getCartList(params) {
return request.get('/home/goods/cart', { params })
}
2 )修改 src\pages\Cart.vue ,具体代码如下。
<script setup>
import { ref, onMounted } from 'vue'
import { getCartList } from '../api'
import useCart from '../stores/cart'
const { cart } = useCart()
const goodsList = ref([])
onMounted(() => { 70
loadCart()
console.log(goodsList)
})
// 加载购物车数据
const loadCart = async () => {
const ids = cart.map(item => item.id)
goodsList.value = await getCartList({ ids: ids.join(',') })
goodsList.value.forEach(goods => {
goods.cart = cart.find(item => goods.id == item.id)
})
}
</script>
3 )购物车数据,具体如下。
8.9.3 显示购物车页面
1 )导入空购物车图片,具体代码如下。
import cartEmptyImage from '../assets/images/cart_empty.png'
2 )渲染页面数据,具体代码如下。
<template>
<div class="cart">
<div class="cart-container">
<van-empty v-show="goodsList.length == 0" description=" 购物车目前还没
有商品 " :image="cartEmptyImage">
<router-link :to="{ name: 'category' }">
<van-button round type="danger" class="bottom-button"> 去购物 </va
n-button>
</router-link>
</van-empty>
<!-- 购物车列表 --> 71
<div v-for="item in goodsList" :key="item.id" class="list">
<van-swipe-cell>
<!-- 复选框 -->
<div class="checkbox">
<van-checkbox :name="item" v-model="item.cart.checked" checked
-color="#f11a27"></van-checkbox>
</div>
<!-- 商品图片 -->
<div class="image">
<router-link :to="{ name: 'goodsDetail', params: { id: item.id
} }">
<van-image width="50" height="50" :src="item.picture" />
</router-link>
</div>
<!-- 商品信息 -->
<div class="goods-info">
<div>{{ item.name }}</div>
<div class="bottom">
<div class="price"><span> </span>{{ item.price }}</div>
<van-stepper v-model="item.cart.num" theme="round" button-s
ize="22" disable-input />
</div>
</div>
<!-- 左滑删除 -->
<template #right>
<van-button aquare icon="delete" type="danger" class="delete-b
utton" />
</template>
</van-swipe-cell>
</div>
</div>
</div>
</template>
3 )编写样式代码,具体代码如下。
<style lang="less" scoped>
.cart {
margin: 0.3rem;
padding: .05rem 0 3rem 0; 72
.cart-container {
margin-top: 1rem;
.list {
position: relative;
height: 5rem;
border-radius: 10px;
box-shadow: 0px 0px 5px #ccc;
margin-bottom: 1rem;
.checkbox {
position: absolute;
top: 1.7rem;
left: .2rem;
}
.image {
position: absolute;
top: .7rem;
left: 2rem;
}
.goods-info {
height: 5rem;
display: flex;
justify-content: space-around;
flex-direction: column;
padding: 0 1rem 0 6rem;
.bottom {
display: flex;
justify-content: space-between;
align-items: center;
.price {
color: #c82519;
font-size: .45rem;
}
}
}
.delete-button {
width: 2rem;
height: 100%;
} 73
}
.bottom-button {
width: 7rem;
height: 2rem;
}
}
.settlement {
margin-bottom: -0.1rem;
}
}
:deep(.van-submit-bar) {
bottom: 3.5rem;
}
:deep(.van-swipe-cell) {
width: 100%;
}
</style>
4 )查看购物车数据,具体如下。
8.9.4 删除(左滑)购物车中的商品
1 )修改 src\pages\Cart.vue ,具体代码如下。
<van-button aquare icon="delete" type="danger" class="delete-button" @cl
ick="onDelete(item)" />
2 )导入 removeCart ,具体代码如下。
const { cart , removeCart } = useCart()
3 )处理删除逻辑,具体代码如下。
<script setup> 74
……(原有代码)
// 删除商品
const onDelete = item => {
const id = item.id
goodsList.value.forEach((item, index) => {
if (item.id == id) {
goodsList.value.splice(index, 1)
}
})
removeCart(id)
}
</script>
4 )测试是否可以删除成功,左滑“陕西蜜梨”,将其删除,具体如下。
8.9.5 实现购物车结算功能
1 )实现结算部分的结构,具体代码如下。
<div class="cart">
……(原有代码)
<!-- 结算 -->
<van-submit-bar v-show="goodsList.length" :price="total * 100" button
text=" 去结算 " button-type="primary" @submit="onSubmit" class="settlement">
<van-checkbox v-model="allChecked" checked-color="#f11a27" @click="o 75
nCheckAll"> 全选 </van-checkbox>
</van-submit-bar>
</div>
2 )定义 allChecked 初始为 false ,具体代码如下。
const goodsList = ref([])
const allChecked = ref(false)
3 )查看结算区域的效果,具体如下。
4 )实现“总金额”和“全选”按钮的逻辑,具体代码如下。
import { ref, onMounted , computed } from 'vue'
// 总金额
const total = computed(() => {
let sum = 0
goodsList.value.forEach(item => {
if (item.cart.checked) {
sum += item.price * item.cart.num
}
})
return sum
})
// 全选
const onCheckAll = () => {
goodsList.value.forEach(el => {
el.cart.checked = allChecked.value
})
}
5 )查看合计处的金额是否正确,具体如下。 76
6 )实现“去结算”逻辑,具体代码如下。
import { showToast } from 'vant'
// 去结算
const onSubmit = () => {
showToast(' 暂无后续逻辑 ~')
}
7 )商品都被选中时,全选按钮高亮,具体代码如下。
<van-checkbox :name="item" v-model="item.cart.checked" @change="onCheck"
checked-color="#f11a27"></van-checkbox>
// 商品都被选中时,全选按钮高亮
const onCheck = () => {
allChecked.value = goodsList.value.every(el => el.cart.checked)
}
8 )页面打开后自动全选,具体代码如下。
onMounted( async () => {
await loadCart()
onCheck() 77
})
9 )商品全部选中时,全选按钮勾选,具体如下。
10 )商品非全选状态时,全选按钮不勾选,具体如下。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值