当前位置:首页 > 技术文章 > 正文内容

Vue3 “微商城”前台开发文档(vue开发pc端商城项目)

zonemu11小时前技术文章3

1.准备工作

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

在上述命令中,less 依赖用于使项目支持使用 Less(一种 CSS 预处理语言)编写样式。

(4)打开 index.html 修改标题。

<title>微商城</title>

(5)禁止双击缩放。

<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />

说明:本文档中标注红的代码为当前步骤新增或修改的代码。

1.2 定义路由

(1)创建 src\router\index.js,具体代码如下。

import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(),
	routes: [
    { 
      path: '/',
      redirect: '/home',
      meta: { title: '首页' }
    },
    {
      path: '/home',
      component: () => import('../pages/Home.vue'),
      name: 'home',
      meta: { title: '首页', 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
if (title) {
  document.title = title + ' - 微商城'
}
  next()
})
export default router

解释

meta:用于在路由规则中保存一些附加信息,从而控制页面标题和标签栏等。

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'
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

1.3 实现底部导航栏

(1)创建 src\components\TabBar.vue,具体代码如下。

<template>
<van-tabbar route fixed placeholder border>
<van-tabbar-item replace :to="{ name: 'home' }" icon="home-o">首页</van-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">我的</van-tabbar-item>
</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)访问测试,此时能看到页面底部的导航栏,效果如下。


2.首页开发

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)访问测试,搜索框的效果如下。

2.2 实现首页轮播图

(1)创建 src\components\HomeSwiper.vue,具体代码如下。

<template>
  <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)查看轮播图效果,具体如下。

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 },
{ 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)查看功能按钮区效果,具体如下。

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%;
    	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)查看商品信息展示区效果,具体如下。

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: '128.00' },
  { name: '减压弹力球', list_pic_url: '/images/new2.jpg', retail_price: '89.00' },
  { name: '简约一字夹发夹', list_pic_url: '/images/new3.jpg', retail_price:'12.8' },
  { name: '毛线小兔子耳朵发夹', list_pic_url: '/images/new4.jpg', retail_price: '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;
        }
        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)查看每周上新效果,具体如下。

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>
  </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',
  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;
    }
  }
}
</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)查看人气推荐效果,具体如下。


3.消息页开发

(1)先实现页面顶部的导航栏,修改 src\App.vue,具体代码如下。

<template>
<van-nav-bar :title="$route.meta.title" v-show="$route.meta.isShowNav" @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>

页面顶部的导航栏效果,具体如下。

(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.value" :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: "食品旗舰店:大量新品到货,速来选购"
  }
]
</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)消息页面的效果,具体如下。


4.用户登录和注册

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" :model="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="请输入密码" name="password"
      clearable type="password" :rules="passwordRules"></van-field>
    </van-cell-group>
    <van-cell-group>
    	<van-button block round type="primary" native-type="submit">登录</van-button>
    </van-cell-group>
  </van-form>
</template>
<script setup>
import { ref, reactive } from 'vue'
const form = reactive({
  username: 'demo1',
  password: '123456'
})
// 定义验证规则
const usernameRules = ref([
  { required: true, message: '用户名不能为空' },
  { 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)查看登录页效果,具体如下。


4.2 封装网络请求

(1)服务器端 API,创建 src\config.js,具体代码如下。

export default {
  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: '加载中...',
    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'
    })
    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)
}

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'
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()
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)查看登录页面效果,具体如下。


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
  })
}

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
    }
  },
]

(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" :model="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" name="confirmPassword" :rules="comfirmPasswordRules"></van-field>
    </van-cell-group>
    <van-cell-group>
   	 <van-button block round type="primary" native-type="submit">注册</van-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 router = useRouter()
// 定义验证规则
const usernameRules = ref([
  { required: true, message: '用户名不能为空', trigger: 'onBlur' },
	{ pattern: /^\w{3,16}$/, message: '用户名长度为 3-16 个字符', trigger: 'onBlur' }
])
const passwordRules = ref([
  { required: true, message: '密码不能为空', trigger: 'onBlur' },
	{ pattern: /^\w{6,24}$/, message: '密码必须为 6-24 位英文字母或数字', trigger: 'onBlur' }
])
const comfirmPasswordRules = ref([
  { required: true, message: '密码不能为空', trigger: 'onBlur' },
	{ pattern: /^\w{6,24}$/, message: '密码必须为 6-24 位英文字母或数字', trigger: '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
    })
    router.push({ name: 'user' })
  }
}
const onFailed = errorInfo => {
	console.log('failed', errorInfo)
}
</script>
<style lang="less" scoped>
button {
	position: fixed;
	top: 270px;
}
.tip {
	position: fixed;
	top: 330px;
	text-align: center;
	width: 100%;
	font-size: 14px;
	color: #666;
}
</style>

(4)查看注册页面效果,具体如下。


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">退出</van-button>
  </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>
<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;
}
: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)单击“登录”链接,进入登录页面,登录成功后页面效果,具体如下。

(4)退出登录后页面效果,具体如下。

(5)单击“注册”链接,跳转到注册页面,输入信息后,单击“注册”按钮,注册成

功则直接登录,具体如下。


6 分类页开发

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)
}
</script>

(3)在控制台打印接口返回的数据,具体如下。

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) {
		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)在控制台打印转换后的数据,具体如下。

6.3 显示分类数据

(1)修改 src\pages\Category.vue,具体代码如下。

<script setup>
import { onMounted, ref } from 'vue'
import { getCategoryList } from '../api'
const menus = ref([])

(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>
<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;
    }
  }
}
.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;
           }
         }
        }
      }
    }
  }
}
</style>

(4)查看数据渲染页面后的分类页,具体如下。

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>
……(原有代码)
// 单击左侧菜单项
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()
	})
})

上述代码中的 nextTick()用于在 DOM 更新之后执行传入的回调函数,这样才可以正确

获取右侧菜单的高度。

(5)为右侧列表添加一个 ref 属性,属性值为 itemList,具体代码如下。

const menus = ref([])
const rightLiTops = ref([])
const itemList = ref()

为元素绑定一个 ref 属性,且属性值为 itemList,具体代码如下。

<div class="menu-right" ref="itemList">

在上述代码中,ref 属性用于获取 DOM 对象的引用,itemList 需要被赋值为 ref()。

(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)
	})
	rightLiTops.value = itemArray
}
</script>

(7)单击当前菜单项时,通过 index 索引会得到右侧菜单每一块<li>标签的高度,具

体代码如下。

// 单击左侧菜单项
const clickList = index => {
	console.log(rightLiTops.value[index])
}

(8)在控制台打印,当单击左侧菜单项时输出对应的右侧菜单的高度,例如,0 表示

单击左侧第一个菜单项时,对应的右侧菜单的高度,依次类推,具体如下。


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(() => {
	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 => {
	scrollY.value = rightLiTops.value[index]
	rightBScroll.scrollTo(0, -scrollY.value)
}

(7)单击左侧菜单项,右侧菜单滚动到相应位置,具体如下。


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>
      <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)单击左侧菜单的最后一项时,右侧菜单可以滚动到相应的位置,具体如下。


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="menuList">

(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
    }
  })
})
</script>

7 商品列表页开发

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: true 
    }
	},
]

在上述代码中,“props: true”用于将路由参数作为组件的 props 传递,使得在组件中

可以通过 props 接收路由参数。

(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 () => {
	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)测试能否接收到参数,具体如下。


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([])
let last_id = '0'

在上述代码中,last_id 用于记录当前查询的商品列表中的最后一个商品的 id,用于在

下次发送请求时,将 last_id 传给服务器,从而实现“加载更多”的功能。

(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">yen{{ item.price }}</span>
          <span class="sell">剩余 {{ item.stock }} 件</span>
        </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;
      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)查看商品列表,具体如下。


7.4 实现加载更多

(1)定义 is_last 默认为 false,具体代码如下。

const goodsList = ref([])
const is_last = ref(false)

(2)添加“加载更多”按钮,具体代码如下。

<div class="goods-list">
……(原有代码)
  <van-button class="more" :disabled="is_last" v-if="goodsList.length !== 0" size="large"
  type="primary" plain hairline @click="getMore">加载更多</van-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: '列表为空',
		type: 'fail'
  })
}

(7)查看已经到达顶部的效果,具体如下。

(8)查看列表为空的效果,具体如下。


8 商品详情页开发

8.1 单击商品跳转到商品详情页

(1)修改 src\router\index.js,添加商品详情页的路由,具体代码如下。

routes: [
……(原有代码)
  {
    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)测试能否接收到参数,单击商品,可以传递过去参数,具体如下。



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)加载商品详情数据和轮播图数据,具体代码如下。

// 加载商品详情
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.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> 
</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>
<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;
      }
    }
    .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;
  font-size: 28px;
}
</style>

(3)查看商品详情页,具体如下。


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('暂无后续逻辑~')
}
const onClickCart = () => {
	router.push({ name: 'cart' })
}
const addCart = () => {
	showToast('暂无后续逻辑~')
}
</script>

(4)单击“购物车”跳转到购物车页面,具体如下。



9 购物车页开发

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)
    }
  }
  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()
</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="onClickCart" text="购物车" />  


(4)添加到购物车,具体代码如下。

const addCart = () => {
	addToCart({ id: props.id, num: 1, checked: true })
	showToast({
    message: '添加成功'
  })
}

(5)查看添加购物车效果,具体如下。


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(() => {
	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)购物车数据,具体如下。


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">去购物</van-button>
				</router-link>
  		</van-empty>
  <!-- 购物车列表 -->
  <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-size="22" disable-input /></div>
  	</div>
  <!-- 左滑删除 -->
  <template #right>
    <van-button aquare icon="delete" type="danger" class="delete-button" />
      	</template>
			</van-swipe-cell>
  		</div>
  	</div>
  </div>
</template>

(3)编写样式代码,具体代码如下。

<style lang="less" scoped>
.cart {
  margin: 0.3rem;
  padding: .05rem 0 3rem 0;
  .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%;
      }
    }
    .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)查看购物车数据,具体如下。


9.4 删除(左滑)购物车中的商品

(1)修改 src\pages\Cart.vue,具体代码如下。

<van-button aquare icon="delete" type="danger" class="delete-button" @click="onDelete(item)"
/>

(2)导入 removeCart,具体代码如下。

const { cart, removeCart } = useCart()

(3)处理删除逻辑,具体代码如下。

<script setup>
……(原有代码)
// 删除商品
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)测试是否可以删除成功,左滑“陕西蜜梨”,将其删除,具体如下。


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="onCheckAll"> 全选
    </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)查看合计处的金额是否正确,具体如下。


(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()
})

(9)商品全部选中时,全选按钮勾选,具体如下。

(10)商品非全选状态时,全选按钮不勾选,具体如下。

相关文章

Linux 发行版介绍 Zenwalk Linux(linux发行版2021)

Zenwalk Linux是基于Slackware的GNU/Linux发行版, 100%兼容Slackware。 致力于精简和快捷的图形桌面及多媒体使用。包含整套编程环境和运行库,还提供了常用服务器套...

linux发行版-openSUSE Agama 15安装程序发布:带来多项可用性升级

openSUSE旗下仍在开发中的全新Linux安装工具Agama,于近日推出v15版本,带来了界面增强、实用新功能等一系列改进,为用户带来更顺畅的系统安装体验!界面优化:细节之处见用心新版本在本地化设...

崩溃!3 道 React 面试必卡题,吃透稳过金九银十

凌晨三点还在对着 “React 组件为什么重复渲染” 抓耳挠腮?别慌!今天挑出 3 道让 90% 候选人卡壳的高频题,全是大厂面试官挖的 “坑”,手把手教你见招拆招,看完直接装进面试 “弹药库”!先问...

Vue3开发极简入门(15.1):emits补完-结合v-model

之前代码是通过按钮触发emit,如果希望输入框里的内容在输入之后也能同步到父组件,就可以结合v-model的update事件来操作,具体如下。Son2.vue:<template>...

零基础开始学 Web 前端开发,有什么建议?(附视频教程)

WEB前端看似简单,其实不然,要学的知识点很多很杂,对于零基础学习前端的小伙伴来说,一份详细的前端学习知识点大纲尤为重要。下面,话不多说,直接上干货(全网最全,没有之一)。PS:文末有福利(全阶段视频...

Vue3快速入门(vue3快速上手)

  1.核心语法  1. 1选项式和组合式的区别  Vue2的API设计是Options(选项)风格的。  Vue3的API设计是Composition(组合)风格的。  Options类型的 API...