20230722----重返学习-vue3项目实战-收尾

day-118-one-hundred-and-eighteen-20230722-vue3项目实战-收尾

vue3项目实战-收尾

对数据统一做处理

  • src/utils/http.js
import axios from 'axios'
// 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列)  3)接口可以保存取消的操作
class Http {
  setInterceptor(instance) {
    instance.interceptors.response.use(
      (res) => {
        if (res.data.resultCode == 200) {
          return res.data.data
        }
        return res.data // {resultCode:200,data:{}}
      },
      (err) => {
        return Promise.reject(err)
      }
    )
  }
}
export default new Http()
  • src/api/index.js
import http from '@/utils/http.js'
const API_LIST = {
  queryIndexInfos: '/index-infos' // 首页的获取数据接口,都在这里
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
  return http.get(API_LIST.queryIndexInfos)
}
  • src/views/Home.vue
<script setup>
import { queryIndexInfos } from '../api/index'
const carousels = ref([])
const hotGoodses = ref([])
const newGoodses = ref([])
const recommendGoodses = ref([])
const loading = ref(true)
onMounted(async () => {
  let data = await queryIndexInfos()
  carousels.value = data.carousels // 轮播图的数据获取成功
  hotGoodses.value = data.hotGoodses
  newGoodses.value = data.newGoodses
  recommendGoodses.value = data.recommendGoodses
  loading.value = false

  console.log(hotGoodses.value[0])
})
</script>

后退按钮

  • src/components/TopBar.vue
<template>
  <van-nav-bar left-arrow @click-left="router.back()">
    <template #title>
      <slot name="title">
        {{ title || $route.meta.title }}
      </slot>
    </template>
    <template #right>
      <slot name="right"></slot>
    </template>
  </van-nav-bar>
</template>
<script setup>
defineProps(['title'])
const router = useRouter()
</script>

表单事件初步

  • src/views/Login.vue
<template>
  <div class="login">
    <TopBar :title="isLogin ? '登录' : '注册'"></TopBar>
    <img :src="logo" class="logo" />
    <van-form>
      <van-field
        label="手机号"
        name="loginName"
        v-model="state.loginName"
        :rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]"
      />
      <van-field
        label="密码"
        type="password"
        name="password"
        v-model="state.password"
        :rules="[{ required: true, message: '密码不能为空' }]"
      />
      <van-field label="验证码" name="captcha" v-model="state.captcha">
        <template #button> 验证码 </template>
      </van-field>
      <a class="text" @click="changeLogin">
        {{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }}
      </a>
      <div style="margin: 16px">
        <van-button round block color="#1baeae"> 确认提交 </van-button>
      </div>
    </van-form>
  </div>
</template>
<script setup>
import logo from '@/assets/images/newbee-mall-vue3-app-logo.png'
import { reactive } from 'vue'
const isLogin = ref(false) // 这个状态用来控制当前是哪一个页面
function changeLogin() {
  isLogin.value = !isLogin.value
}

// 对用户的数据进行维护
const state = reactive({
  loginName: '', // 用户名
  password: '', // 密码
  captcha: '' // 验证码
})
</script>

<style scoped lang="less">
.login {
  padding: 0 20px;
  .logo {
    display: block;
    margin: 20px auto 20px;
    width: 140px;
    height: 140px;
  }
}
.text {
  display: inline-block;
  margin-top: 20px;
  margin-bottom: 20px;
  padding: 0 15px;
  color: #1989fa;
  font-size: 14px;
}
</style>

验证码组件封装

  • src/components/Captcha.vue
<template>
  <canvas :width="width" :height="height" ref="canvas" @click="generateCaptcha"></canvas>
</template>
<script setup>
const width = 120
const height = 30
function getRandom() {
  let str = 'abcdefgghijklmnopqrstuvwxyz'
  str += str.toUpperCase()
  str += '0123456789'

  let idx = 0
  let status = ''
  while (idx < 4) {
    status += str[Math.round(Math.random() * 61)]
    idx++
  }
  return status
}
const canvas = ref(null)

const emit = defineEmits(['update:text'])
function generateCaptcha() {
  const context = canvas.value.getContext('2d')
  context.clearRect(0, 0, width, height)
  context.fillStyle = '#00f'
  context.font = 'italic 30px 黑体'
  context.textBaseline = 'top'
  const text = getRandom()
  context.fillText(text, 10, 0)

  for (let i = 0; i < 4; i++) {
    context.beginPath()
    context.moveTo(Math.random() * width, Math.random() * height)
    context.lineTo(Math.random() * width, Math.random() * height)
    context.strokeStyle = '#005588'
    context.stroke()
  }
  emit('update:text', text)
}

defineExpose({
  reset: generateCaptcha
})
onMounted(() => {
  generateCaptcha()
})
</script>

验证码使用及校验

  • src/components/Captcha.vue
<template>
  <canvas :width="width" :height="height" ref="canvas" @click="generateCaptcha"></canvas>
</template>
<script setup>
const width = 120
const height = 30
function getRandom() {
  let str = 'abcdefgghijklmnopqrstuvwxyz'
  str += str.toUpperCase()
  str += '0123456789'

  let idx = 0
  let status = ''
  while (idx < 4) {
    status += str[Math.round(Math.random() * 61)]
    idx++
  }
  return status
}
const canvas = ref(null)

const emit = defineEmits(['update:text'])
function generateCaptcha() {
  const context = canvas.value.getContext('2d')
  context.clearRect(0, 0, width, height)
  context.fillStyle = '#00f'
  context.font = 'italic 30px 黑体'
  context.textBaseline = 'top'
  const text = getRandom()
  context.fillText(text, 10, 0)

  for (let i = 0; i < 4; i++) {
    context.beginPath()
    context.moveTo(Math.random() * width, Math.random() * height)
    context.lineTo(Math.random() * width, Math.random() * height)
    context.strokeStyle = '#005588'
    context.stroke()
  }
  emit('update:text', text)
}

defineExpose({
  reset: generateCaptcha
})
onMounted(() => {
  generateCaptcha()
})
</script>
  • src/views/Login.vue
<template>
  <div class="login">
    <TopBar :title="isLogin ? '登录' : '注册'"></TopBar>
    <img :src="logo" class="logo" />
    <van-form>
      <van-field
        label="手机号"
        name="loginName"
        v-model="state.loginName"
        :rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]"
      />
      <van-field
        label="密码"
        type="password"
        name="password"
        v-model="state.password"
        :rules="[{ required: true, message: '密码不能为空' }]"
      />
      <van-field
        label="验证码"
        name="captcha"
        v-model="state.captcha"
        :rules="[{ validator: validatorCaptcha, message: '验证码不正确' }]"
      >
        <template #button>
          <Captcha v-model:text="captchaImgText" ref="captcha"></Captcha>
        </template>
      </van-field>
      <a class="text" @click="changeLogin">
        {{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }}
      </a>
      <div style="margin: 16px">
        <van-button round block color="#1baeae"> 确认提交 </van-button>
      </div>
    </van-form>
    {{ captchaImgText }}
  </div>
</template>
<script setup>
import logo from '@/assets/images/newbee-mall-vue3-app-logo.png'
import { reactive } from 'vue'
const isLogin = ref(false) // 这个状态用来控制当前是哪一个页面
function changeLogin() {
  isLogin.value = !isLogin.value
}

// 对用户的数据进行维护
const state = reactive({
  loginName: '', // 用户名
  password: '', // 密码
  captcha: '' // 验证码
})
const captchaImgText = ref('')
const captcha = ref(null)
function validatorCaptcha() {
  let isSame = state.captcha === captchaImgText.value // 如果相同
  if (!isSame) {
    captcha.value.reset()
    return false
  }
  return true
}
</script>

<style scoped lang="less">
.login {
  padding: 0 20px;
  .logo {
    display: block;
    margin: 20px auto 20px;
    width: 140px;
    height: 140px;
  }
}
.text {
  display: inline-block;
  margin-top: 20px;
  margin-bottom: 20px;
  padding: 0 15px;
  color: #1989fa;
  font-size: 14px;
}
</style>

md5摘要算法

pnpm i blueimp-md5
  1. md5特点?
    • 不是加密算法,摘要算法。
    1. 内容不同摘要的结果不同
    2. 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
    3. 生成长度一致
    4. 相同的内容生成的结果一直
    5. 不可逆
  • src/api/index.js
import http from '@/utils/http.js'
import md5 from 'blueimp-md5'
const API_LIST = {
  queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
  userLogin: '/user/login',
  userRegister: '/user/register'
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
  return http.get(API_LIST.queryIndexInfos)
}
// md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
// 3) 生成长度一致  4)相同的内容生成的结果一直 5) 不可逆
export const userLogin = (loginName, password) => {
  password = md5(password)
  return http.post(API_LIST.userLogin, {
    loginName,
    passwordMd5: password
  })
}
// 用户注册
export const userRegister = (loginName, password) => {
  return http.post(API_LIST.userRegister, {
    loginName,
    password
  })
}

注册登录操作

  • 核心操作:

    • src/utils/http.js

      import axios from 'axios'
      // 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列)  3)接口可以保存取消的操作
      class Http {
        setInterceptor(instance) {
          instance.interceptors.response.use(
            (res) => {
              if (res.data.resultCode == 200) {
                // 对返回值的状态码是200的情况统一处理
                return res.data.data
              }
              if (res.data.resultCode === 500) {
                return Promise.reject(res.data)
              }
              return res.data // {resultCode:200,data:{}}
            },
            (err) => {
              return Promise.reject(err)
            }
          )
        }
      }
      export default new Http()
      
    • src/api/index.js

      import http from '@/utils/http.js'
      import md5 from 'blueimp-md5'
      const API_LIST = {
        userLogin: '/user/login',
        userRegister: '/user/register'
      }
      // md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
      // 3) 生成长度一致  4)相同的内容生成的结果一直 5) 不可逆
      export const userLogin = (loginName, password) => {
        password = md5(password)
        return http.post(API_LIST.userLogin, {
          loginName,
          passwordMd5: password
        })
      }
      // 用户注册
      export const userRegister = (loginName, password) => {
        return http.post(API_LIST.userRegister, {
          loginName,
          password
        })
      }
      
    • src/views/Login.vue

      <template>
        <div class="login">
          <TopBar :title="isLogin ? '登录' : '注册'"></TopBar>
          <van-form @submit="submit">
            <van-field
              label="手机号"
              name="loginName"
              v-model="state.loginName"
              :rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]"
            />
            <van-field
              label="密码"
              type="password"
              name="password"
              v-model="state.password"
              :rules="[{ required: true, message: '密码不能为空' }]"
            />
            <van-field
              label="验证码"
              name="captcha"
              v-model="state.captcha"
              :rules="[{ validator: validatorCaptcha, message: '验证码不正确' }]"
            >
              <template #button>
                <Captcha v-model:text="captchaImgText" ref="captcha"></Captcha>
              </template>
            </van-field>
            <a class="text" @click="changeLogin">
              {{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }}
            </a>
            <div style="margin: 16px">
              <van-button round block color="#1baeae" native-type="submit"> 确认提交 </van-button>
            </div>
          </van-form>
          {{ captchaImgText }}
        </div>
      </template>
      <script setup>
      import { getCurrentInstance, reactive } from 'vue'
      const isLogin = ref(true) // 这个状态用来控制当前是哪一个页面
      function changeLogin() {
        isLogin.value = !isLogin.value
        state.loginName = ''
        state.password = ''
        state.captcha = ''
        captcha.value.reset()
      }
      // 对用户的数据进行维护
      const state = reactive({
        loginName: '', // 用户名
        password: '', // 密码
        captcha: '' // 验证码
      })
      const captchaImgText = ref('')
      const captcha = ref(null)
      function validatorCaptcha() {
        let isSame = state.captcha === captchaImgText.value // 如果相同
        if (!isSame) {
          captcha.value.reset()
          return false
        }
        return true
      }
      // 提交操作
      import { userLogin, userRegister } from '@/api'
      const { proxy } = getCurrentInstance()
      const router = useRouter()
      async function submit() {
        if (isLogin.value) {
          // 如果是200 我们直接拿到的就是token
          try {
            let token = await userLogin(state.loginName, state.password)
            proxy.$showToast('登录成功')
            localStorage.setItem('token', token)
            router.push('/')
          } catch (e) {
            proxy.$showToast(e.message)
          }
        } else {
          try {
            await userRegister(state.loginName, state.password)
            proxy.$showToast('注册成功')
            changeLogin() // 注册成功跳转到登录
          } catch (e) {
            proxy.$showToast(e.message)
          }
        }
      }
      </script>
      
  • 代码示例

    • src/utils/http.js

      import axios from 'axios'
      // 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列)  3)接口可以保存取消的操作
      class Http {
        constructor() {
          // 根据环境变量设置请求的路径
          this.baseURL = import.meta.env.DEV ? 'http://backend-api-01.newbee.ltd/api/v1' : '/'
          this.timeout = 5000
        }
      
        setInterceptor(instance) {
          instance.interceptors.request.use(
            (config) => {
              // 携带token来做处理
              return config
            },
            (err) => {
              return Promise.reject(err)
            }
          )
          instance.interceptors.response.use(
            (res) => {
              if (res.data.resultCode == 200) {
                // 对返回值的状态码是200的情况统一处理
                return res.data.data
              }
              if (res.data.resultCode === 500) {
                return Promise.reject(res.data)
              }
              return res.data // {resultCode:200,data:{}}
            },
            (err) => {
              return Promise.reject(err)
            }
          )
        }
        request(options) {
          // 请求会实现拦截器
          const instance = axios.create() // 1.每次请求要创建一个新的实例
          this.setInterceptor(instance) // 2.设置拦截器
          // 发送请求参数
          return instance({
            ...options,
            baseURL: this.baseURL,
            timeout: this.timeout
          })
        }
        get(url, data) {
          return this.request({
            method: 'get',
            url,
            params: data
          })
        }
        post(url, data) {
          return this.request({
            method: 'post',
            url,
            data
          })
        }
      }
      export default new Http()
      
    • src/api/index.js

      import http from '@/utils/http.js'
      import md5 from 'blueimp-md5'
      const API_LIST = {
        queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
        userLogin: '/user/login',
        userRegister: '/user/register'
      }
      // 首页数据的获取,直接通过 api 这个文件来操作
      export function queryIndexInfos() {
        return http.get(API_LIST.queryIndexInfos)
      }
      // md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
      // 3) 生成长度一致  4)相同的内容生成的结果一直 5) 不可逆
      export const userLogin = (loginName, password) => {
        password = md5(password)
        return http.post(API_LIST.userLogin, {
          loginName,
          passwordMd5: password
        })
      }
      // 用户注册
      export const userRegister = (loginName, password) => {
        return http.post(API_LIST.userRegister, {
          loginName,
          password
        })
      }
      
    • src/components/Captcha.vue

      <template>
        <canvas :width="width" :height="height" ref="canvas" @click="generateCaptcha"></canvas>
      </template>
      <script setup>
      const width = 120
      const height = 30
      function getRandom() {
        let str = 'abcdefgghijklmnopqrstuvwxyz'
        str += str.toUpperCase()
        str += '0123456789'
      
        let idx = 0
        let status = ''
        while (idx < 4) {
          status += str[Math.round(Math.random() * 61)]
          idx++
        }
        return status
      }
      const canvas = ref(null)
      
      const emit = defineEmits(['update:text'])
      function generateCaptcha() {
        const context = canvas.value.getContext('2d')
        context.clearRect(0, 0, width, height)
        context.fillStyle = '#00f'
        context.font = 'italic 30px 黑体'
        context.textBaseline = 'top'
        const text = getRandom()
        context.fillText(text, 10, 0)
      
        for (let i = 0; i < 4; i++) {
          context.beginPath()
          context.moveTo(Math.random() * width, Math.random() * height)
          context.lineTo(Math.random() * width, Math.random() * height)
          context.strokeStyle = '#005588'
          context.stroke()
        }
        emit('update:text', text)
      }
      
      defineExpose({
        reset: generateCaptcha
      })
      onMounted(() => {
        generateCaptcha()
      })
      </script>
      
    • src/views/Login.vue

      <template>
        <div class="login">
          <TopBar :title="isLogin ? '登录' : '注册'"></TopBar>
          <img :src="logo" class="logo" />
          <van-form @submit="submit">
            <van-field
              label="手机号"
              name="loginName"
              v-model="state.loginName"
              :rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]"
            />
            <van-field
              label="密码"
              type="password"
              name="password"
              v-model="state.password"
              :rules="[{ required: true, message: '密码不能为空' }]"
            />
            <van-field
              label="验证码"
              name="captcha"
              v-model="state.captcha"
              :rules="[{ validator: validatorCaptcha, message: '验证码不正确' }]"
            >
              <template #button>
                <Captcha v-model:text="captchaImgText" ref="captcha"></Captcha>
              </template>
            </van-field>
            <a class="text" @click="changeLogin">
              {{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }}
            </a>
            <div style="margin: 16px">
              <van-button round block color="#1baeae" native-type="submit"> 确认提交 </van-button>
            </div>
          </van-form>
          {{ captchaImgText }}
        </div>
      </template>
      <script setup>
      import logo from '@/assets/images/newbee-mall-vue3-app-logo.png'
      import { getCurrentInstance, reactive } from 'vue'
      const isLogin = ref(true) // 这个状态用来控制当前是哪一个页面
      function changeLogin() {
        isLogin.value = !isLogin.value
        state.loginName = ''
        state.password = ''
        state.captcha = ''
        captcha.value.reset()
      }
      // 对用户的数据进行维护
      const state = reactive({
        loginName: '', // 用户名
        password: '', // 密码
        captcha: '' // 验证码
      })
      const captchaImgText = ref('')
      const captcha = ref(null)
      function validatorCaptcha() {
        let isSame = state.captcha === captchaImgText.value // 如果相同
        if (!isSame) {
          captcha.value.reset()
          return false
        }
        return true
      }
      // 提交操作
      import { userLogin, userRegister } from '@/api'
      const { proxy } = getCurrentInstance()
      const router = useRouter()
      async function submit() {
        if (isLogin.value) {
          // 如果是200 我们直接拿到的就是token
          try {
            let token = await userLogin(state.loginName, state.password)
            proxy.$showToast('登录成功')
            localStorage.setItem('token', token)
            router.push('/')
          } catch (e) {
            proxy.$showToast(e.message)
          }
        } else {
          try {
            await userRegister(state.loginName, state.password)
            proxy.$showToast('注册成功')
            changeLogin() // 注册成功跳转到登录
          } catch (e) {
            proxy.$showToast(e.message)
          }
        }
      }
      </script>
      
      <style scoped lang="less">
      .login {
        padding: 0 20px;
        .logo {
          display: block;
          margin: 20px auto 20px;
          width: 140px;
          height: 140px;
        }
      }
      .text {
        display: inline-block;
        margin-top: 20px;
        margin-bottom: 20px;
        padding: 0 15px;
        color: #1989fa;
        font-size: 14px;
      }
      </style>
      
      

登录态校验

  • 代码示例

    • src/router/index.js

      import { createRouter, createWebHistory } from 'vue-router'
      import Home from '../views/Home.vue'
      const router = createRouter({
        history: createWebHistory(import.meta.env.BASE_URL),
        routes: [
          {
            path: '/',
            redirect: '/home'
          },
          {
            path: '/home',
            name: 'home',
            component: Home,
            meta: { title: '首页' }
          },
          {
            path: '/category',
            meta: { title: '分类' },
            name: 'category',
            component: () => import('../views/Category.vue')
          },
          {
            path: '/cart',
            name: 'cart',
            component: () => import('../views/Cart.vue'),
            meta: {
              needLogin: true // 需要登录才能访问的 我们需要配置这个属性
            }
          },
          {
            path: '/user',
            name: 'user',
            component: () => import('../views/User.vue')
          },
          {
            path: '/login',
            name: 'login',
            component: () => import('../views/Login.vue')
          }
        ]
      })
      
      router.beforeEach(async (to, from) => {
        let token = localStorage.getItem('token')
        if (token) {
          // 如果有token 说明登录成功,但是如果你访问的还是登录
          if (to.name === 'login') {
            return { path: '/' }
          }
        } else {
          // 没有登录,跳转到登录页面
          // /a/b  -> ['/a','/a/b']
          if (to.matched.some((item) => item.meta.needLogin)) {
            // 此路由需要登录但是没有登录, 应该跳转到登录页面
            return {
              path: '/login',
              query: {
                redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来
                ...to.query // 当前页面的其他参数也添加进去
              }
            }
          }
        }
      })
      export default router
      
    • src/views/Login.vue

      <template>
        <div class="login">
          <TopBar :title="isLogin ? '登录' : '注册'"></TopBar>
          <img :src="logo" class="logo" />
          <van-form @submit="submit">
            <van-field
              label="手机号"
              name="loginName"
              v-model="state.loginName"
              :rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]"
            />
            <van-field
              label="密码"
              type="password"
              name="password"
              v-model="state.password"
              :rules="[{ required: true, message: '密码不能为空' }]"
            />
            <van-field
              label="验证码"
              name="captcha"
              v-model="state.captcha"
              :rules="[{ validator: validatorCaptcha, message: '验证码不正确' }]"
            >
              <template #button>
                <Captcha v-model:text="captchaImgText" ref="captcha"></Captcha>
              </template>
            </van-field>
            <a class="text" @click="changeLogin">
              {{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }}
            </a>
            <div style="margin: 16px">
              <van-button round block color="#1baeae" native-type="submit"> 确认提交 </van-button>
            </div>
          </van-form>
          {{ captchaImgText }}
        </div>
      </template>
      <script setup>
      import logo from '@/assets/images/newbee-mall-vue3-app-logo.png'
      import { getCurrentInstance, reactive } from 'vue'
      const isLogin = ref(true) // 这个状态用来控制当前是哪一个页面
      function changeLogin() {
        isLogin.value = !isLogin.value
        state.loginName = ''
        state.password = ''
        state.captcha = ''
        captcha.value.reset()
      }
      // 对用户的数据进行维护
      const state = reactive({
        loginName: '', // 用户名
        password: '', // 密码
        captcha: '' // 验证码
      })
      const captchaImgText = ref('')
      const captcha = ref(null)
      function validatorCaptcha() {
        let isSame = state.captcha === captchaImgText.value // 如果相同
        if (!isSame) {
          captcha.value.reset()
          return false
        }
        return true
      }
      // 提交操作
      import { userLogin, userRegister } from '@/api'
      const { proxy } = getCurrentInstance()
      const router = useRouter()
      const route = useRoute()
      async function submit() {
        if (isLogin.value) {
          // 如果是200 我们直接拿到的就是token
          try {
            let token = await userLogin(state.loginName, state.password)
            proxy.$showToast('登录成功')
            localStorage.setItem('token', token)
            // 从别的页面跳转过来,会携带跳回的路径,和其他的查询参数
            let { redirect, ...query } = route.query
            redirect
              ? router.push({
                  path: redirect,
                  query
                })
              : router.push('/')
          } catch (e) {
            proxy.$showToast(e.message)
          }
        } else {
          try {
            await userRegister(state.loginName, state.password)
            proxy.$showToast('注册成功')
            changeLogin() // 注册成功跳转到登录
          } catch (e) {
            proxy.$showToast(e.message)
          }
        }
      }
      </script>
      
      <style scoped lang="less">
      .login {
        padding: 0 20px;
        .logo {
          display: block;
          margin: 20px auto 20px;
          width: 140px;
          height: 140px;
        }
      }
      .text {
        display: inline-block;
        margin-top: 20px;
        margin-bottom: 20px;
        padding: 0 15px;
        color: #1989fa;
        font-size: 14px;
      }
      </style>
      
  • 核心代码

    • src/router/index.js

      const router = createRouter({
        routes: [
          {
            path: '/cart',
            name: 'cart',
            component: () => import('../views/Cart.vue'),
            meta: {
              needLogin: true // 需要登录才能访问的 我们需要配置这个属性
            }
          },
          {
            path: '/login',
            name: 'login',
            component: () => import('../views/Login.vue')
          }
        ]
      })
      
      router.beforeEach(async (to, from) => {
        let token = localStorage.getItem('token')
        if (token) {
          // 如果有token 说明登录成功,但是如果你访问的还是登录
          if (to.name === 'login') {
            return { path: '/' }
          }
        } else {
          // 没有登录,跳转到登录页面
          // /a/b  -> ['/a','/a/b']
          if (to.matched.some((item) => item.meta.needLogin)) {
            // 此路由需要登录但是没有登录, 应该跳转到登录页面
            return {
              path: '/login',
              query: {
                redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来
                ...to.query // 当前页面的其他参数也添加进去
              }
            }
          }
        }
      })
      export default router
      
    • src/views/Login.vue

      <template>
        <div class="login">
          <van-form @submit="submit">
            <div style="margin: 16px">
              <van-button round block color="#1baeae" native-type="submit"> 确认提交 </van-button>
            </div>
          </van-form>
        </div>
      </template>
      <script setup>
      // 提交操作
      import { userLogin, userRegister } from '@/api'
      const { proxy } = getCurrentInstance()
      const router = useRouter()
      const route = useRoute()
      async function submit() {
        if (isLogin.value) {
          // 如果是200 我们直接拿到的就是token
          try {
            let token = await userLogin(state.loginName, state.password)
            proxy.$showToast('登录成功')
            localStorage.setItem('token', token)
            // 从别的页面跳转过来,会携带跳回的路径,和其他的查询参数
            let { redirect, ...query } = route.query
            redirect
              ? router.push({
                  path: redirect,
                  query
                })
              : router.push('/')
          } catch (e) {
            proxy.$showToast(e.message)
          }
        } else {
          //...
        }
      }
      </script>
      

使用pinia的集成

  • 核心代码:

    • src/api/index.js

      import http from '@/utils/http.js'
      import md5 from 'blueimp-md5'
      const API_LIST = {
        queryCategories: '/categories' // 查询分类的路径
      }
      
      // 查询所有的分类
      export function queryCategories() {
        return http.get(API_LIST.queryCategories)
      }
      
      
    • src/stores/category.js

      import { queryCategories } from '@/api'
      export const useCategoryStore = defineStore('category', () => {
        const list = ref([]) // 默认所有分类的信息
        async function getCategories() {
          list.value = await queryCategories()
        }
        return { list, getCategories }
      })
      
    • src/views/Category.vue

      <script setup>
      import classify from '@/assets/images/classify.png'
      import { useCategoryStore } from '../stores/category'
      import { onMounted } from 'vue'
      
      const store = useCategoryStore()
      // 通过storeToRefs 将store中的所有属性,转换成ref不包过方法
      // toRefs 在转化的时候会将函数也转成ref
      const { list } = storeToRefs(store)
      onMounted(async () => {
        // 没有数据我要获取数据
        if (list.value.length == 0) {
          await store.getCategories()
        }
        console.log(`list.value-->`, list.value)
      })
      </script>
      
  • 代码示例:

    • src/api/index.js

      import http from '@/utils/http.js'
      import md5 from 'blueimp-md5'
      const API_LIST = {
        queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
        userLogin: '/user/login',
        userRegister: '/user/register',
        queryCategories: '/categories' // 查询分类的路径
      }
      // 首页数据的获取,直接通过 api 这个文件来操作
      export function queryIndexInfos() {
        return http.get(API_LIST.queryIndexInfos)
      }
      // md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
      // 3) 生成长度一致  4)相同的内容生成的结果一直 5) 不可逆
      export const userLogin = (loginName, password) => {
        password = md5(password)
        return http.post(API_LIST.userLogin, {
          loginName,
          passwordMd5: password
        })
      }
      // 用户注册
      export const userRegister = (loginName, password) => {
        return http.post(API_LIST.userRegister, {
          loginName,
          password
        })
      }
      
      // 查询所有的分类
      export function queryCategories() {
        return http.get(API_LIST.queryCategories)
      }
      
      
    • src/stores/category.js

      import { queryCategories } from '@/api'
      export const useCategoryStore = defineStore('category', () => {
        const list = ref([]) // 默认所有分类的信息
        async function getCategories() {
          list.value = await queryCategories()
        }
        return { list, getCategories }
      })
      
    • src/views/Category.vue

      <script setup>
      import classify from '@/assets/images/classify.png'
      import { useCategoryStore } from '../stores/category'
      import { onMounted } from 'vue'
      
      const store = useCategoryStore()
      // 通过storeToRefs 将store中的所有属性,转换成ref不包过方法
      // toRefs 在转化的时候会将函数也转成ref
      const { list } = storeToRefs(store)
      onMounted(async () => {
        // 没有数据我要获取数据
        if (list.value.length == 0) {
          await store.getCategories()
        }
        console.log(`list.value-->`, list.value)
      })
      </script>
      

多层级渲染

  • src/views/Category.vue
<script setup>
import classify from '@/assets/images/classify.png'
import { useCategoryStore } from '../stores/category'
import { computed, onMounted } from 'vue'

const store = useCategoryStore()
// 通过storeToRefs 将store中的所有属性,转换成ref不包过方法
// toRefs 在转化的时候会将函数也转成ref
const { list } = storeToRefs(store)
onMounted(async () => {
  // 没有数据我要获取数据
  if (list.value.length == 0) {
    await store.getCategories()
  }
})

const activeIndex = ref(5)
const contentArray = computed(() => {
  return list.value[activeIndex.value]?.secondLevelCategoryVOS
})
</script>
<template>
  <TopBar>
    <template #title>
      <van-search placeholder="搜索需要的产品" @click="$router.push('/search')"></van-search>
    </template>
  </TopBar>
  <div class="main">
    <van-sidebar v-model="activeIndex">
      <van-sidebar-item
        :title="category.categoryName"
        v-for="category in list"
        :key="category.categoryName"
      />
    </van-sidebar>
    <div class="content" v-if="contentArray && contentArray.length > 0">
      <div v-for="content in contentArray" :key="content.categoryId">
        <h3 class="title">{{ content.categoryName }}</h3>
        <div class="list">
          <router-link
            :to="{
              path: '/search',
              query: {
                categoryId: item.categoryId
              }
            }"
            v-for="item in content.thirdLevelCategoryVOS"
            :key="item.categoryId"
          >
            <van-image :src="classify" />
            <span>{{ item.categoryName }}</span>
          </router-link>
        </div>
      </div>
    </div>
  </div>
</template>
<style scoped lang="less">
.van-search {
  :deep(.van-search__content) {
    border-radius: 20px;
  }
}
.main {
  height: calc(100vh - 96px);
  display: flex;
}
.van-sidebar {
  width: 120px;
  height: 100%;
  background-color: #f7f8fa;
}
.van-sidebar-item {
  padding: 15px 10px;
}
.content {
  width: 255px;
  height: 100%;
  overflow-x: hidden;
  overflow-y: auto;

  .title {
    padding-left: 15px;
    font-size: 16px;
    line-height: 40px;
    margin-bottom: 15px;
  }
  .list {
    display: flex;
    flex-wrap: wrap;
    padding: 0 10px;
    a {
      margin-right: 2%;
      margin-bottom: 10px;
      width: 32%;
      .van-image {
        display: block;
        margin: 0 auto;
        width: 30px;
        height: 30px;
      }
      span {
        display: block;
        text-align: center;
        line-height: 30px;
        color: #555;
      }
      &:nth-child(3n) {
        margin-right: 0;
      }
    }
  }
}
</style>

新增一个跳转详情页

  • src/utils/http.js
import axios from 'axios'
// 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列)  3)接口可以保存取消的操作
class Http {
  constructor() {
    // 根据环境变量设置请求的路径
    this.baseURL = import.meta.env.DEV ? 'http://backend-api-01.newbee.ltd/api/v1' : '/'
    this.timeout = 5000
  }

  setInterceptor(instance) {
    instance.interceptors.request.use(
      (config) => {
        // 携带token来做处理
        let token = localStorage.getItem('token')
        if (token) {
          config.headers['token'] = token
        }
        return config
      },
      (err) => {
        return Promise.reject(err)
      }
    )

    instance.interceptors.response.use(
      (res) => {
        if (res.data.resultCode == 200) {
          // 对返回值的状态码是200的情况统一处理
          return res.data.data
        }
        if (res.data.resultCode === 500) {
          return Promise.reject(res.data)
        }
        if (res.data.resultCode === 416) {
          localStorage.removeItem('token') // 416 可能是token错误,这个时候清除token,重新刷新
          // 刷新后就在此路由的全局钩子,就会走没有token的逻辑
          return window.location.reload()
          // return Promise.reject(res.data)
        }
        return res.data // {resultCode:200,data:{}}
      },
      (err) => {
        return Promise.reject(err)
      }
    )
  }
  request(options) {
    // 请求会实现拦截器
    const instance = axios.create() // 1.每次请求要创建一个新的实例
    this.setInterceptor(instance) // 2.设置拦截器
    // 发送请求参数
    return instance({
      ...options,
      baseURL: this.baseURL,
      timeout: this.timeout
    })
  }
  get(url, data) {
    return this.request({
      method: 'get',
      url,
      params: data
    })
  }
  post(url, data) {
    return this.request({
      method: 'post',
      url,
      data
    })
  }
}
export default new Http()
  • src/api/index.js
import http from '@/utils/http.js'
import md5 from 'blueimp-md5'
const API_LIST = {
  queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
  userLogin: '/user/login',
  userRegister: '/user/register',
  queryCategories: '/categories', // 查询分类的路径

  queryGoodsInfo: '/goods/detail'
}
export const queryGoodsInfo = (id) => {
  return http.get(API_LIST.queryGoodsInfo + `/${id}`)
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
  return http.get(API_LIST.queryIndexInfos)
}
// md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
// 3) 生成长度一致  4)相同的内容生成的结果一直 5) 不可逆
export const userLogin = (loginName, password) => {
  password = md5(password)
  return http.post(API_LIST.userLogin, {
    loginName,
    passwordMd5: password
  })
}
// 用户注册
export const userRegister = (loginName, password) => {
  return http.post(API_LIST.userRegister, {
    loginName,
    password
  })
}

// 查询所有的分类
export function queryCategories() {
  return http.get(API_LIST.queryCategories)
}
  • src/components/HomeGoodsItem.vue
<script setup>
import { processURL, addPrefix } from '../utils'
defineProps(['title', 'goodsList'])
</script>
<template>
  <div class="title">{{ title }}</div>
  <van-grid :column-num="2" :border="false">
    <van-grid-item v-for="goods in goodsList" :key="goods.goodsId">
      <router-link
        :to="`/detail/${goods.goodsId}`"
        style="display: flex; flex-direction: column; align-items: center"
      >
        <van-image :src="processURL(goods.goodsCoverImg)" lazy-load />
        <div class="desc">
          {{ goods.goodsIntro }}
        </div>
        <div class="price">
          {{ addPrefix(goods.sellingPrice) }}
        </div>
      </router-link>
    </van-grid-item>
  </van-grid>
</template>
<style scoped lang="less">
.title {
  color: @theme;
  font-size: 20px;
  text-align: center;
  font-weight: bold;
  padding: 10px 0;
}
.van-image {
  :deep(.van-image__img) {
    width: 120px;
    height: 140px;
  }
}
.desc {
  font-size: 14px;
  color: #555;
}
.price {
  color: @theme;
  font-size: 16px;
}
</style>

  • src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
  if (!route.params.id) {
    return router.push('/')
  }
  currentGoods.value = await queryGoodsInfo(route.params.id)
})
</script>

<template>
  <div class="detail-box">
    <!-- 导航 -->
    <TopBar title="商品详情"></TopBar>

    <!-- 商品详情 -->
    <div class="info" v-if="currentGoods">
      <van-image lazy-load :src="currentGoods.goodsCoverImg" />
      <div class="desc">
        <h3 class="title">{{ currentGoods.goodsName }}</h3>
        <p class="tag">{{ currentGoods.goodsIntro }}</p>
        <p class="price">¥{{ currentGoods.sellingPrice }}</p>
      </div>
      <div class="tab">
        <a href="javascript:;">概述</a>
        <span>|</span>
        <a href="javascript:;">参数</a>
        <span>|</span>
        <a href="javascript:;">安装服务</a>
        <span>|</span>
        <a href="javascript:;">常见问题</a>
      </div>
      <div class="content" v-html="currentGoods.goodsDetailContent"></div>
    </div>

    <!-- 相关操作 -->
    <van-action-bar style="max-width: 540px">
      <van-action-bar-icon icon="chat-o" text="客服" />
      <van-action-bar-icon icon="cart-o" text="购物车" @click="$router.push('/cart')" />
      <van-action-bar-button color="linear-gradient(90deg,#6bd8d8,#1baeae)" text="加入购物车" />
      <van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
    </van-action-bar>
  </div>
</template>

<script setup></script>

<style lang="less" scoped>
.van-skeleton {
  margin-top: 20px;
}

.detail-box {
  padding-bottom: 50px;
  .info {
    padding: 0 10px;
    .content {
      :deep(img) {
        max-width: 100%;
      }
    }
    .van-image {
      width: 100%;
      min-height: 240px;
    }

    .desc {
      .title {
        font-size: 18px;
        color: #555;
        font-weight: normal;
      }

      .tag {
        padding: 10px 0;
        font-size: 14px;
        color: #999;
      }
      .price {
        font-size: 18px;
        color: #f63515;
      }
    }
    .tab {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 15px 0;
      a,
      span {
        font-size: 15px;
        color: #555;
      }

      a {
        padding: 0 10px;
      }
    }
  }
}
</style>

请求携带token

  1. 在请求拦截器中做,如果本地有token这个字段,就在请求头上带着token。
  • src/utils/http.js
class Http {

  setInterceptor(instance) {
    instance.interceptors.request.use(
      (config) => {
        // 携带token来做处理
        let token = localStorage.getItem('token')
        if (token) {
          config.headers['token'] = token
        }
        return config
      },
      (err) => {
        return Promise.reject(err)
      }
    )
  }
}
  1. 每次请求如果有token,就带上token。
  • src/utils/http.js
import axios from 'axios'
// 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列)  3)接口可以保存取消的操作
class Http {
  constructor() {
    // 根据环境变量设置请求的路径
    this.baseURL = import.meta.env.DEV ? 'http://backend-api-01.newbee.ltd/api/v1' : '/'
    this.timeout = 5000
  }

  setInterceptor(instance) {
    instance.interceptors.request.use(
      (config) => {
        // 携带token来做处理
        let token = localStorage.getItem('token')
        if (token) {
          config.headers['token'] = token
        }
        return config
      },
      (err) => {
        return Promise.reject(err)
      }
    )

    instance.interceptors.response.use(
      (res) => {
        if (res.data.resultCode == 200) {
          // 对返回值的状态码是200的情况统一处理
          return res.data.data
        }
        if (res.data.resultCode === 500) {
          return Promise.reject(res.data)
        }
        return res.data // {resultCode:200,data:{}}
      },
      (err) => {
        return Promise.reject(err)
      }
    )
  }
  request(options) {
    // 请求会实现拦截器
    const instance = axios.create() // 1.每次请求要创建一个新的实例
    this.setInterceptor(instance) // 2.设置拦截器
    // 发送请求参数
    return instance({
      ...options,
      baseURL: this.baseURL,
      timeout: this.timeout
    })
  }
  get(url, data) {
    return this.request({
      method: 'get',
      url,
      params: data
    })
  }
  post(url, data) {
    return this.request({
      method: 'post',
      url,
      data
    })
  }
}
export default new Http()

处理无效token

  • src/utils/http.js
import axios from 'axios'
// 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列)  3)接口可以保存取消的操作
class Http {
  constructor() {
    // 根据环境变量设置请求的路径
    this.baseURL = import.meta.env.DEV ? 'http://backend-api-01.newbee.ltd/api/v1' : '/'
    this.timeout = 5000
  }

  setInterceptor(instance) {
    instance.interceptors.request.use(
      (config) => {
        // 携带token来做处理
        let token = localStorage.getItem('token')
        if (token) {
          config.headers['token'] = token
        }
        return config
      },
      (err) => {
        return Promise.reject(err)
      }
    )

    instance.interceptors.response.use(
      (res) => {
        if (res.data.resultCode == 200) {
          // 对返回值的状态码是200的情况统一处理
          return res.data.data
        }
        if (res.data.resultCode === 500) {
          return Promise.reject(res.data)
        }
        if (res.data.resultCode === 416) {
          localStorage.removeItem('token') // 416 可能是token错误,这个时候清除token,重新刷新
          // 刷新后就在此路由的全局钩子,就会走没有token的逻辑
          return window.location.reload()
          // return Promise.reject(res.data)
        }
        return res.data // {resultCode:200,data:{}}
      },
      (err) => {
        return Promise.reject(err)
      }
    )
  }
  request(options) {
    // 请求会实现拦截器
    const instance = axios.create() // 1.每次请求要创建一个新的实例
    this.setInterceptor(instance) // 2.设置拦截器
    // 发送请求参数
    return instance({
      ...options,
      baseURL: this.baseURL,
      timeout: this.timeout
    })
  }
  get(url, data) {
    return this.request({
      method: 'get',
      url,
      params: data
    })
  }
  post(url, data) {
    return this.request({
      method: 'post',
      url,
      data
    })
  }
}
export default new Http()
class Http {

  setInterceptor(instance) {

    instance.interceptors.response.use(
      (res) => {
        if (res.data.resultCode === 416) {
          localStorage.removeItem('token') // 416 可能是token错误,这个时候清除token,重新刷新
          // 刷新后就在此路由的全局钩子,就会走没有token的逻辑
          return window.location.reload()
          // return Promise.reject(res.data)
        }
        return res.data // {resultCode:200,data:{}}
      },
      (err) => {
        return Promise.reject(err)
      }
    )
  }
}
  • src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      redirect: '/home'
    },
    {
      path: '/home',
      name: 'home',
      component: Home,
      meta: { title: '首页' }
    },
    {
      path: '/category',
      meta: { title: '分类' },
      name: 'category',
      component: () => import('../views/Category.vue')
    },
    {
      path: '/cart',
      name: 'cart',
      component: () => import('../views/Cart.vue'),
      meta: {
        needLogin: true // 需要登录才能访问的 我们需要配置这个属性
      }
    },
    {
      path: '/user',
      name: 'user',
      component: () => import('../views/User.vue')
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('../views/Login.vue')
    },
    {
      path: '/detail/:id',
      name: 'detail',
      meta: {
        needLogin: true
      },
      component: () => import('../views/Detail.vue')
    }
  ]
})

router.beforeEach(async (to, from) => {
  let token = localStorage.getItem('token')
  if (token) {
    // 如果有token 说明登录成功,但是如果你访问的还是登录
    if (to.name === 'login') {
      return { path: '/' }
    }
  } else {
    // 没有登录,跳转到登录页面
    // /a/b  -> ['/a','/a/b']
    if (to.matched.some((item) => item.meta.needLogin)) {
      // 此路由需要登录但是没有登录, 应该跳转到登录页面
      return {
        path: '/login',
        query: {
          redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来
          ...to.query // 当前页面的其他参数也添加进去
        }
      }
    }
  }
})
export default router

购物车相关操作

  • src/api/index.js
// 获取购物车列表
export const queryShopCart = () => http.get('/shop-cart')

// 修改购买数量
export const setCartCount = (cartItemId, goodsCount) => {
  return http.request(
    {
      url: '/shop-cart',
      method: 'PUT'
    },
    {
      cartItemId,
      goodsCount
    }
  )
}
// 删除某条购物车数据
export const removeCart = (cartItemId) => {
  return http.request({
    url: `/shop-cart/${cartItemId}`,
    method: 'DELETE'
  })
}

// 新增购物车数据
export const addNewCart = (goodsId, goodsCount = 1) => {
  return http.post('/shop-cart', {
    goodsId,
    goodsCount
  })
}
  • src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
  const list = ref([])
  // 在pinia中缓存数据
  async function getShopCartList() {
    list.value = await queryShopCart()
  }
  async function setShopCartItem(id, count) {
    await setCartCount(id, count)
    // 将列表中的数据也要更新
  }
  async function removeShopCartItem(id) {
    await removeCart(id)
    // 将列表中的数据也要更新
  }
  async function addShopCartItem(id) {
    await addNewCart(id)
    // 将列表中的数据也要更新
  }

  return { list, getShopCartList, setShopCartItem, removeShopCartItem, addShopCartItem }
})
  • src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
  if (!route.params.id) {
    return router.push('/')
  }
  currentGoods.value = await queryGoodsInfo(route.params.id)
})
</script>

<template>
  <div class="detail-box">
    <!-- 导航 -->
    <TopBar title="商品详情"></TopBar>

    <!-- 商品详情 -->
    <div class="info" v-if="currentGoods">
      <van-image lazy-load :src="currentGoods.goodsCoverImg" />
      <div class="desc">
        <h3 class="title">{{ currentGoods.goodsName }}</h3>
        <p class="tag">{{ currentGoods.goodsIntro }}</p>
        <p class="price">¥{{ currentGoods.sellingPrice }}</p>
      </div>
      <div class="tab">
        <a href="javascript:;">概述</a>
        <span>|</span>
        <a href="javascript:;">参数</a>
        <span>|</span>
        <a href="javascript:;">安装服务</a>
        <span>|</span>
        <a href="javascript:;">常见问题</a>
      </div>
      <div class="content" v-html="currentGoods.goodsDetailContent"></div>
    </div>

    <!-- 相关操作 -->
    <van-action-bar style="max-width: 540px">
      <van-action-bar-icon icon="chat-o" text="客服" />
      <van-action-bar-icon icon="cart-o" text="购物车" @click="$router.push('/cart')" />
      <van-action-bar-button color="linear-gradient(90deg,#6bd8d8,#1baeae)" text="加入购物车" />
      <van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
    </van-action-bar>
  </div>
</template>

<script setup></script>

<style lang="less" scoped>
.van-skeleton {
  margin-top: 20px;
}

.detail-box {
  padding-bottom: 50px;
  .info {
    padding: 0 10px;
    .content {
      :deep(img) {
        max-width: 100%;
      }
    }
    .van-image {
      width: 100%;
      min-height: 240px;
    }

    .desc {
      .title {
        font-size: 18px;
        color: #555;
        font-weight: normal;
      }

      .tag {
        padding: 10px 0;
        font-size: 14px;
        color: #999;
      }
      .price {
        font-size: 18px;
        color: #f63515;
      }
    }
    .tab {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 15px 0;
      a,
      span {
        font-size: 15px;
        color: #555;
      }

      a {
        padding: 0 10px;
      }
    }
  }
}
</style>

购物车列表获取

  1. 在pinia中定义两个状态,一个hasCartStore表示是否已经请求过。一个list表示购物车列表。
  2. 购物车列表需要在用户跳转时,于路由前置守卫中在pinia中获取。
    • 如果有token,就从pinia中获取hasCartStore。
      • hasCartStore为true,就代表已经请求过后端了,页面中就可以直接获取到pinia中的购物车列表了。
      • hasCartStore为false,就表示没请求过后端了。调用pinia中的方法,更新数据,在请求完成后,缓存数据,并设置hasCartStore为true。
  • src/api/index.js
import http from '@/utils/http.js'
import md5 from 'blueimp-md5'
const API_LIST = {
  queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
  userLogin: '/user/login',
  userRegister: '/user/register',
  queryCategories: '/categories', // 查询分类的路径

  queryGoodsInfo: '/goods/detail'
}

// 获取购物车列表
export const queryShopCart = () => http.get('/shop-cart')

// 修改购买数量
export const setCartCount = (cartItemId, goodsCount) => {
  return http.request(
    {
      url: '/shop-cart',
      method: 'PUT'
    },
    {
      cartItemId,
      goodsCount
    }
  )
}
// 删除某条购物车数据
export const removeCart = (cartItemId) => {
  return http.request({
    url: `/shop-cart/${cartItemId}`,
    method: 'DELETE'
  })
}

// 新增购物车数据
export const addNewCart = (goodsId, goodsCount = 1) => {
  return http.post('/shop-cart', {
    goodsId,
    goodsCount
  })
}

export const queryGoodsInfo = (id) => {
  return http.get(API_LIST.queryGoodsInfo + `/${id}`)
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
  return http.get(API_LIST.queryIndexInfos)
}
// md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
// 3) 生成长度一致  4)相同的内容生成的结果一直 5) 不可逆
export const userLogin = (loginName, password) => {
  password = md5(password)
  return http.post(API_LIST.userLogin, {
    loginName,
    passwordMd5: password
  })
}
// 用户注册
export const userRegister = (loginName, password) => {
  return http.post(API_LIST.userRegister, {
    loginName,
    password
  })
}

// 查询所有的分类
export function queryCategories() {
  return http.get(API_LIST.queryCategories)
}
  • src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
  const list = ref([])
  const hasCartStore = ref(false)
  // 在pinia中缓存数据
  async function getShopCartList() {
    list.value = await queryShopCart()
    hasCartStore.value = true
  }

  async function setShopCartItem(id, count) {
    await setCartCount(id, count)
    // 将列表中的数据也要更新
  }
  async function removeShopCartItem(id) {
    await removeCart(id)
    // 将列表中的数据也要更新
  }
  async function addShopCartItem(id) {
    await addNewCart(id)
    // 将列表中的数据也要更新
  }

  return {
    list,
    getShopCartList,
    setShopCartItem,
    removeShopCartItem,
    addShopCartItem,
    hasCartStore
  }
})
  • src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import { useCartStore } from '@/stores/cart.js'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      redirect: '/home'
    },
    {
      path: '/home',
      name: 'home',
      component: Home,
      meta: { title: '首页' }
    },
    {
      path: '/category',
      meta: { title: '分类' },
      name: 'category',
      component: () => import('../views/Category.vue')
    },
    {
      path: '/cart',
      name: 'cart',
      component: () => import('../views/Cart.vue'),
      meta: {
        needLogin: true // 需要登录才能访问的 我们需要配置这个属性
      }
    },
    {
      path: '/user',
      name: 'user',
      component: () => import('../views/User.vue')
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('../views/Login.vue')
    },
    {
      path: '/detail/:id',
      name: 'detail',
      meta: {
        needLogin: true
      },
      component: () => import('../views/Detail.vue')
    }
  ]
})

// 登陆态校验。
router.beforeEach(async (to, from) => {
  let token = localStorage.getItem('token')
  if (token) {
    // 如果有token 说明登录成功,但是如果你访问的还是登录
    if (to.name === 'login') {
      return { path: '/' }
    }
  } else {
    // 没有登录,跳转到登录页面
    // /a/b  -> ['/a','/a/b']
    if (to.matched.some((item) => item.meta.needLogin)) {
      // 此路由需要登录但是没有登录, 应该跳转到登录页面
      return {
        path: '/login',
        query: {
          redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来
          ...to.query // 当前页面的其他参数也添加进去
        }
      }
    }
  }
})

//
router.beforeEach(() => {
  let token = localStorage.getItem('token')
  if (token) {
    const store = useCartStore()
    if (!store.hasCartStore) {
      // 如果当前没有获取过购物车信息
      store.getShopCartList()
    }
  }
})

export default router
  • src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
  if (!route.params.id) {
    return router.push('/')
  }
  currentGoods.value = await queryGoodsInfo(route.params.id)
})
</script>

<template>
  <div class="detail-box">
    <!-- 导航 -->
    <TopBar title="商品详情"></TopBar>

    <!-- 商品详情 -->
    <div class="info" v-if="currentGoods">
      <van-image lazy-load :src="currentGoods.goodsCoverImg" />
      <div class="desc">
        <h3 class="title">{{ currentGoods.goodsName }}</h3>
        <p class="tag">{{ currentGoods.goodsIntro }}</p>
        <p class="price">¥{{ currentGoods.sellingPrice }}</p>
      </div>
      <div class="tab">
        <a href="javascript:;">概述</a>
        <span>|</span>
        <a href="javascript:;">参数</a>
        <span>|</span>
        <a href="javascript:;">安装服务</a>
        <span>|</span>
        <a href="javascript:;">常见问题</a>
      </div>
      <div class="content" v-html="currentGoods.goodsDetailContent"></div>
    </div>

    <!-- 相关操作 -->
    <van-action-bar style="max-width: 540px">
      <van-action-bar-icon icon="chat-o" text="客服" />
      <van-action-bar-icon icon="cart-o" text="购物车" @click="$router.push('/cart')" />
      <van-action-bar-button color="linear-gradient(90deg,#6bd8d8,#1baeae)" text="加入购物车" />
      <van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
    </van-action-bar>
  </div>
</template>

<script setup></script>

<style lang="less" scoped>
.van-skeleton {
  margin-top: 20px;
}

.detail-box {
  padding-bottom: 50px;
  .info {
    padding: 0 10px;
    .content {
      :deep(img) {
        max-width: 100%;
      }
    }
    .van-image {
      width: 100%;
      min-height: 240px;
    }

    .desc {
      .title {
        font-size: 18px;
        color: #555;
        font-weight: normal;
      }

      .tag {
        padding: 10px 0;
        font-size: 14px;
        color: #999;
      }
      .price {
        font-size: 18px;
        color: #f63515;
      }
    }
    .tab {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 15px 0;
      a,
      span {
        font-size: 15px;
        color: #555;
      }

      a {
        padding: 0 10px;
      }
    }
  }
}
</style>

Vue.js devtools的使用

购物车数量及添加

  • src/api/index.js
import http from '@/utils/http.js'
import md5 from 'blueimp-md5'
const API_LIST = {
  queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
  userLogin: '/user/login',
  userRegister: '/user/register',
  queryCategories: '/categories', // 查询分类的路径

  queryGoodsInfo: '/goods/detail'
}

// 获取购物车列表
export const queryShopCart = () => http.get('/shop-cart')

// 修改购买数量
export const setCartCount = (cartItemId, goodsCount) => {
  return http.request({
    url: '/shop-cart',
    method: 'PUT',
    data: {
      cartItemId,
      goodsCount
    }
  })
}

// 删除某条购物车数据
export const removeCart = (cartItemId) => {
  return http.request({
    url: `/shop-cart/${cartItemId}`,
    method: 'DELETE'
  })
}

// 新增购物车数据
export const addNewCart = (goodsId, goodsCount = 1) => {
  return http.post('/shop-cart', {
    goodsId,
    goodsCount
  })
}

export const queryGoodsInfo = (id) => {
  return http.get(API_LIST.queryGoodsInfo + `/${id}`)
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
  return http.get(API_LIST.queryIndexInfos)
}
// md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
// 3) 生成长度一致  4)相同的内容生成的结果一直 5) 不可逆
export const userLogin = (loginName, password) => {
  password = md5(password)
  return http.post(API_LIST.userLogin, {
    loginName,
    passwordMd5: password
  })
}
// 用户注册
export const userRegister = (loginName, password) => {
  return http.post(API_LIST.userRegister, {
    loginName,
    password
  })
}

// 查询所有的分类
export function queryCategories() {
  return http.get(API_LIST.queryCategories)
}
  • src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
  const list = ref([])
  const hasCartStore = ref(false)
  // 在pinia中缓存数据
  async function getShopCartList() {
    list.value = await queryShopCart()
    hasCartStore.value = true
  }
  async function setShopCartItem(id, count) {
    await setCartCount(id, count)
    // 将列表中的数据也要更新,如果是修改也可以不发请求 通过id 获取到这一项,更改数量即可
    list.value.find((item) => item.cartItemId == id).goodsCount = count
  }
  async function removeShopCartItem(id) {
    await removeCart(id)
    // 将列表中的数据也要更新
  }
  async function addShopCartItem(id) {
    await addNewCart(id)
    // 将列表中的数据也要更新
    // 正常操作的情况下 应该是添加成功后,返回添加后的值
    getShopCartList()
  }
  return {
    hasCartStore,
    list,
    getShopCartList,
    setShopCartItem,
    removeShopCartItem,
    addShopCartItem
  }
})
  • src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
import { processURL } from '../utils'
import { useCartStore } from '../stores/cart.js'
import { computed, getCurrentInstance } from 'vue'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
  const id = route.params.id
  if (!id) {
    return router.push('/')
  }
  currentGoods.value = await queryGoodsInfo(id)
})
// 购物车store
const store = useCartStore()
const { list } = storeToRefs(store) // 购物车的数据

function findCurrentItem() {
  return list.value.find((item) => item.goodsId == route.params.id)
}

const badge = computed(() => {
  // 暂时先计算出 我们当前选中的详情页的数量
  const current = findCurrentItem()
  return current?.goodsCount || 0
})
const { proxy } = getCurrentInstance()
function addCart() {
  const current = findCurrentItem()
  if (current) {
    // 修改数量
    if (current.goodsCount == 5) {
      return proxy.$showToast('超出最大购买限制')
    }
    store.setShopCartItem(current.cartItemId, current.goodsCount + 1)
  } else {
    // 新增
    store.addShopCartItem(route.params.id)
  }
}
</script>

<template>
  <div class="detail-box">
    <!-- 导航 -->
    <TopBar title="商品详情"></TopBar>
    <!-- 商品详情 -->
    <div class="info" v-if="currentGoods">
      <van-image lazy-load :src="processURL(currentGoods.goodsCoverImg)" />
      <div class="desc">
        <h3 class="title">{{ currentGoods.goodsName }}</h3>
        <p class="tag">{{ currentGoods.goodsIntro }}</p>
        <p class="price">¥{{ currentGoods.sellingPrice }}</p>
      </div>
      <div class="tab">
        <a href="javascript:;">概述</a>
        <span>|</span>
        <a href="javascript:;">参数</a>
        <span>|</span>
        <a href="javascript:;">安装服务</a>
        <span>|</span>
        <a href="javascript:;">常见问题</a>
      </div>
      <div class="content" v-html="currentGoods.goodsDetailContent"></div>
    </div>

    <!-- 相关操作 -->
    <van-action-bar style="max-width: 540px">
      <van-action-bar-icon icon="chat-o" text="客服" />
      <van-action-bar-icon
        icon="cart-o"
        text="购物车"
        @click="$router.push('/cart')"
        :badge="badge"
      />
      <van-action-bar-button
        color="linear-gradient(90deg,#6bd8d8,#1baeae)"
        text="加入购物车"
        @click="addCart"
      />
      <van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
    </van-action-bar>
  </div>
</template>

<script setup></script>

<style lang="less" scoped>
.van-skeleton {
  margin-top: 20px;
}

.detail-box {
  padding-bottom: 50px;
  .info {
    padding: 0 10px;
    .content {
      :deep(img) {
        max-width: 100%;
      }
    }
    .van-image {
      width: 100%;
      min-height: 240px;
    }

    .desc {
      .title {
        font-size: 18px;
        color: #555;
        font-weight: normal;
      }

      .tag {
        padding: 10px 0;
        font-size: 14px;
        color: #999;
      }
      .price {
        font-size: 18px;
        color: #f63515;
      }
    }
    .tab {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 15px 0;
      a,
      span {
        font-size: 15px;
        color: #555;
      }

      a {
        padding: 0 10px;
      }
    }
  }
}
</style>

购物车页渲染

  • src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
  const list = ref([])
  const hasCartStore = ref(false)
  // 在pinia中缓存数据
  async function getShopCartList() {
    list.value = await queryShopCart()
    hasCartStore.value = true
  }
  async function setShopCartItem(id, count) {
    await setCartCount(id, count)
    // 将列表中的数据也要更新,如果是修改也可以不发请求 通过id 获取到这一项,更改数量即可
    list.value.find((item) => item.cartItemId == id).goodsCount = count
  }
  async function removeShopCartItem(id) {
    await removeCart(id)
    // 将列表中的数据也要更新
  }
  async function addShopCartItem(id) {
    await addNewCart(id)
    // 将列表中的数据也要更新
    // 正常操作的情况下 应该是添加成功后,返回添加后的值
    getShopCartList()
  }
  return {
    hasCartStore,
    list,
    getShopCartList,
    setShopCartItem,
    removeShopCartItem,
    addShopCartItem
  }
})
  • src/views/Cart.vue
<script setup>
import empty from '@/assets/images/empty-car.png'
import { useCartStore } from '../stores/cart'
import { storeToRefs } from 'pinia'

const store = useCartStore()
const { list } = storeToRefs(store)
</script>
<template>
  <div class="cart-box">
    <!-- 头部导航 -->
    <TopBar title="购物车"></TopBar>
    <van-empty description="购物车空空如也" :image="empty" v-if="list.length == 0" />
    <!-- 中间列表 -->
    <div class="cart-list" v-else>
      <van-swipe-cell v-for="item in list" :key="item.goodsId">
        <div class="item">
          <van-checkbox checked-color="#1baeae"></van-checkbox>
          <GoodsItem :cart="true" :info="item" />
        </div>
        <template #right>
          <van-button square type="danger" text="删除" />
        </template>
      </van-swipe-cell>
    </div>

    <!-- 底部结算 -->
    <van-submit-bar :price="100" button-text="结算" button-type="primary">
      <van-checkbox>全选</van-checkbox>
    </van-submit-bar>
  </div>
</template>

<style lang="less" scoped>
.van-submit-bar {
  bottom: 60px;
}

.cart-box {
  padding: 0 10px;

  .cart-list {
    height: calc(100vh - 46px - 110px);
    overflow-y: auto;

    .item {
      display: flex;
      align-items: center;
      border-bottom: 1px solid #eee;

      .van-checkbox {
        flex: 0 0 auto;
        width: 20px;
      }
    }
  }
}

.van-button--square {
  height: 100%;
}

:deep(.van-swipe-cell__right) {
  right: -1px;
}
</style>
  • src/components/GoodsItem.vue
<template>
  <div :class="{ 'goods-item-box': true, 'goods-cart': cart }">
    <router-link :to="`/detail/${info.goodsId}`">
      <van-image :src="processURL(info.goodsCoverImg)" />
      <div class="desc" v-if="!cart">
        <h3 class="title">{{ info.goodsName }}</h3>
        <p class="info">商品介绍</p>
        <p class="price">{{ info.sellingPrice }}</p>
      </div>
      <div class="cart-desc" v-else>
        <h3 class="title">{{ info.goodsName }}</h3>
        <p class="info">
          <span>{{ info.sellingPrice }}</span>
          <van-stepper max="5" />
        </p>
      </div>
    </router-link>
  </div>
</template>
<script setup>
import { processURL } from '../utils'

defineProps(['cart', 'info'])
</script>

<style lang="less" scoped>
.clip {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.goods-item-box {
  padding: 10px;
  border-bottom: 1px solid #eee;

  a {
    display: flex;

    .van-image {
      margin-right: 10px;
      width: 120px;
      height: 120px;
      flex: 0 0 auto;
    }

    .desc {
      color: #555;
      flex: 1 0;

      .title {
        .clip;
        font-size: 14px;
        font-weight: normal;
        line-height: 20px;
        max-height: 40px;
      }

      .info {
        .clip;
        padding: 8px 0;
        color: #999;
        font-size: 12px;
        line-height: 16px;
        max-height: 32px;
      }

      .price {
        color: @theme;
        font-size: 14px;
      }
    }
  }

  /* 购物车独有 */
  &.goods-cart {
    border-bottom: none;

    a {
      .van-image {
        width: 100px;
        height: 100px;
      }

      .cart-desc {
        box-sizing: border-box;
        padding: 10px;
        min-width: 220px;
        color: #555;
        flex: 1 0;

        .title {
          .clip;
          font-size: 14px;
          font-weight: normal;
          line-height: 20px;
          max-height: 40px;
        }

        .info {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-top: 10px;
          line-height: 28px;

          span {
            font-size: 14px;
            color: #ee0a24;
          }
        }
      }
    }
  }
}
</style>

购物车页操作

  • src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
  const list = ref([])
  const hasCartStore = ref(false)
  // 在pinia中缓存数据
  async function getShopCartList() {
    list.value = await queryShopCart()
    hasCartStore.value = true
  }
  async function setShopCartItem(id, count) {
    await setCartCount(id, count)
    // 将列表中的数据也要更新,如果是修改也可以不发请求 通过id 获取到这一项,更改数量即可
    list.value.find((item) => item.cartItemId == id).goodsCount = count
  }
  async function removeShopCartItem(id) {
    await removeCart(id)
    // 将列表中的数据也要更新
  }
  async function addShopCartItem(id) {
    await addNewCart(id)
    // 将列表中的数据也要更新
    // 正常操作的情况下 应该是添加成功后,返回添加后的值
    getShopCartList()
  }

  const badge = computed(() => {
    // 暂时先计算出 我们当前选中的详情页的数量
    const current = list.value.reduce((memo, current) => ((memo += current.goodsCount), memo), 0)
    return current
  })

  return {
    badge,
    hasCartStore,
    list,
    getShopCartList,
    setShopCartItem,
    removeShopCartItem,
    addShopCartItem
  }
})
  • src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
import { processURL } from '../utils'
import { useCartStore } from '../stores/cart.js'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
  const id = route.params.id
  if (!id) {
    return router.push('/')
  }
  currentGoods.value = await queryGoodsInfo(id)
})
// 购物车store
const store = useCartStore()
const { list } = storeToRefs(store) // 购物车的数据

function findCurrentItem() {
  return list.value.find((item) => item.goodsId == route.params.id)
}

const { proxy } = getCurrentInstance()
function addCart() {
  const current = findCurrentItem()
  if (current) {
    // 修改数量
    if (current.goodsCount == 5) {
      return proxy.$showToast('超出最大购买限制')
    }
    store.setShopCartItem(current.cartItemId, current.goodsCount + 1)
  } else {
    // 新增
    store.addShopCartItem(route.params.id)
  }
}
</script>

<template>
  <div class="detail-box">
    <!-- 导航 -->
    <TopBar title="商品详情"></TopBar>
    <!-- 商品详情 -->
    <div class="info" v-if="currentGoods">
      <van-image lazy-load :src="processURL(currentGoods.goodsCoverImg)" />
      <div class="desc">
        <h3 class="title">{{ currentGoods.goodsName }}</h3>
        <p class="tag">{{ currentGoods.goodsIntro }}</p>
        <p class="price">¥{{ currentGoods.sellingPrice }}</p>
      </div>
      <div class="tab">
        <a href="javascript:;">概述</a>
        <span>|</span>
        <a href="javascript:;">参数</a>
        <span>|</span>
        <a href="javascript:;">安装服务</a>
        <span>|</span>
        <a href="javascript:;">常见问题</a>
      </div>
      <div class="content" v-html="currentGoods.goodsDetailContent"></div>
    </div>

    <!-- 相关操作 -->
    <van-action-bar style="max-width: 540px">
      <van-action-bar-icon icon="chat-o" text="客服" />
      <van-action-bar-icon
        icon="cart-o"
        text="购物车"
        @click="$router.push('/cart')"
        :badge="store.badge"
      />
      <van-action-bar-button
        color="linear-gradient(90deg,#6bd8d8,#1baeae)"
        text="加入购物车"
        @click="addCart"
      />
      <van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
    </van-action-bar>
  </div>
</template>

<script setup></script>

<style lang="less" scoped>
.van-skeleton {
  margin-top: 20px;
}

.detail-box {
  padding-bottom: 50px;
  .info {
    padding: 0 10px;
    .content {
      :deep(img) {
        max-width: 100%;
      }
    }
    .van-image {
      width: 100%;
      min-height: 240px;
    }

    .desc {
      .title {
        font-size: 18px;
        color: #555;
        font-weight: normal;
      }

      .tag {
        padding: 10px 0;
        font-size: 14px;
        color: #999;
      }
      .price {
        font-size: 18px;
        color: #f63515;
      }
    }
    .tab {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 15px 0;
      a,
      span {
        font-size: 15px;
        color: #555;
      }

      a {
        padding: 0 10px;
      }
    }
  }
}
</style>
  • src/views/Cart.vue
<script setup>
import empty from '@/assets/images/empty-car.png'
import { useCartStore } from '../stores/cart'
import { storeToRefs } from 'pinia'

const store = useCartStore()
const { list } = storeToRefs(store)
</script>
<template>
  <div class="cart-box">
    <!-- 头部导航 -->
    <TopBar title="购物车"></TopBar>
    <van-empty description="购物车空空如也" :image="empty" v-if="list.length == 0" />
    <!-- 中间列表 -->
    <div class="cart-list" v-else>
      <van-swipe-cell v-for="item in list" :key="item.goodsId">
        <div class="item">
          <van-checkbox checked-color="#1baeae"></van-checkbox>
          <GoodsItem :cart="true" :info="item" />
        </div>
        <template #right>
          <van-button square type="danger" text="删除" />
        </template>
      </van-swipe-cell>
    </div>

    <!-- 底部结算 -->
    <van-submit-bar :price="100" button-text="结算" button-type="primary">
      <van-checkbox>全选</van-checkbox>
    </van-submit-bar>
  </div>
</template>

<style lang="less" scoped>
.van-submit-bar {
  bottom: 60px;
}

.cart-box {
  padding: 0 10px;

  .cart-list {
    height: calc(100vh - 46px - 110px);
    overflow-y: auto;

    .item {
      display: flex;
      align-items: center;
      border-bottom: 1px solid #eee;

      .van-checkbox {
        flex: 0 0 auto;
        width: 20px;
      }
    }
  }
}

.van-button--square {
  height: 100%;
}

:deep(.van-swipe-cell__right) {
  right: -1px;
}
</style>
  • src/components/NavBar.vue
<template>
  <van-tabbar route v-if="props.visible">
    <van-tabbar-item to="/home">
      <template #icon>
        <SvgIcon icon-class="home"></SvgIcon>
      </template>
      首页</van-tabbar-item
    >
    <van-tabbar-item to="/category">
      <template #icon>
        <SvgIcon icon-class="category"></SvgIcon>
      </template>
      分类</van-tabbar-item
    >
    <van-tabbar-item to="/cart" :badge="store.badge">
      <template #icon>
        <SvgIcon icon-class="cart"></SvgIcon>
      </template>
      购物车</van-tabbar-item
    >
    <van-tabbar-item to="/user">
      <template #icon>
        <SvgIcon icon-class="user"></SvgIcon>
      </template>
      我的</van-tabbar-item
    >
  </van-tabbar>
</template>
<script setup>
import { useCartStore } from '@/stores/cart.js'
const store = useCartStore()
const route = useRoute()
// 父组件告诉我要不要渲染,我自己判断如果需要渲染,告诉父亲
const props = defineProps(['visible'])
const emit = defineEmits(['update:visible'])
watchEffect(() => {
  // 根据路由判断是否要渲染
  const paths = ['/home', '/category', '/cart', '/user']
  // 如果访问的是这几个页面
  emit('update:visible', paths.includes(route.path))
  // 通知父组件我的最新状态是多少
})
</script>
  • src/components/GoodsItem.vue
<template>
  <div :class="{ 'goods-item-box': true, 'goods-cart': cart }">
    <router-link :to="`/detail/${info.goodsId}`">
      <van-image :src="processURL(info.goodsCoverImg)" />
      <div class="desc" v-if="!cart">
        <h3 class="title">{{ info.goodsName }}</h3>
        <p class="info">商品介绍</p>
        <p class="price">{{ info.sellingPrice }}</p>
      </div>
      <div class="cart-desc" v-else>
        <h3 class="title">{{ info.goodsName }}</h3>
        <p class="info">
          <span>{{ info.sellingPrice }}</span>
          <van-stepper max="5" :modelValue="info.goodsCount" @change="handle" />
        </p>
      </div>
    </router-link>
  </div>
</template>
<script setup>
import { useCartStore } from '../stores/cart'
import { processURL } from '../utils'

const props = defineProps(['cart', 'info'])
const store = useCartStore()
function handle(value) {
  store.setShopCartItem(props.info.cartItemId, value)
}
</script>

<style lang="less" scoped>
.clip {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.goods-item-box {
  padding: 10px;
  border-bottom: 1px solid #eee;

  a {
    display: flex;

    .van-image {
      margin-right: 10px;
      width: 120px;
      height: 120px;
      flex: 0 0 auto;
    }

    .desc {
      color: #555;
      flex: 1 0;

      .title {
        .clip;
        font-size: 14px;
        font-weight: normal;
        line-height: 20px;
        max-height: 40px;
      }

      .info {
        .clip;
        padding: 8px 0;
        color: #999;
        font-size: 12px;
        line-height: 16px;
        max-height: 32px;
      }

      .price {
        color: @theme;
        font-size: 14px;
      }
    }
  }

  /* 购物车独有 */
  &.goods-cart {
    border-bottom: none;

    a {
      .van-image {
        width: 100px;
        height: 100px;
      }

      .cart-desc {
        box-sizing: border-box;
        padding: 10px;
        min-width: 220px;
        color: #555;
        flex: 1 0;

        .title {
          .clip;
          font-size: 14px;
          font-weight: normal;
          line-height: 20px;
          max-height: 40px;
        }

        .info {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-top: 10px;
          line-height: 28px;

          span {
            font-size: 14px;
            color: #ee0a24;
          }
        }
      }
    }
  }
}
</style>

购物车全选与删除操作

  • src/views/Cart.vue
<script setup>
// @ts-ignore
import empty from '@/assets/images/empty-car.png'
import { useCartStore } from '../stores/cart'
import { storeToRefs } from 'pinia'

// @ts-ignore
const store = useCartStore()
const { list } = storeToRefs(store)

function remove(id) {
  store.removeShopCartItem(id)
}
// @ts-ignore
</script>
<template>
  <div class="cart-box">
    <!-- 头部导航 -->
    <TopBar title="购物车"></TopBar>
    <van-empty description="购物车空空如也" :image="empty" v-if="list.length == 0" />
    <!-- 中间列表 -->
    <div class="cart-list" v-else>
      <van-swipe-cell v-for="item in list" :key="item.goodsId">
        <div class="item">
          <van-checkbox checked-color="#1baeae" v-model="item.checked"></van-checkbox>
          <GoodsItem :cart="true" :info="item" />
        </div>
        <template #right>
          <van-button square type="danger" text="删除" @click="remove(item.cartItemId)" />
        </template>
      </van-swipe-cell>
    </div>

    <!-- 底部结算 -->
    <van-submit-bar :price="store.total * 100" button-text="结算" button-type="primary">
      <van-checkbox v-model="store.isCheckedAll">全选</van-checkbox>
    </van-submit-bar>
  </div>
</template>

<style lang="less" scoped>
.van-submit-bar {
  bottom: 60px;
}

.cart-box {
  padding: 0 10px;

  .cart-list {
    height: calc(100vh - 46px - 110px);
    overflow-y: auto;

    .item {
      display: flex;
      align-items: center;
      border-bottom: 1px solid #eee;

      .van-checkbox {
        flex: 0 0 auto;
        width: 20px;
      }
    }
  }
}

.van-button--square {
  height: 100%;
}

:deep(.van-swipe-cell__right) {
  right: -1px;
}
</style>
  • src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
// @ts-ignore
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
  const list = ref([])
  const hasCartStore = ref(false)
  // 在pinia中缓存数据
  async function getShopCartList() {
    list.value = await queryShopCart()
    hasCartStore.value = true
  }
  async function setShopCartItem(id, count) {
    await setCartCount(id, count)
    // 将列表中的数据也要更新,如果是修改也可以不发请求 通过id 获取到这一项,更改数量即可
    list.value.find((item) => item.cartItemId == id).goodsCount = count
  }
  async function removeShopCartItem(id) {
    await removeCart(id)
    // 将列表中的数据也要更新
    list.value = list.value.filter((item) => item.cartItemId != id)
  }
  async function addShopCartItem(id) {
    await addNewCart(id)
    // 将列表中的数据也要更新
    // 正常操作的情况下 应该是添加成功后,返回添加后的值
    getShopCartList()
  }

  const badge = computed(() => {
    // 暂时先计算出 我们当前选中的详情页的数量
    const current = list.value.reduce((memo, current) => ((memo += current.goodsCount), memo), 0)
    return current
  })

  const total = computed(() => {
    return list.value.reduce((memo, current) => {
      if (current.checked) {
        // 选中就累加
        memo += current.goodsCount * current.sellingPrice
      }
      return memo
    }, 0)
  })
  const isCheckedAll = computed({
    get() {
      return list.value.every((item) => item.checked)
    },
    set(newVal) {
      list.value.forEach((item) => (item.checked = newVal))
    }
  })
  return {
    total,
    isCheckedAll,
    badge,
    hasCartStore,
    list,
    getShopCartList,
    setShopCartItem,
    removeShopCartItem,
    addShopCartItem
  }
})

搜索操作

  • src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import { useCartStore } from '@/stores/cart'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      redirect: '/home'
    },
    {
      path: '/home',
      name: 'home',
      component: Home,
      meta: { title: '首页' }
    },
    {
      path: '/category',
      meta: { title: '分类' },
      name: 'category',
      component: () => import('../views/Category.vue')
    },
    {
      path: '/cart',
      name: 'cart',
      component: () => import('../views/Cart.vue'),
      meta: {
        needLogin: true // 需要登录才能访问的 我们需要配置这个属性
      }
    },
    {
      path: '/user',
      name: 'user',
      component: () => import('../views/User.vue')
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('../views/Login.vue')
    },
    {
      path: '/detail/:id',
      name: 'detail',
      meta: {
        needLogin: true
      },
      component: () => import('../views/Detail.vue')
    },
    {
      path: '/search',
      name: 'search',
      meta: {
        needLogin: true
      },
      component: () => import('../views/Search.vue')
    }
  ]
})

// 鉴权看是否登录过
router.beforeEach(async (to) => {
  let token = localStorage.getItem('token')
  if (token) {
    // 如果有token 说明登录成功,但是如果你访问的还是登录
    if (to.name === 'login') {
      return { path: '/' }
    }
  } else {
    // 没有登录,跳转到登录页面
    // /a/b  -> ['/a','/a/b']
    if (to.matched.some((item) => item.meta.needLogin)) {
      // 此路由需要登录但是没有登录, 应该跳转到登录页面
      return {
        path: '/login',
        query: {
          redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来
          ...to.query // 当前页面的其他参数也添加进去
        }
      }
    }
  }
})
// 如果登陆过查询是否已经获取购物车信息
// 获取购物车的信息存储到pinia中
router.beforeEach(() => {
  let token = localStorage.getItem('token')
  if (token) {
    const store = useCartStore()
    if (!store.hasCartStore) {
      // 如果当前没有获取过购物车信息
      store.getShopCartList()
    }
  }
})
export default router
  • src/stores/category.js
import { queryCategories } from '@/api'
export const useCategoryStore = defineStore('category', () => {
  const list = ref([]) // 默认所有分类的信息
  async function getCategories() {
    // 获取分类将获取的数据保存到list变量中
    list.value = await queryCategories()
  }
  return { list, getCategories }
})
  • src/views/Search.vue
<script setup>
import { onMounted } from 'vue'
import { useCategoryStore } from '../stores/category'
const route = useRoute()
const goodsCategoryId = ref(parseInt(route.query.categoryId) || '')
const categoryOptions = ref([{ text: '全部分类', value: '' }])
const orderBy = ref('')
const sortOptions = [
  { text: '综合排序', value: '' },
  { text: '时间排序', value: 'new' },
  { text: '价格排序', value: 'price' }
]

const store = useCategoryStore()
const { list } = storeToRefs(store)
onMounted(async () => {
  if (list.value.length == 0) {
    await store.getCategories()
  }
  console.log(`list.value-->`, list.value)

  const result = []
  const stack = [...toRaw(list.value)] // 所以的数据
  console.log(`stack-->`, JSON.parse(JSON.stringify(stack)))

  // 树的遍历, 层序遍历
  let index = 0 // 指针
  let current
  // 递归会出现爆栈的问题,避免用递归
  while ((current = stack[index++]) != null) {
    if (current.categoryLevel == 1) {
      stack.push(...current.secondLevelCategoryVOS)
    } else if (current.categoryLevel == 2) {
      stack.push(...current.thirdLevelCategoryVOS)
    } else {
      result.push({ text: current.categoryName, value: current.categoryId })
    }
  }
  categoryOptions.value.push(...result)
})
</script>

<template>
  <div class="search-box">
    <!-- 头部导航 -->
    <TopBar>
      <template #title>
        <van-search />
      </template>
      <template #right>
        <van-button type="primary" size="small"> 搜索 </van-button>
      </template>
    </TopBar>
    <!-- 筛选 -->
    <van-dropdown-menu>
      <van-dropdown-item :options="categoryOptions" v-model="goodsCategoryId" />
      <van-dropdown-item :options="sortOptions" v-model="orderBy" />
    </van-dropdown-menu>
    <!-- 内容 -->
    <div class="content">
      <van-empty description="赶快搜索你想要的产品吧" />
      <van-list>
        <!-- <GoodsItem /> -->
      </van-list>
    </div>
  </div>
</template>

<style scoped>
.content {
  height: calc(100vh - 94px);
  overflow: scroll;
}
</style>

数组数据转化-扁平化

层序遍历
const stack = [
  {
    categoryId: 15,
    categoryLevel: 1,
    categoryName: '家电 数码 手机',
    secondLevelCategoryVOS: [
      {
        categoryId: 17,
        parentId: 15,
        categoryLevel: 2,
        categoryName: '家电',
        thirdLevelCategoryVOS: [
          {
            categoryId: 20,
            categoryLevel: 3,
            categoryName: '生活电器'
          },
          {
            categoryId: 110,
            categoryLevel: 3,
            categoryName: 'wer'
          },
          {
            categoryId: 21,
            categoryLevel: 3,
            categoryName: '厨房电器'
          },
          {
            categoryId: 22,
            categoryLevel: 3,
            categoryName: '扫地机器人'
          },
          {
            categoryId: 31,
            categoryLevel: 3,
            categoryName: '空气净化器'
          }
        ]
      },
      {
        categoryId: 19,
        parentId: 15,
        categoryLevel: 2,
        categoryName: '手机',
        thirdLevelCategoryVOS: [
          {
            categoryId: 45,
            categoryLevel: 3,
            categoryName: '荣耀手机'
          },
          {
            categoryId: 57,
            categoryLevel: 3,
            categoryName: 'vivo'
          },
          {
            categoryId: 58,
            categoryLevel: 3,
            categoryName: '手机以旧换新'
          }
        ]
      }
    ]
  },
  {
    categoryId: 16,
    categoryLevel: 1,
    categoryName: '女装 男装 穿搭',
    secondLevelCategoryVOS: [
      {
        categoryId: 67,
        parentId: 16,
        categoryLevel: 2,
        categoryName: '女装',
        thirdLevelCategoryVOS: [
          {
            categoryId: 76,
            categoryLevel: 3,
            categoryName: '外套'
          }
        ]
      }
    ]
  },
  {
    categoryId: 61,
    categoryLevel: 1,
    categoryName: '家具 家饰 家纺',
    secondLevelCategoryVOS: [
      {
        categoryId: 70,
        parentId: 61,
        categoryLevel: 2,
        categoryName: '家具',
        thirdLevelCategoryVOS: [
          {
            categoryId: 77,
            categoryLevel: 3,
            categoryName: '沙发'
          }
        ]
      }
    ]
  }
]
const result = []
console.log(`0. stack-->`, JSON.parse(JSON.stringify(stack)))

// 树的遍历, 层序遍历
let index = 0 // 指针
let current
// 递归会出现爆栈的问题,避免用递归
while ((current = stack[index++]) != null) {
  if (current.categoryLevel == 1) {
    stack.push(...current.secondLevelCategoryVOS)
    console.log(`1. stack-->`, JSON.parse(JSON.stringify(stack)))
  } else if (current.categoryLevel == 2) {
    stack.push(...current.thirdLevelCategoryVOS)
    console.log(`2. stack-->`, JSON.parse(JSON.stringify(stack)))
  } else {
    result.push({ text: current.categoryName, value: current.categoryId })
    console.log(`3. stack-->`, JSON.parse(JSON.stringify(stack)))
  }
}
console.log(`result-->`, result)
<script setup>
import { onMounted } from 'vue'
import { useCategoryStore } from '../stores/category'
const route = useRoute()
const goodsCategoryId = ref(parseInt(route.query.categoryId) || '')
const categoryOptions = ref([{ text: '全部分类', value: '' }])
const orderBy = ref('')
const sortOptions = [
  { text: '综合排序', value: '' },
  { text: '时间排序', value: 'new' },
  { text: '价格排序', value: 'price' }
]

const store = useCategoryStore()
const { list } = storeToRefs(store)
onMounted(async () => {
  if (list.value.length == 0) {
    await store.getCategories()
  }
  console.log(`list.value-->`, list.value)

  const result = []
  const stack = [...toRaw(list.value)] // 所以的数据
  console.log(`stack-->`, JSON.parse(JSON.stringify(stack)))

  // 树的遍历, 层序遍历
  let index = 0 // 指针
  let current
  // 递归会出现爆栈的问题,避免用递归
  while ((current = stack[index++]) != null) {
    if (current.categoryLevel == 1) {
      stack.push(...current.secondLevelCategoryVOS)
    } else if (current.categoryLevel == 2) {
      stack.push(...current.thirdLevelCategoryVOS)
    } else {
      result.push({ text: current.categoryName, value: current.categoryId })
    }
  }
  categoryOptions.value.push(...result)
})
</script>

进阶参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值