Vue2-智慧商城移动端项目

vue2

智慧商城项目

项目流程

image


项目收获

image


步骤

创建项目

基于VueCli自定义项目架子

image

当进入创建的项目目录中,运行项目可以在网页看见默认布局时表示项目创建成功


调整初始化目录

将目录调整成符合企业规范的目录

  1. 删除多余的文件:

    • assets下的logo.png

    • components下的HelloWorld.vue

    • views下的vue文件

  2. 修改路由配置和App.vue

    • 清空路由规则:

      import Vue from 'vue'
      import VueRouter from 'vue-router'
      
      Vue.use(VueRouter)
      
      const routes = [
      
      ]
      
      const router = new VueRouter({
        routes
      })
      
      export default router
      
    • 删除App.vue中的样式及nav元素

  3. 新增两个目录:

    • api:发送ajax请求的接口模块

    • utils:自己封装的一些工具方法模块


vant-ui组件库

组件库:第三方封装好了很多的组件,整合在一起就是一个组件库

常见组件库:


vant2安装:npm i vant@latest-v2 -S

引入组件:

  • 按需导入(自动/手动):性能高(上线时用户加载速度快),但需要多次引入

    • 安装插件:npm i babel-plugin-import -D D表示仅在开发过程使用

      (如果安装失败提示版本冲突可以使用--force强制安装)

    • babel.config.js 中配置:

      module.exports = {
        plugins: [
          ['import', {
            libraryName: 'vant',
            libraryDirectory: 'es',
            style: true
          }, 'vant']
        ]
      };
      
    • main.js中按需导入并注册:

      //按需导入
      import { Button,Switch } from 'vant';
      Vue.use(Button)
      Vue.use(Switch)
      

    如果需要导入的组件越来越多,那么main.js中的内容会越来越多,为了便于管理,将导入组件的代码放在utils>vant-ui.js 并且只需在main.js中导入import @/utils/vant-ui 即可

    测试使用:

    <van-button type="primary">主要按钮</van-button>
    <van-button type="info">信息按钮</van-button>
    <van-button type="default">默认按钮</van-button>
    <van-button type="warning">警告按钮</van-button>
    <van-button type="danger">危险按钮</van-button>
    

  • 全部导入:方便但是会增加项目体积

    //在main.js中注册
    import Vue from 'vue';
    import Vant from 'vant';
    import 'vant/lib/index.css'; //导入所有组件
    
    Vue.use(Vant); //插件安装初始化
    
    <!-- 使用测试 -->
    <van-button type="primary">主要按钮</van-button>
    <van-button type="info">信息按钮</van-button>
    <van-button type="default">默认按钮</van-button>
    <van-button type="warning">警告按钮</van-button>
    <van-button type="danger">危险按钮</van-button>
    

image


项目中的vw(viewport)适配

基于postcss插件实现项目vw适配,可以实现px->vw的自动转换

  1. postcss-px-to-viewport插件安装:npm i postcss-px-to-viewport --force

  2. 在根目录下新建postcss.config.js

    // postcss.config.js
    module.exports = {
      plugins: {
        'postcss-px-to-viewport': {
          viewportWidth: 375, //vw适配的标准屏宽度(iPhoneX)
        },
      },
    };
    

    image

    300/375=0.8

    0.8*100vw=80vw


路由设计配置

只要是单个页面,独立展示的都是一级路由

image

推荐将每个一级路由模块新建为一个文件夹,并在每个文件夹中新建index.vue 页面


配置路由规则:

import Login from '@/views/login'
import Layout from '@/views/layout'
import Myorder from '@/views/myorder'
import Pay from '@/views/pay'
import Prodetail from '@/views/prodetail'
import Search from '@/views/search/index.vue'
import SearchList from '@/views/search/list.vue'

const routes = [
  { path: '/login', component: Login },
  { path: '/', component: Layout },
  { path: '/myorder', component: Myorder },
  { path: '/pay', component: Pay },
  // 动态路由传参 确认是哪个商品
  { path: '/prodetail/:id', component: Prodetail },
  { path: '/search', component: Search },
  { path: '/searchlist', component: SearchList }
]

首页底部导航配置:

基于vant组件库,实现底部导航tabbar

  1. 按需导入

    import { Tabbar, TabbarItem } from 'vant'
    import Vue from 'vue'
    
    Vue.use(Tabbar)
    Vue.use(TabbarItem)
    
  2. layout.vue中使用,并将组件中绑定的变量先删除

  3. 修改原本组件中的文字内容及图标

  4. 按需导入图标

image


二级路由配置:

  1. 在layout文件夹下新建二级路由对应的组件

  2. 配置导航规则

    {
        path: '/',
        component: Layout,
        children: [
          { path: '/home', component: Home },
          { path: '/category', component: Category },
          { path: '/cart', component: Cart },
          { path: '/user', component: User }
        ]
      }
    
  3. 配置导航链接

    <van-tabbar active-color="#ee0a24" inactive-color="#000" route>
            <van-tabbar-item replace to="/home" icon="wap-home-o">首页</van-tabbar-item>
            <van-tabbar-item replace to="/category" icon="apps-o" >分类页</van-tabbar-item>
            <van-tabbar-item replace to="/cart" icon="shopping-cart-o">购物车</van-tabbar-item>
            <van-tabbar-item replace to="/user" icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
    

  4. 配置导航出口

    <router-view />
    
    
  5. 首页的路由/添加重定向:redirect: '/home',

image


登录页的静态布局

  • 头部组件:NavBar 按需导入使用

  • 其他

    • 新建style>common.less重置默认样式,并让文本在两行显示多余省略

      // 重置默认样式
      *{
          margin: 0;
          padding: 0;
          box-sizing: border-box;
      }
      
      //让文本在显示两行后溢出部分以省略号显示
      .text-ellipsis-2{
          overflow: hidden;              // 隐藏超出元素框的内容
          -webkit-line-clamp: 2;         // 指定文本最多显示 2 行(WebKit 内核浏览器)
          text-overflow: ellipsis;       // 文本溢出时显示省略号
          display: -webkit-box;          // 将元素作为弹性盒容器显示(WebKit 内核浏览器)
          -webkit-box-orient: vertical;  // 指定弹性盒内子元素垂直排列(WebKit 内核浏览器)
      }
      
      //导航栏通用样式覆盖
      .van-nav-bar{
          .van-nav-bar__arrow{
              color: black;
          }
      }
      
    • 将common.less导入main.js中:import '@/style/common.less'

    • 编写登录页样式

      <template>
          <div class="login">
            <van-nav-bar
              title="会员登录"
              left-text=" "
              left-arrow
               @click-left="$router.go(-1)"
            />
            <!-- 静态结构 -->
             <div class="container">
              <div class="title">
                <h3>手机号登录</h3>
                <p>未注册的手机号登录后将自动注册</p>
              </div>
              <div class="form">
                <van-cell-group>
                  <van-field  v-model="tel" type="tel"  placeholder="请输入手机号码" />
                  <van-field v-model="number" type="number" placeholder="请输入图形验证码" />
                  <div class="code">
                    <van-field v-model="number" type="number" placeholder="请输入短信验证码" />
                    <span>获取验证码</span>
                  </div>
                  <van-field v-model="number" type="number"  />
                </van-cell-group>
                <van-button round type="info">登录</van-button>
              </div>
             </div>
          </div>
      </template>
      
      <script>
      export default {
        name: 'LoginIndex'
      }
      </script>
      
      <style scoped>
      .login{
        .container{
          margin: 0 auto;
          .title{
            margin-left: 20px;
            margin-top: 60px;
            margin-bottom: 30px;
            p{
              color: gainsboro;
              font-size: 12px;
              margin-top: 10px;
            }
            h3{
              font-size: 23px;
              font-weight: normal;
            }
          }
          .form{
            margin-left: 10px;
            text-align: center;
            .code{
              display: flex;
              span{
                display: block;
                color: rgb(255, 154, 19);
                font-size: 12px;
                margin-top: 10px;
                width: 160px;
              }
            }
          }
        }
        .van-button{
          margin-top: 40px;
          width: 80%;
          background-color: rgb(255, 154, 19);
          border: none;
        }
      }
      </style>
      

      image


登录页的数据请求

通常会将axios请求方法(配置基地址,请求响应拦截器等)封装到request模块中

所有项目开发中,都会对axios进行基本的二次封装,单独封装到一个request模块中,便于维护使用

接口文档:wiki - 智慧商城-实战项目

步骤:

  1. 安装axios:npm i axios

  2. 新建request模块:在utils>request.js 中配置axios

  3. 创建实例/配置,导出实例

  4. 测试使用

// 创建axios实例,将来对创建的实例进行自定义配置,好处:不会污染原始的axios实例
import axios from 'axios'

const instance = axios.create({
  baseURL: 'http://smart-shop.itheima.net/index.php?s=/api',
  timeout: 5000
})

// 自定义配置-请求/响应拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么(默认axios会多包装一层data)
  return response.data
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

// 导出配置好的实例
export default instance
<script>
import request from '@/utils/request'
export default {
  name: 'LoginIndex',
  async created () {
    const res = await request.get('/captcha/image')
    console.log(res) // 测试封装的axios
  }
}
</script>

image


图形验证码功能

基于获取的请求中的base64图片实现图形验证码的功能

图形验证码用于强制人机交互,可以抵御机器自动化攻击 (避免批量请求获取短信)

需求:

  • 动态将请求回来的base64图片解析渲染出来

  • 点击验证码图片盒子,可以实现刷新验证码

image

其中base64是验证码图片,key是该验证码图片的唯一标识,后台基于唯一标识key去找到验证码的结果与发送的进行匹配

<div class="pic">
   <van-field v-model="picCode" type="number" placeholder="请输入图形验证码" />
   <img v-if="picUrl" :src="picUrl" alt="" @click="getPicCode">
</div>

<script>
import request from '@/utils/request'
export default {
  name: 'LoginIndex',
  async created () {
    this.getPicCode()
  },
  data () {
    return {
      picCode: '', // 用户输入的图形验证码
      picKey: '', // 图形验证码的唯一标识
      picUrl: '' // 图形验证码的url地址
    }
  },
  methods: {
    async getPicCode () {
      const { data: { base64, key } } = await request.get('/captcha/image')
      this.picUrl = base64
      this.picKey = key
    }
  }
}
</script>

image


封装图片验证码接口

请求封装成方法,统一存放到api模块(存放封装好的请求函数)

好处:

  • 请求与页面逻辑分离

  • 相同的请求可以直接复用

  • 请求进行了统一管理


步骤:

  1. 新建请求模块

    在api目录下新建login.js ,用于存放所有登录相关的接口请求

  2. 封装请求函数

    // 获取图形验证码
    export const getPicCode = () => {
      return request.get('/captcha/image')
    }
    
  3. 页面中导入调用

      async created () {
        this.getPicCode()
      },
      data () {
        return {
          picCode: '', // 用户输入的图形验证码
          picKey: '', // 图形验证码的唯一标识
          picUrl: '' // 图形验证码的url地址
        }
      },
      methods: {
        async getPicCode () {
          const { data: { base64, key } } = await getPicCode()
          this.picUrl = base64
          this.picKey = key
        }
      }
    }
    

Toast轻提示

基于vant文档,完成Toast请提示

步骤:

  1. 注册安装:

    import { Toast } from 'vant';
    
    Vue.use(Toast);
    
  2. 导入调用:

    • 组件内/非组件内均可:

      import { Toast } from 'vant'
      Toast('提示内容')
      
    • 通过this直接调用(必须在组件内):本质:将方法注册挂载到了Vue原型上Vue.prototype.$toast=xxx

      this.$toast('提示内容')
      

短信倒计时模块(节流)

默认是:246810

步骤:

  1. 点击按钮,实现倒计时效果(往后台发送请求才开始倒计时),在离开页面后要清除定时器

     data () {
        return {
          picCode: '', // 用户输入的图形验证码
          picKey: '', // 图形验证码的唯一标识
          picUrl: '', // 图形验证码的url地址
          totalSecond: 60, // 倒计时总秒数
          second: 60, // 当前秒数
          timer: null // 定时器id
        }
      },
      methods: {
        async getPicCode () {
          const { data: { base64, key } } = await getPicCode()
          this.picUrl = base64
          this.picKey = key
        },
        // 获取短信验证码
        getCode () {
          // 当目前不存在倒计时,且当前秒数为总秒数时(归位),开启倒计时
          if (!this.timer && this.second === this.totalSecond) {
            // 开启定时器
            this.timer = setInterval(() => {
              this.second--
            }, 1000)
            if (this.second === 0) {
              clearInterval(this.timer)
              this.timer = null
              this.second = this.totalSecond
            }
          }
        },
          // 离开页面清除定时器
        destroyed () {
          clearInterval(this.timer)
        }
    

  2. 倒计时之前的校验处理

    • 手机号

    • 验证码

    // 校验手机号和验证码是否合法
        checkPhoneAndCode () {
          if (!/^1[3-9]\d{9}$/.test(this.tel)) {
            this.$toast('请输入正确的手机号')
            return false
          }
          if (!/^\w{4}$/.test(this.picCode)) {
            this.$toast('请输入正确的图形验证码')
            return false
          }
          return true
        }
    

  3. 封装短信验证请求接口,发送请求添加提示

    // 获取短信验证码
    export const getSmsCode = (captchaCode, captchaKey, mobile) => {
      return request.post('/captcha/sendSmsCaptcha', {
        form: {
          captchaCode,
          captchaKey,
          mobile
        }
      })
    }
    
    // 获取短信验证码
        async getCode () {
          if (this.checkPhoneAndCode()) {
            // 发送请求
            await getSmsCode(this.picCode, this.picKey, this.tel)
            this.$toast('短信验证码发送成功')
    
            // 当目前不存在倒计时,且当前秒数为总秒数时(归位),开启倒计时
            if (!this.timer && this.second === this.totalSecond) {
            // 开启定时器
              this.timer = setInterval(() => {
                this.second--
              }, 1000)
              if (this.second === 0) {
                clearInterval(this.timer)
                this.timer = null
                this.second = this.totalSecond
              }
            }
          }
        }
    

登录功能

封装api登录接口,实现登录功能

步骤:

  • 阅读接口文档,封装登录接口

    // 登录接口
    export const login = (mobile, smsCode) => {
      return request.post('/passport/login', {
        form: {
          isParty: false,
          mobile,
          partyData: {},
          smsCode
        }
      })
    }
    

  • 登录前校验:

    • 手机号

    • 图形验证码

    • 短信验证码

  • 调用方法,发送请求,成功添加提示并跳转

 // 登录
async login () {
  // 重新校验手机号和验证码是否合法,防止在发送短信后修改
  if (!this.checkPhoneAndCode()) return
  // 验证短信验证码
  if (!/^\d{6}$/.test(this.smsNumber)) {
    this.$toast('请输入正确的短信验证码')
    return false
  }
  await loginCode(this.tel, this.picCode)
  this.$toast('登录成功')
  this.$router.push('/')
}

只需关注成功的情况,失败有响应拦截器


相应拦截器统一处理错误提示

上述代码只关注成功的情况,但是每次请求都有可能有错误,需要错误提示,可在相应拦截器进行统一的错误处理

相应拦截器是第一个数据流转站

当发送请求响应的status非200,抛出一个promise错误,await只会等待成功的promise

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么(默认axios会多包装一层data)
  const res = response.data
  if (res.status !== 200) {
    // 给提示
    Toast(res.message)
    // 抛出错误的promise
    return Promise.reject(res.message)
  }
  return res
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

登录权证信息存储

基于vuex构建user 模块存储登录权证:

  • token

  • userId


步骤:

  1. 构建user模块

    export default {
      namespaced: true,
      state () {
        return {
          userInfo: {
            token: '',
            userId: ''
          }
        }
      },
      mutations: {},
      actions: {},
      getters: {}
    }
    
    

  2. 挂载到vuex

    import user from './modules/user'
    
    modules: {
        user
      }
    

  3. 提供mutations

    mutations: {
        setUserInfo (state, userInfo) {
          state.userInfo = userInfo
        }
      },
    

  4. 页面中commit调用

    this.$store.commit('user/setUserInfo', res.data)
    

vuex的持久化处理

利用本地存储实现vuex的持久化处理

utils>storage.js中封装相关方法

// 约定一个通用的键名
const INFO_KEY = 'SHOP_INFO'

// 获取个人信息
export const getInfo = () => {
  const defaultInfo = {
    token: '',
    userId: ''
  }
  const result = localStorage.getItem(INFO_KEY)
  return result ? JSON.parse(result) : defaultInfo
}

// 修改个人信息
export const setInfo = (obj) => {
  localStorage.setItem(INFO_KEY, JSON.stringify(obj))
}

// 删除个人信息
export const removeInfo = () => {
  localStorage.removeItem(INFO_KEY)
}

storage>utils>user.js中使用方法存入localStorage中

import { setInfo, getInfo } from '@/utils/storage'
export default {
  namespaced: true,
  state () {
    return {
      userInfo: getInfo()
    }
  },
  mutations: {
    setUserInfo (state, userInfo) {
      state.userInfo = userInfo
      setInfo(userInfo)
    }
  },
  actions: {},
  getters: {}
}

image


添加请求loading效果

统一在每次请求后台时,添加loading效果

有时因为网络原因一次请求的结果可能需要一段时间后才能回来,此时需要给用户添加loading提示

好处:

  • 节流处理:防止用户在一次请求还没回来之前多次进行点击,发送无效的请求

  • 友好提示:告知用户,目前在加载中,请耐心等待,增加用户体验感


步骤:

  1. 在请求拦截器中:每次请求打开loading,禁止背景点击

    (基于vant-ui)

    instance.interceptors.request.use(function (config) {
      // 在发送请求之前做些什么
      Toast.loading({
        message: '加载中...',
        forbidClick: true,
        duration: 0 // 表示一直加载请求回来之后才能关闭
      })
      return config
    }, function (error) {
      // 对请求错误做些什么
      return Promise.reject(error)
    })
    

  2. 在响应拦截器中:每次响应关闭loading

    instance.interceptors.response.use(function (response) {
      // 2xx 范围内的状态码都会触发该函数。
      // 对响应数据做点什么(默认axios会多包装一层data)
      const res = response.data
      // if (res.status !== 200) {
      //   // 给提示
      //   Toast(res.message)
      //   // 抛出错误的promise
      //   return Promise.reject(res.message)
      // }
      Toast.clear()
      return res
    }, function (error) {
      // 超出 2xx 范围的状态码都会触发该函数。
      // 对响应错误做点什么
      return Promise.reject(error)
    })
    

Toast默认是单例模式,后面的Toast调用了,会将前一个Toast效果覆盖(同时只能存在一个toast)

image


页面访问拦截

基于全局前置导航守卫,进行页面访问拦截处理

对于大部分的页面游客可以直接访问,如果遇到需要登录(支付页、订单页等)才能进行的操作,提示并跳转到登录页,需要做拦截处理

  • 所有的路由一旦被匹配到,都会被经过全局前置守卫

  • 只有全局前置守卫通过,才会真正解析渲染组件,才能看见页面内容

官网参考文档:导航守卫 | Vue Router

当一个导航触发时,全局前置守卫按照创建顺序调用

语法:

router.beforeEach((to,from,next) =>{
  // 1. to 往哪去,到哪去的路由信息对象
  // 2. from 往哪来,从哪来的路由信息对象
  // 3. next()是否放行
  // 如果next()调用就是放行;如果next(路径)拦截到某个路径页面
})

访问权限页面时,拦截或放行的关键点:用户是否具有token

image

// 全局前置路由守卫
router.beforeEach((to, from, next) => {
  if (!authPages.includes(to.path)) {
    next() // 非权限页面,直接放行
    return
  }
  // 权限页面,需要判断token
  const token = store.getters['user/token']
  if (token) {
    next() // 有token,放行
    // return
  } else {
    next('/login')
  }
})

首页

步骤:

  1. 完成首页的静态布局

    <!--home.vue-->
    <template>
        <div class="home">
            <van-nav-bar class="custom"
            title="智慧商城"
            />
            <van-search
              v-model="value"
              shape="round"
              placeholder="请输入搜索关键词"
            />
            <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
              <van-swipe-item><img src="@/assets/1.jpg" alt=""></van-swipe-item>
              <van-swipe-item><img src="@/assets/2.png" alt=""></van-swipe-item>
              <van-swipe-item><img src="@/assets/3.png" alt=""></van-swipe-item>
            </van-swipe>
            <van-grid :column-num="5">
              <van-grid-item v-for="value in 10" :key="value" icon="photo-o" text="新品首发" />
            </van-grid>
            <div class="pic">
              <a href=""><img src="@/assets/banner.png" alt=""></a>
            </div>
            <p>——猜你喜欢——</p>
            <div class="list">
              <GoodsItem v-for="item in 10" :key="item"></GoodsItem>
            </div>
        </div>
    </template>
    
    <script>
    import 'vant/lib/index.less'
    import GoodsItem from '@/components/GoodsItem.vue'
    export default {
      name: 'HomePage',
      components: {
        GoodsItem
      }
    }
    </script>
    
    <style scoped lang="less">
      *{
          margin: 0;
          padding: 0;
        }
       .van-nav-bar{
          background-color: rgb(207, 35, 0);
          /deep/ .van-nav-bar__title {
            color: white;
            font-weight: 700;
          }
        }
       .my-swipe img{
        width: 100vw;
        height: 30vh;
      }
      .pic {
        margin-bottom: 20px;
        img{
        width: 100vw;
      }
      }
      p{
        text-align: center;
      }
      .list{
        margin-top: 20px;
      }
    </style>
    
    <!--GoodsItem.vue-->
    <template>
      <div class="goods-item" @click="$router.push('/prodetail')">
        <div class="left">
          <img src="@/assets/4.jpg" alt="">
        </div>
        <div class="right">
          <p>小熊玩偶</p>
          <p class="count">已售104件</p>
            <span class="new">¥3999.00</span>
            <span class="old">¥6699.00</span>
        </div>
      </div>
    </template>
    
    <style scoped>
        .goods-item{
            display: flex;
            height: 148px;
            width: 100vw;
            .left{
                margin-right: 10px;
                margin-left: 10px;
                img{
                    width: 120px;
                    height: 120px;
                }
            }
            .right{
                .count{
                   color: #999;
                   font-size: 12px;
                   margin-bottom: 10px;
                   margin-top: 10px;
                }
                .new{
                   color: red;
                   font-size: 16px;
                   margin-right: 10px;
               }
               .old{
                   color: #999;
                   font-size: 16px;
                   text-decoration: line-through;
               }
            }
        }
    </style>
    

    image


  1. 封装接口:

    import request from '@/utils/request'
    
    // 获取首页数据
    export const getHomeData = () => {
      return request.get('/page/detail', {
        params: {
          pageId: 0
        }
      })
    }
    
  2. 页面调用:

    <!--GoodsItem.vue-->
    <template>
      <div v-if="item.goods_id" class="goods-item" @click="$router.push(`/prodetail/${item.goods_id}`)">
        <div class="left">
          <img :src="item.goods_image" alt="">
        </div>
        <div class="right">
          <p class="text-ellipsis-2">{{ item.goods_name }}</p>
          <p class="count">已售{{item.goods_sales}}件</p>
          <span class="new">¥{{item.goods_price_min}}</span>
          <span class="old">¥{{item.goods_price_max}}</span>
        </div>
      </div>
    </template>
    
    <style scoped>
    .goods-item {
      display: flex;
      height: 148px;
      width: 100vw;
    
      .left {
        margin-right: 10px;
        margin-left: 10px;
    
        img {
          width: 120px;
          height: 120px;
        }
      }
    
      .right {
        .count {
          color: #999;
          font-size: 12px;
          margin-bottom: 10px;
          margin-top: 10px;
        }
    
        .new {
          color: red;
          font-size: 16px;
          margin-right: 10px;
        }
    
        .old {
          color: #999;
          font-size: 16px;
          text-decoration: line-through;
        }
      }
    }
    </style>
    
    <script>
    export default {
      name: 'GoodsItem',
      props: {
        item: {
          type: Object,
          default: () => {
            return {}
          }
        }
      }
    }
    
    </script>
    
    <!--home.vue-->
    <template>
        <div class="home">
            <van-nav-bar class="custom"
            title="智慧商城"
            />
            <van-search
              v-model="value"
              shape="round"
              placeholder="请输入搜索关键词"
            />
            <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
              <van-swipe-item v-for="item in bannerList" :key="item.imgUrl">
                <img :src="item.imgUrl" alt="">
              </van-swipe-item>
            </van-swipe>
            <van-grid :column-num="5">
              <van-grid-item v-for="value in navList" :key="value.imgUrl" :icon="value.imgUrl" text="新品首发" />
            </van-grid>
            <div class="pic">
              <a href=""><img src="@/assets/banner.png" alt=""></a>
            </div>
            <p>——猜你喜欢——</p>
            <div class="list">
              <GoodsItem v-for="item in proList" :key="item.goods_id" :item="item"></GoodsItem>
            </div>
        </div>
    </template>
    
    <script>
    import 'vant/lib/index.less'
    import GoodsItem from '@/components/GoodsItem.vue'
    import { getHomeData } from '@/api/home'
    export default {
      name: 'HomePage',
      components: {
        GoodsItem
      },
      data () {
        return {
          bannerList: [], // 轮播图数据
          navList: [], // 导航数据
          proList: [] // 商品数据
        }
      },
      async created () {
        const { data: { pageData } } = await getHomeData()
        this.bannerList = pageData.items[1].data
        this.navList = pageData.items[3].data
        this.proList = pageData.items[6].data
      }
    }
    </script>
    
    <style scoped lang="less">
      *{
          margin: 0;
          padding: 0;
        }
       .van-nav-bar{
          background-color: rgb(207, 35, 0);
          /deep/ .van-nav-bar__title {
            color: white;
            font-weight: 700;
          }
        }
       .my-swipe img{
        width: 100vw;
        height: 30vh;
      }
      .pic {
        margin-bottom: 20px;
        img{
        width: 100vw;
      }
      }
      p{
        text-align: center;
      }
      .list{
        margin-top: 20px;
      }
    </style>
    

搜索页

构建搜索页的静态布局,完成历史记录的管理

功能:

  1. 搜索历史基本渲染

  2. 点击搜索(添加历史)

    点击搜索按钮或底下历史记录,都能进行搜索

    • 若之前没有相同搜索关键词:直接追加到最前面

    • 若之前相同搜索关键词:将原有的关键字移除,再追加到最前面

  3. 清空历史:添加清空图标,可以清空历史记录

  4. 持久化:刷新历史不丢失

    (通常多端的搜索历史并不同步,不需要存入后台)

const HISTORTY_KEY = 'HISTORYLIST'

// 获取历史记录
export const getHistory = () => {
  const result = localStorage.getItem(HISTORTY_KEY)
  return result ? JSON.parse(result) : []
}

// 修改历史记录
export const setHistory = (list) => {
  localStorage.setItem(HISTORTY_KEY, JSON.stringify(list))
}
<template>
    <div class="search">
        <van-nav-bar
          title="商品搜索"
          left-arrow
          @click-left="$router.back()"
        />
        <div class="searchInput">
          <van-search
            v-model="value"
            show-action
            placeholder="请输入搜索关键词"
          >
            <template #action>
              <van-button @click="goSearch(value)" square type="primary" size="small" color="red">搜索</van-button>
            </template>
          </van-search>
        </div>
        <div class="history" v-if="history.length>0">
          <div class="delete">
              <p>最近搜索</p>
              <van-icon name="delete-o"  @click="clear"/>
          </div>
          <!-- <van-row>
            <van-col span="8"> -->
              <div class="tag">
                <van-tag round type="primary" plain size="large"
                color="grey" text-color="black" @click="goSearch(item)"
                v-for="item in history" :key="item">{{item}}</van-tag>
              </div>
          <!-- </van-col>
          </van-row> -->
        </div>
    </div>
</template>

<script>
import { getHistory, setHistory } from '@/utils/storage'
export default {
  name: 'SearchIndex',
  data () {
    return {
      value: '', // 搜索框的值
      history: getHistory()
    }
  },
  methods: {
    goSearch (value) {
      const index = this.history.indexOf(value)
      // 如果存在相同的项
      if (index !== -1) {
        this.history.splice(index, 1)
      }
      this.history.unshift(value)
      setHistory(this.history)
      this.value = ''
      this.$router.push(`/searchlist?search=${value}`)
    },
    clear () {
      this.history = []
      setHistory([])
    }
  }
}
</script>

<style scoped>
  .searchInput {
    button{
      width: 70px;
      display: block;
      margin-top: 1px;
      margin: 0;
    }
  }
  .history {
    .delete{
      width: 95vw;
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 10px;
    }
    padding: 10px;
    p {
      font-size: 14px;
      color: #999;
      margin-bottom: 10px;
    }
    .tag{
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      align-items: center;
      span{
      display: block;
      width: 80px;
      height: 30px;
      text-align: center;
      margin-right: 30px;
      margin-bottom: 10px;
    }
    }
  }
</style>

image


搜索列表页

步骤:

  1. 完成静态结构

  2. 封装接口

  3. 完成搜索列表页的渲染:通过this.$route.query.search 获取地址栏的参数

    (如果传递的是undefined或null,则axios会自动把该值屏蔽)

import request from '@/utils/request.js'

export const getProList = (obj) => {
  const { categoryId, goodsName, page } = obj
  return request.get('/goods/list', {
    params: {
      categoryId,
      goodsName,
      page
    }
  })
}
<!--search > list.vue-->
<template>
    <div class="searchList">
      <van-nav-bar
          title="商品列表"
          left-arrow
          @click-left="$router.back()"
        />
        <div class="searchInput">
          <van-search
            :value="querySearch || '搜索商品'"
            @click="$router.push('/search')"
            shape="round"
          >
            <template #action>
              <van-icon name="apps-o" size="25"/>
            </template>
          </van-search>
        </div>
        <div class="txt">
          <span>综合</span>
          <span>销量</span>
          <span>价格</span>
        </div>
        <GoodsItem v-for="item in list" :key="item.goods_id" :item="item"/>
    </div>
</template>

<script>
import GoodsItem from '@/components/GoodsItem.vue'
import { getProList } from '@/api/product'
export default {
  name: 'SearchList',
  components: {
    GoodsItem
  },
  data () {
    return {
      list: [],
      page: 1
    }
  },
  computed: {
    // 获取地址栏的搜索关键字
    querySearch () {
      return this.$route.query.search
    }
  },
  async created () {
    const { data: { list } } = await getProList({
      goodsName: this.querySearch,
      page: this.page,
      categoryId: this.$router.currentRoute.query.categoryId
    })
    this.list = list.data
  }
}
</script>

<style scoped>
  .searchInput{
    i{
      margin-top: 10px;
    }
  }
  .txt{
    display: flex;
    justify-content: space-around;
    margin-top: 10px;
    margin-bottom: 10px;
  }
</style>

image


分类页

  • 展示不同类别下的商品

  • 点击商品图片可以实现跳转到对应类别的搜索结果

import request from '@/utils/request'

export const getCategory = () => {
  return request.get('/category/list')
}
<!--category.vue-->
<template>
    <div class="category">
        <van-nav-bar
          title="全部分类"
        />
        <van-search
          v-model="value"
          shape="round"
          background="rgb(241,241,241)"
          placeholder="请输入搜索关键词"
          @click="$router.push('/search')"
        />
        <div class="contain">
          <van-sidebar v-model="activeKey">
            <van-sidebar-item v-for="(item,index) in list" :key="index" :title="item" @click="getIndex(index)" />
          </van-sidebar>
          <van-grid :border="false" :column-num="3" >
          <van-grid-item v-for="item in proList" :key="item.category_id"
          :icon="item.image.external_url" :text="item.name"  @click="$router.push(`/searchlist?categoryId=${item.category_id}`)"/>
          </van-grid>
        </div>
    </div>
</template>

<script>
import { getCategory } from '@/api/category'
export default {
  name: 'CategoryPage',
  data () {
    return {
      list: [],
      activeKey: 0,
      proList: []
    }
  },
  methods: {
    getIndex (index) {
      this.activeKey = index
      this.loadData()
    },
    async loadData () {
      const { data: { list } } = await getCategory()
      this.list = list.map(item => item.name)
      const res = list.slice(0, 7).map(item => item.children)
      this.proList = res[this.activeKey]
    }
  },
  async created () {
    this.loadData()
  }
}
</script>

<style scoped lang="less">
.contain{
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  .van-sidebar-item{
    width: 100px;
  }
  /deep/ .van-grid{
    width: 230px;
    height: auto;
    display: flex;
    flex: 1;
    /deep/ .van-grid-item{
      height: 20px !important;
    }
  }
}
</style>

image

image


商品详情页

可以看见不同商品的详情及购买用户的评论

步骤:

  1. 静态结构布局

  2. 封装接口

    // 获取商品详情信息
    export const getProDetail = (goodsId) => {
      return request.get('/goods/detail', {
        params: {
          goodsId
        }
      })
    }
    
    // 获取商品评价
    export const getProComment = (goodsId, limit) => {
      return request.get('/comment/listRows', {
        params: {
          goodsId,
          limit
        }
      })
    }
    

  3. 动态路由获取参数

    computed: {
        goodsId () {
          // 动态路由获取参数
          return this.$route.params.id
        }
      },
    

  4. 获取数据动态渲染

<template>
    <div class="prodetail">
        <van-nav-bar
          title="商品详情页"
          left-arrow
          @click-left="$router.back()"
        />
        <van-swipe :autoplay="3000" @change="onSwipeChange">
          <van-swipe-item v-for="(image, index) in images" :key="index" >
            <img :src="image.external_url" />
          </van-swipe-item>
        </van-swipe>
        <van-tag round type="primary" color="rgb(230,230,230)">{{ index }}/{{ images.length }}</van-tag>
        <div class="info">
          <div class="price">
            <span class="new">¥{{ info.line_price_min }}</span>
            <span class="old">¥{{ info.line_price_max }}</span>
        </div>
        <p class="count">已售 {{ info.goods_sales }} 件</p>
        </div>
        <p class=".text-ellipsis-2">{{ info.goods_name }}</p>
        <van-goods-action>
          <van-goods-action-icon icon="wap-home-o" text="首页"  @click="$router.push('/')"/>
          <van-goods-action-icon icon="shopping-cart-o" text="购物车" @click="$router.push('/cart')" />
          <van-goods-action-button color="rgb(241,153,61)" type="warning" text="加入购物车" />
          <van-goods-action-button color="rgb(229,84,60)" type="danger" text="立即购买" />
        </van-goods-action>
        <div class="power">
          <van-icon name="success"  color="red"/><span style="margin-right: 10px;">七天无理由退货</span>
          <van-icon name="success"  color="red"/><span>48小时发货</span>
        </div>
        <div class="comment">
          <p style="margin-left: 10px;">商品评价({{ total }}条)</p>
          <p class="more">查看更多<van-icon name="arrow" /></p>
        </div>
        <div class="list" v-for="item in commentList" :key="item.comment_id">
          <div class="top">
            <img :src="item.user.avatar_url || defaultImg" alt="">
            <span class="name">{{item.user.nick_name}}</span>
            <van-rate
              :value="item.score / 2"
              :size="16"
              color="#ffd21e"
              void-icon="star"
              void-color="#eee"
            />
          </div>
          <p class="content">{{ item.content }}</p>
          <p class="time">{{ item.create_time }}</p>
        </div>
        <div class="desc" v-html="info.content"></div>
    </div>
</template>

<script>
import { getProDetail, getProComment } from '@/api/product'
import defaultImg from '@/assets/user.jpg'
export default {
  name: 'ProdetailIndex',
  computed: {
    goodsId () {
      // 动态路由获取参数
      return this.$route.params.id
    }
  },
  data () {
    return {
      defaultImg: defaultImg,
      info: {},
      images: [],
      index: 1,
      total: 0, // 总评论数
      commentList: [] // 评论列表
    }
  },
  async created () {
    const { data: res } = await getProDetail(this.goodsId)
    this.info = res.detail
    this.images = res.detail.goods_images
    // 获取评价,只显示前3条评论
    const { data: { list, total } } = await getProComment(this.goodsId, 3)
    this.commentList = list
    this.total = total
  },
  methods: {
    onSwipeChange (index) {
      this.index = index + 1
    }
  }
}
</script>

<style scoped>
*{
  margin: 0;
  padding: 0;
}
.more{
  color: #999;
  font-size: 12px;
  margin-bottom: 10px;
}
.info{
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}
.new {
      color: red;
      font-size: 16px;
      margin-right: 10px;
    }

    .old {
      color: #999;
      font-size: 16px;
      text-decoration: line-through;
    }
    .count {
      color: #999;
      font-size: 12px;
      margin-bottom: 10px;
      margin-top: 10px;
    }
    .van-goods-action{
      position: fixed;
      bottom: 0;
      width: 100vw;
      background-color: #fff;
    }
    .van-swipe{
      img{
        width: 100%;
        height: 400px;
      }
    }
    .van-tag{
      position: absolute;
      top: 430px;
      right: 0;
    }
    .power{
      line-height: 50px;
      background-color: rgb(251, 251, 251);
      vertical-align: middle;
      height: 50px;
    }
    .comment{
      display: flex;
      justify-content: space-between;
      font-size: 12px;
      margin-top: 10px;
    }
    .desc{
      width: 100vw;
    }
    .top img{
      width: 18px;
      height: 18px;
      border-radius: 50%;
      margin-left: 10px;
    }
    .name{
      font-size: 14px;
      margin-right: 10px;
    }
    .content{
      font-size: 14px;
      margin-bottom: 10px;
      margin-top: 10px;
      margin-left: 10px;
    }
    .time{
      font-size: 12px;
      color: #999;
      margin-bottom: 10px;
      margin-top: 10px;
      margin-left: 10px;
    }
</style>

image

image


加入购物车功能

点击加入购物车,唤起弹层效果

基于vant-ui的ActionSheet动作面板使用弹层

封装了数字框组件,实现商品数量的改变

<!--Components > CountBox.vue-->
<template>
    <div class="count-box">
        <button class="minus" @click="handleSub">-</button>
        <input class="inp" @change="handleChange" type="text" :value="1">
        <button class="add" @click="handleAdd">+</button>
    </div>
</template>

<script>
export default {
  props: {
    value: {
      type: Number,
      default: 1
    }
  },
  methods: {
    handleSub () {
      if (this.value <= 1) return
      this.$emit('input', this.value - 1)
    },
    handleAdd () {
      this.$emit('input', this.value + 1)
    },
    handleChange (e) {
      const num = +e.target.value // 转化为数字
      // 如果输入的值不合理则回退为原来的值
      if (isNaN(num) || num < 1) {
        e.target.value = this.value
        return
      }
      this.$emit('input', num)
    }
  }
}

</script>

<style scoped>
    .count-box{
        display: flex;
        width: 110px;
        .minus,.add{
        width: 30px;
        height: 30px;
        outline: none;
        border: none;
        background-color: #efefef;
       }
       .inp{
        width: 40px;
        height: 30px;
        outline: none;
        border: none;
        margin: 0 5px;
        background-color: #efefef;
        text-align: center;
        }
    }
</style>
<!--prodetail > index.vue-->
<template>
    <div class="prodetail">
        <van-nav-bar
          title="商品详情页"
          left-arrow
          @click-left="$router.back()"
        />
        <van-swipe :autoplay="3000" @change="onSwipeChange">
          <van-swipe-item v-for="(image, index) in images" :key="index" >
            <img :src="image.external_url" />
          </van-swipe-item>
        </van-swipe>
        <van-tag round type="primary" color="rgb(230,230,230)">{{ index }}/{{ images.length }}</van-tag>
        <div class="info">
          <div class="price">
            <span class="new">¥{{ info.line_price_min }}</span>
            <span class="old">¥{{ info.line_price_max }}</span>
        </div>
        <p class="count">已售 {{ info.goods_sales }} 件</p>
        </div>
        <p class=".text-ellipsis-2">{{ info.goods_name }}</p>
        <van-goods-action>
          <van-goods-action-icon icon="wap-home-o" text="首页"  @click="$router.push('/')"/>
          <van-goods-action-icon icon="shopping-cart-o" text="购物车" @click="$router.push('/cart')" />
          <van-goods-action-button color="rgb(241,153,61)" @click="addFn" type="warning" text="加入购物车"/>
          <van-goods-action-button color="rgb(229,84,60)" @click="buyFn" type="danger" text="立即购买" />
        </van-goods-action>
        <div class="power">
          <van-icon name="success"  color="red"/><span style="margin-right: 10px;">七天无理由退货</span>
          <van-icon name="success"  color="red"/><span>48小时发货</span>
        </div>
        <div class="comment">
          <p style="margin-left: 10px;">商品评价({{ total }}条)</p>
          <p class="more">查看更多<van-icon name="arrow" /></p>
        </div>
        <div class="list" v-for="item in commentList" :key="item.comment_id">
          <div class="top">
            <img :src="item.user.avatar_url || defaultImg" alt="">
            <span class="name">{{item.user.nick_name}}</span>
            <van-rate
              :value="item.score / 2"
              :size="16"
              color="#ffd21e"
              void-icon="star"
              void-color="#eee"
            />
          </div>
          <p class="content">{{ item.content }}</p>
          <p class="time">{{ item.create_time }}</p>
        </div>
        <div class="desc" v-html="info.content"></div>

        <!-- 弹层 -->
         <van-action-sheet v-model="show"
         style="width: 100vw;"
         :title="mode === 'cart' ? '加入购物车' : '立即购买'">
          <div class="contentPannel">
            <div class="main">
            <div class="left">
              <img :src="info.goods_image" alt="">
            </div>
            <div class="right">
              <p class="price" style="color: rgb(240,149,60);font-size: 22px;">¥{{ info.line_price_min }}</p>
              <p class="nums">库存{{ info.stock_total }}</p>
            </div>
            </div>
            <div class="text">
            <p>数量</p>
            <p><CountBox v-model="addCount"></CountBox></p>
            </div>
            <div class="button" v-if="info.stock_total>0">
            <van-goods-action-button
              type="danger"
              text="加入购物车"
              color="rgb(251,151,1)"
              style="position: fixed; bottom: 0;left: 30px;width: 80vw;"
            />
            </div>
            <div class="button" v-else>
            <van-goods-action-button
              text="该商品已抢完"
              color="rgb(204,204,204)"
              style="position: fixed; bottom: 0;left: 30px;width: 80vw;"
            />
            </div>
          </div>
        </van-action-sheet>
    </div>
</template>

<script>
import { getProDetail, getProComment } from '@/api/product'
import defaultImg from '@/assets/user.jpg'
import CountBox from '@/components/CountBox.vue'
export default {
  name: 'ProdetailIndex',
  components: {
    CountBox
  },
  computed: {
    goodsId () {
      // 动态路由获取参数
      return this.$route.params.id
    }
  },
  data () {
    return {
      addCount: 1, // 数字框绑定的数据
      show: false,
      mode: 'cart',
      defaultImg: defaultImg,
      info: {},
      images: [],
      index: 1,
      total: 0, // 总评论数
      commentList: [] // 评论列表
    }
  },
  async created () {
    const { data: res } = await getProDetail(this.goodsId)
    this.info = res.detail
    console.log(this.info)
    this.images = res.detail.goods_images
    // 获取评价,只显示前3条评论
    const { data: { list, total } } = await getProComment(this.goodsId, 3)
    this.commentList = list
    this.total = total
  },
  methods: {
    onSwipeChange (index) {
      this.index = index + 1
    },
    addFn () {
      this.show = true
    },
    buyFn () {
      this.mode = 'buy'
      this.show = true
    }
  }
}
</script>

<style scoped>
*{
  margin: 0;
  padding: 0;
}
.text{
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}
.main{
  display: flex;
}
.left img{
  width: 100px;
  height: 100px;
  margin-right: 10px;
}
.button{
  width: 100vw;
  text-align:left;
  padding-left: -10px;
}
.contentPannel {
  margin: auto;
    padding: 16px 16px 160px;
    width: 100vw;
  }
.more{
  color: #999;
  font-size: 12px;
  margin-bottom: 10px;
}
.info{
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}
.new {
      color: red;
      font-size: 16px;
      margin-right: 10px;
    }

    .old {
      color: #999;
      font-size: 16px;
      text-decoration: line-through;
    }
    .count {
      color: #999;
      font-size: 12px;
      margin-bottom: 10px;
      margin-top: 10px;
    }
    .van-goods-action{
      position: fixed;
      bottom: 0;
      width: 100vw;
      background-color: #fff;
    }
    .van-swipe{
      img{
        width: 100%;
        height: 400px;
      }
    }
    .van-tag{
      position: absolute;
      top: 430px;
      right: 0;
    }
    .power{
      line-height: 50px;
      background-color: rgb(251, 251, 251);
      vertical-align: middle;
      height: 50px;
    }
    .comment{
      display: flex;
      justify-content: space-between;
      font-size: 12px;
      margin-top: 10px;
    }
    .desc{
      width: 100vw;
    }
    .top img{
      width: 18px;
      height: 18px;
      border-radius: 50%;
      margin-left: 10px;
    }
    .name{
      font-size: 14px;
      margin-right: 10px;
    }
    .content{
      font-size: 14px;
      margin-bottom: 10px;
      margin-top: 10px;
      margin-left: 10px;
    }
    .time{
      font-size: 12px;
      color: #999;
      margin-bottom: 10px;
      margin-top: 10px;
      margin-left: 10px;
    }
</style>

image


判断token

加入购物车必须是登录的用户才可以进行的操作,需要进行鉴权判断用户的token是否存在

addCart () {
      // 判断用户是否登录,是否具有token
      console.log(this.$store.getters['user/token'])
      if (!this.$store.getters['user/token']) {
        // 没有登录,弹出对话框
        this.$dialog.confirm({
          message: '请先登录',
          title: '温馨提示',
          confirmButtonText: '去登录',
          cancelButtonText: '取消'
        }).then(() => {
          // on confirm
          this.$router.push('/login')
        }).catch(() => {
          // on cancel
        })
      }
    },

image


登录页回跳:

//login > index.vue
const url = this.$route.query.backUrl || '/'
this.$router.replace(url)
addCart () {
      // 判断用户是否登录,是否具有token
      console.log(this.$store.getters['user/token'])
      if (!this.$store.getters['user/token']) {
        // 没有登录,弹出对话框
        this.$dialog.confirm({
          message: '请先登录',
          title: '温馨提示',
          confirmButtonText: '去登录',
          cancelButtonText: '取消'
        }).then(() => {
          // 如果需要登录之后跳转回当前页面,需要携带参数
          // this.$router.fullPath(带查询参数)
          console.log('fullpath', this.$router.fullPath)
          this.$router.replace({
            path: '/login',
            query: {
              backUrl: this.$router.currentRoute.fullPath
            }
          })
        }).catch(() => {
        })
      }
    },

加入购物车
import request from '@/utils/request'

// 加入购物车
export const addCart = (goodsId, goodsNum, goodsSkuId) => {
  return request.post('/cart/add', {
    goodsId,
    goodsNum,
    goodsSkuId
  })
}

在请求拦截器中增加header,补充token信息

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  Toast.loading({
    message: '加载中...',
    forbidClick: true,
    duration: 0 // 表示一直加载请求回来之后才能关闭
  })

  // 携带token
  const token = store.getters['user/token']
  if (token) {
    config.headers['Access-Token'] = token
    config.headers.platform = 'H5'
  }
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})
<!--prodetail > index.vue-->
<template>
  <div class="prodetail">
    <van-nav-bar title="商品详情页" left-arrow @click-left="$router.back()" />
    <van-swipe :autoplay="3000" @change="onSwipeChange">
      <van-swipe-item v-for="(image, index) in images" :key="index">
        <img :src="image.external_url" />
      </van-swipe-item>
    </van-swipe>
    <van-tag round type="primary" color="rgb(230,230,230)">{{ index }}/{{ images.length }}</van-tag>
    <div class="info">
      <div class="price">
        <span class="new">¥{{ info.line_price_min }}</span>
        <span class="old">¥{{ info.line_price_max }}</span>
      </div>
      <p class="count">已售 {{ info.goods_sales }} 件</p>
    </div>
    <p class=".text-ellipsis-2">{{ info.goods_name }}</p>
    <van-goods-action>
      <van-goods-action-icon icon="wap-home-o" text="首页" @click="$router.push('/')" />
      <van-goods-action-icon icon="shopping-cart-o" text="购物车" :badge="cartTotal" @click="$router.push('/cart')" />
      <van-goods-action-button color="rgb(241,153,61)" @click="addFn" type="warning" text="加入购物车" />
      <van-goods-action-button color="rgb(229,84,60)" @click="buyFn" type="danger" text="立即购买" />
    </van-goods-action>
    <div class="power">
      <van-icon name="success" color="red" /><span style="margin-right: 10px;">七天无理由退货</span>
      <van-icon name="success" color="red" /><span>48小时发货</span>
    </div>
    <div class="comment">
      <p style="margin-left: 10px;">商品评价({{ total }}条)</p>
      <p class="more">查看更多<van-icon name="arrow" /></p>
    </div>
    <div class="list" v-for="item in commentList" :key="item.comment_id">
      <div class="top">
        <img :src="item.user.avatar_url || defaultImg" alt="">
        <span class="name">{{ item.user.nick_name }}</span>
        <van-rate :value="item.score / 2" :size="16" color="#ffd21e" void-icon="star" void-color="#eee" />
      </div>
      <p class="content">{{ item.content }}</p>
      <p class="time">{{ item.create_time }}</p>
    </div>
    <div class="desc" v-html="info.content"></div>

    <!-- 弹层 -->
    <van-action-sheet v-model="show" style="width: 100vw;" :title="mode === 'cart' ? '加入购物车' : '立即购买'">
      <div class="contentPannel">
        <div class="main">
          <div class="left">
            <img :src="info.goods_image" alt="">
          </div>
          <div class="right">
            <p class="price" style="color: rgb(240,149,60);font-size: 22px;">¥{{ info.line_price_min }}</p>
            <p class="nums">库存{{ info.stock_total }}</p>
          </div>
        </div>
        <div class="text">
          <p>数量</p>
          <p>
            <CountBox v-model="addCount"></CountBox>
          </p>
        </div>
        <div class="button" v-if="info.stock_total > 0">
          <van-goods-action-button type="danger" text="加入购物车" color="rgb(251,151,1)" @click="addCart"
            style="position: fixed; bottom: 0;left: 30px;width: 80vw;" />
        </div>
        <div class="button" v-else>
          <van-goods-action-button text="该商品已抢完" color="rgb(204,204,204)"
            style="position: fixed; bottom: 0;left: 30px;width: 80vw;" />
        </div>
      </div>
    </van-action-sheet>
  </div>
</template>

<script>
import { getProDetail, getProComment } from '@/api/product'
import defaultImg from '@/assets/user.jpg'
import CountBox from '@/components/CountBox.vue'
import { addCart } from '@/api/cart'
export default {
  name: 'ProdetailIndex',
  components: {
    CountBox
  },
  computed: {
    goodsId () {
      // 动态路由获取参数
      return this.$route.params.id
    }
  },
  data () {
    return {
      cartTotal: 0,
      addCount: 5, // 数字框绑定的数据
      show: false,
      mode: 'cart',
      defaultImg: defaultImg,
      info: {},
      images: [],
      index: 1,
      total: 0, // 总评论数
      commentList: [] // 评论列表
    }
  },
  async created () {
    const { data: res } = await getProDetail(this.goodsId)
    this.info = res.detail
    this.images = res.detail.goods_images
    // 获取评价,只显示前3条评论
    const { data: { list, total } } = await getProComment(this.goodsId, 3)
    this.commentList = list
    this.total = total
  },
  methods: {
    onSwipeChange (index) {
      this.index = index + 1
    },
    addFn () {
      this.show = true
    },
    async addCart () {
      // 判断用户是否登录,是否具有token
      console.log(this.$store.getters['user/token'])
      if (!this.$store.getters['user/token']) {
        // 没有登录,弹出对话框
        this.$dialog.confirm({
          message: '请先登录',
          title: '温馨提示',
          confirmButtonText: '去登录',
          cancelButtonText: '取消'
        }).then(() => {
          // 如果需要登录之后跳转回当前页面,需要携带参数
          // this.$router.fullPath(带查询参数)
          console.log('fullpath', this.$router.fullPath)
          this.$router.replace({
            path: '/login',
            query: {
              backUrl: this.$router.currentRoute.fullPath
            }
          })
        }).catch(() => {
        })
        return
      }
      const { data } = await addCart(this.goodsId, this.addCount, this.info.skuList[0].goods_sku_id)
      this.cartTotal = data.cartTotal
      this.$toast('添加购物车成功')
      this.show = false
    },
    buyFn () {
      this.mode = 'buy'
      this.show = true
    }
  }
}
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}

.text {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}

.main {
  display: flex;
}

.left img {
  width: 100px;
  height: 100px;
  margin-right: 10px;
}

.button {
  width: 100vw;
  text-align: left;
  padding-left: -10px;
}

.contentPannel {
  margin: auto;
  padding: 16px 16px 160px;
  width: 100vw;
}

.more {
  color: #999;
  font-size: 12px;
  margin-bottom: 10px;
}

.info {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}

.new {
  color: red;
  font-size: 16px;
  margin-right: 10px;
}

.old {
  color: #999;
  font-size: 16px;
  text-decoration: line-through;
}

.count {
  color: #999;
  font-size: 12px;
  margin-bottom: 10px;
  margin-top: 10px;
}

.van-goods-action {
  position: fixed;
  bottom: 0;
  width: 100vw;
  background-color: #fff;
}

.van-swipe {
  img {
    width: 100%;
    height: 400px;
  }
}

.van-tag {
  position: absolute;
  top: 430px;
  right: 0;
}

.power {
  line-height: 50px;
  background-color: rgb(251, 251, 251);
  vertical-align: middle;
  height: 50px;
}

.comment {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  margin-top: 10px;
}

.desc {
  width: 100vw;
}

.top img {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  margin-left: 10px;
}

.name {
  font-size: 14px;
  margin-right: 10px;
}

.content {
  font-size: 14px;
  margin-bottom: 10px;
  margin-top: 10px;
  margin-left: 10px;
}

.time {
  font-size: 12px;
  color: #999;
  margin-bottom: 10px;
  margin-top: 10px;
  margin-left: 10px;
}
</style>

购物车

通常会将购物车数据基于vuex分模块管理

//cart.js
import { getCartList } from '@/api/cart'
export default {
  namespaced: true,
  state: {
    cartList: []
  },
  mutations: {
    setCartList (state, newList) {
      state.cartList = newList
    }
  },
  actions: {
    // 异步获取购物车列表数据
    async getCartList (constext) {
      const { data } = await getCartList('/cart/list')
      // 后台返回的数据不包含复选框的选中状态,需要手动维护数据,为每一项添加isChecked状态
      data.list.forEach(item => {
        item.isChecked = true
      })
      constext.commit('setCartList', data.list)
    }
  },
  getters: {}
}

需要注意的是vuex中的数据不能使用v-model直接双向绑定,应该使用:value 进行单向绑定

<!--cart.vue-->
<template>
  <div class="cart">
    <van-nav-bar title="购物车" />
    <div class="text">
      <p>共<span>{{ totalCount }}</span>件商品</p>
      <p><van-icon name="edit" />编辑</p>
    </div>
    <div class="cart-item" v-for="item in cartList" :key="item.goods_id">
      <div class="main">
        <div class="left">
          <van-checkbox :value="item.isChecked">
            <img :src="item.goods.goods_image" alt="">
          </van-checkbox>
        </div>
        <div class="right">
          <p class="text-ellipsis-2" style="font-size: 12px;">{{item.goods.goods_name}}</p>
          <div class="bottom">
            <span class="new">¥{{ item.goods.goods_price_min }}</span>
            <CountBox :value="item.goods_num"></CountBox>
          </div>
        </div>
      </div>
    </div>
    <van-submit-bar :price="selTotalPrice" button-text="提交订单" @submit="onSubmit">
      <van-checkbox v-model="checked">全选</van-checkbox>
    </van-submit-bar>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'
import CountBox from '@/components/CountBox'
export default {
  name: 'CartPage',
  components: {
    CountBox
  },
  computed: {
    ...mapState('cart', ['cartList']),
    ...mapGetters('cart', ['totalCount', 'selCartList', 'selTotalCount', 'selTotalPrice'])
  },
  created () {
    if (this.$store.getters['user/token']) {
      this.$store.dispatch('cart/getCartList')
    }
  }
}
</script>

<style scoped>
.text {
  display: flex;
  justify-content: space-between;
  margin: 10px auto;
  background-color: rgb(245, 245, 245);
  height: 40px;
  line-height: 40px;
  padding: 0 10px;

  span {
    color: red;
  }
}
.cart-item {
  padding-left: 10px;
}

.left img {
  width: 80px;
  height: 80px;
}

.main {
  display: flex;
}

.bottom {
  display: flex;
  margin-top: 25px;

  .new {
    margin-right: 50px;
    margin-left: 10px;
    color: rgb(234, 101, 65);
  }
}

.right p {
  margin-left: 10px;
}
</style>

获取选中的商品项:

getters: {
    // 计算购物车商品总数量
    totalCount (state) {
      return state.cartList.reduce((preTotal, item) => preTotal + item.goods_num, 0)
    },
    // 选中的商品项
    selCartList (state) {
      return state.cartList.filter(item => item.isChecked)
    },
    // 选中的商品总数量
    selTotalCount (state, getters) {
      return getters.selCartList.reduce((preTotal, item) => preTotal + item.goods_num, 0)
    },
    // 选中的商品总价格
    selTotalPrice (state, getters) {
      return getters.selCartList.reduce((preTotal, item) => {
        const price = +item.goods.goods_price_min
        return preTotal + item.goods_num * price
      }, 0)
    }
  }

image


全选/反选功能
// cart.js
import { getCartList } from '@/api/cart'
export default {
  namespaced: true,
  state: {
    cartList: []
  },
  mutations: {
    setCartList (state, newList) {
      state.cartList = newList
    },
    toggle (state, goodsId) {
      const goods = state.cartList.find(item => item.goods_id === goodsId)
      goods.isChecked = !goods.isChecked
    },
    toggleAll (state, isChecked) {
      state.cartList.forEach(item => {
        item.isChecked = isChecked
      })
    }
  },
  actions: {
    // 异步获取购物车列表数据
    async getCartList (constext) {
      const { data } = await getCartList('/cart/list')
      // 后台返回的数据不包含复选框的选中状态,需要手动维护数据,为每一项添加isChecked状态
      data.list.forEach(item => {
        item.isChecked = true
      })
      constext.commit('setCartList', data.list)
    }
  },
  getters: {
    // 计算购物车商品总数量
    totalCount (state) {
      return state.cartList.reduce((preTotal, item) => preTotal + item.goods_num, 0)
    },
    // 选中的商品项
    selCartList (state) {
      return state.cartList.filter(item => item.isChecked)
    },
    // 选中的商品总数量
    selTotalCount (state, getters) {
      return getters.selCartList.reduce((preTotal, item) => preTotal + item.goods_num, 0)
    },
    // 选中的商品总价格
    selTotalPrice (state, getters) {
      return getters.selCartList.reduce((preTotal, item) => {
        const price = +item.goods.goods_price_min
        return preTotal + item.goods_num * price
      }, 0)
    },
    isAllChecked (state) {
      return state.cartList.every(item => item.isChecked)
    }
  }
}
toggleAll () {
      this.$store.commit('cart/toggleAll', !this.isAllChecked)
    }

购物车数据更新
// 更新购物车商品
export const updateCart = (goodsId, goodsNum, goodsSkuId) => {
  return request.post('/cart/update', {
    goodsId,
    goodsNum,
    goodsSkuId
  })
}

技巧:既希望保留原本的形参,有需要通过调用函数传参,可以使用箭头函数包装一层

<CountBox @input="(value) => changeCount(value, item.goods_id, item.goods_sku_id)" :value="item.goods_num"></CountBox>
// cart.js
import { getCartList, updateCart } from '@/api/cart'
export default {
  namespaced: true,
  state: {
    cartList: []
  },
  mutations: {
    setCartList (state, newList) {
      state.cartList = newList
    },
    toggle (state, goodsId) {
      const goods = state.cartList.find(item => item.goods_id === goodsId)
      goods.isChecked = !goods.isChecked
    },
    toggleAll (state, isChecked) {
      state.cartList.forEach(item => {
        item.isChecked = isChecked
      })
    },
    // 改变购物车商品数量
    changeCartNum (state, obj) {
      const goods = state.cartList.find(item => item.goods_id === obj.goodsId)
      goods.goods_num = obj.goodsNum
    }
  },
  actions: {
    // 异步获取购物车列表数据
    async getCartList (constext) {
      const { data } = await getCartList('/cart/list')
      // 后台返回的数据不包含复选框的选中状态,需要手动维护数据,为每一项添加isChecked状态
      data.list.forEach(item => {
        item.isChecked = true
      })
      constext.commit('setCartList', data.list)
    },
    // 改变购物车商品数量
    async changeCartNumAction (constext, obj) {
      const { goodsNum, goodsId, goodsSkuId } = obj
      // console.log(res)
      // 先本地修改再同步到后台
      constext.commit('changeCartNum', obj)
      await updateCart(goodsNum, goodsId, goodsSkuId)
    }
  },
  getters: {
    // 计算购物车商品总数量
    totalCount (state) {
      return state.cartList.reduce((preTotal, item) => preTotal + item.goods_num, 0)
    },
    // 选中的商品项
    selCartList (state) {
      return state.cartList.filter(item => item.isChecked)
    },
    // 选中的商品总数量
    selTotalCount (state, getters) {
      return getters.selCartList.reduce((preTotal, item) => preTotal + item.goods_num, 0)
    },
    // 选中的商品总价格
    selTotalPrice (state, getters) {
      return getters.selCartList.reduce((preTotal, item) => {
        const price = +item.goods.goods_price_min
        return preTotal + item.goods_num * price
      }, 0)
    },
    isAllChecked (state) {
      return state.cartList.every(item => item.isChecked)
    }
  }
}
changeCount (goodsNum, goodsId, goodsSkuId) {
      // console.log(goodsNum, goodsId, goodsSkuId)
      // 调用vuex的action进行数量的修改
      this.$store.dispatch('cart/changeCartNumAction', { goodsNum, goodsId, goodsSkuId })
    }

点击编辑切换结算与删除的状态
<p @click="isEdit = !isEdit"><van-icon name="edit" />编辑</p>
  
<van-submit-bar v-if="isEdit" :price="selTotalPrice" button-text="删除">
<van-checkbox :value="isAllChecked" @click="toggleAll">全选</van-checkbox>
</van-submit-bar>
<van-submit-bar v-else :price="selTotalPrice" button-text="提交订单">
<van-checkbox :value="isAllChecked" @click="toggleAll">全选</van-checkbox>
</van-submit-bar>


  data () {
    return {
      isEdit: false
    }
  },
  watch: {
    // 监视是否进行编辑,如果需要编辑那么就将全选框的状态改为false
    isEdit (isEdit) {
      if (isEdit) {
        // 如果是编辑状态,那么希望选中的删除的商品是0项
        this.$store.commit('cart/toggleAll', false)
      } else {
        // 如果是结算状态,那么希望选中所有商品进行购买
        this.$store.commit('cart/toggleAll', true)
      }
    }
  },

删除购物车商品
// 删除购物车商品
export const deleteCart = (cartIds) => {
  return request.post('/cart/clear', {
    cartIds
  })
}
<van-submit-bar @submit="handleDel" v-if="isEdit" :price="selTotalPrice" button-text="删除">
  <van-checkbox :value="isAllChecked" @click="toggleAll">全选</van-checkbox>
</van-submit-bar>

async handleDel () {
      console.log(this.selTotalCount)
      if (this.selTotalCount === 0) {
        return
      }
      // 调用vuex的action进行删除
      await this.$store.dispatch('cart/delCartAction')
      this.isEdit = false
    }
// 删除购物车商品
    async delCartAction (constext) {
      const cartId = constext.getters.selCartList.map(item => item.id)
      await deleteCart(cartId)
      Toast('删除成功')
      // 重新加载一次购物车数据
      constext.dispatch('getCartList')
    }

空购物车处理
<!--定义新的计算属性-->
isLogin () {
      return this.$store.getters['user/token']
    }
<!--cart.vue-->
<template>
  <div class="cart">
    <van-nav-bar title="购物车" />
    <div class="box" v-if="isLogin && cartList.length > 0">
      <div class="text">
        <p>共<span>{{ totalCount }}</span>件商品</p>
        <p @click="isEdit = !isEdit"><van-icon name="edit" />编辑</p>
      </div>
      <div class="cart-item" v-for="item in cartList" :key="item.goods_id">
        <div class="main">
          <div class="left">
            <van-checkbox :value="item.isChecked" @click="toggle(item.goods_id)">
              <img :src="item.goods.goods_image" alt="">
            </van-checkbox>
          </div>
          <div class="right">
            <p class="text-ellipsis-2" style="font-size: 12px;">{{item.goods.goods_name}}</p>
            <div class="bottom">
              <span class="new">¥{{ item.goods.goods_price_min }}</span>
              <!-- 既希望保留原本的形参,有需要通过调用函数传参,可以使用箭头函数包装一层 -->
              <!-- <CountBox @input="changeCount(item)" :value="item.goods_num"></CountBox> -->
              <CountBox @input="(value) => changeCount(value, item.goods_id, item.goods_sku_id)"
                :value="item.goods_num">
              </CountBox>
            </div>
          </div>
        </div>
      </div>
      <van-submit-bar @submit="handleDel" v-if="isEdit" :price="selTotalPrice" button-text="删除">
        <van-checkbox :value="isAllChecked" @click="toggleAll">全选</van-checkbox>
      </van-submit-bar>
      <van-submit-bar v-else :price="selTotalPrice" button-text="提交订单">
        <van-checkbox :value="isAllChecked" @click="toggleAll">全选</van-checkbox>
      </van-submit-bar>
    </div>
    <div v-else>
      <img src="@/assets/empty.png" style="width: 200px;position: fixed;top: 30%;left: 30%;" alt="">
      <van-button round type="info" color="rgb(255,44,67)" style="width: 96px;height: 37px;position: fixed;top: 61%;left: 41%;" @click="$router.push('/')">去逛逛</van-button>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'
import CountBox from '@/components/CountBox'
export default {
  name: 'CartPage',
  components: {
    CountBox
  },
  data () {
    return {
      isEdit: false
    }
  },
  watch: {
    // 监视是否进行编辑,如果需要编辑那么就将全选框的状态改为false
    isEdit (isEdit) {
      if (isEdit) {
        // 如果是编辑状态,那么希望选中的删除的商品是0项
        this.$store.commit('cart/toggleAll', false)
      } else {
        // 如果是结算状态,那么希望选中所有商品进行购买
        this.$store.commit('cart/toggleAll', true)
      }
    }
  },
  computed: {
    ...mapState('cart', ['cartList']),
    ...mapGetters('cart', ['totalCount', 'selCartList', 'selTotalCount', 'selTotalPrice', 'isAllChecked']),
    isLogin () {
      return this.$store.getters['user/token']
    }
  },
  created () {
    if (this.isLogin) {
      this.$store.dispatch('cart/getCartList')
    }
  },
  methods: {
    toggle (goodsid) {
      this.$store.commit('cart/toggle', goodsid)
    },
    toggleAll () {
      this.$store.commit('cart/toggleAll', !this.isAllChecked)
    },
    changeCount (goodsNum, goodsId, goodsSkuId) {
      // console.log(goodsNum, goodsId, goodsSkuId)
      // 调用vuex的action进行数量的修改
      this.$store.dispatch('cart/changeCartNumAction', { goodsNum, goodsId, goodsSkuId })
    },
    async handleDel () {
      console.log(this.selTotalCount)
      if (this.selTotalCount === 0) {
        return
      }
      // 调用vuex的action进行删除
      await this.$store.dispatch('cart/delCartAction')
      this.isEdit = false
    }
  }
}
</script>

<style scoped>
.van-submit-bar{
  position: fixed;
  bottom: 60px;
}
.text {
  display: flex;
  justify-content: space-between;
  margin: 10px auto;
  background-color: rgb(245, 245, 245);
  height: 40px;
  line-height: 40px;
  padding: 0 10px;

  span {
    color: red;
  }
}
.cart-item {
  padding-left: 10px;
}

.left img {
  width: 80px;
  height: 80px;
}

.main {
  display: flex;
}

.bottom {
  display: flex;
  margin-top: 25px;

  .new {
    margin-right: 50px;
    margin-left: 10px;
    color: rgb(234, 101, 65);
  }
}

.right p {
  margin-left: 10px;
}
</style>

image


结算

跳转到订单结算页面,且跳转的同时需要提供订单相关的参数

  • 购物车跳转结算

  • 商品详情页跳转结算

import request from '@/utils/request'

// 获取收获地址
export const getAddress = (id) => {
  return request.get('/address/list')
}
import request from '@/utils/request'

// 订单结算确认
// mdoe: cart => obj { cartIds }
// mode: buyNow => obj { goodsId, goodsNum, goodsSkuId }
export const checkOrder = (mode, obj) => {
  return request.get('/checkout/order', {
    params: {
      mode, // 结算模式 cart/buyNow
      delivery: 10, // 配送方式 10: 快递 20: 自提
      couponId: 0, // 优惠券id
      isUsePoints: 0, // 是否使用积分
      ...obj // 将传递过来的参数对象 动态展开
    }
  })
}
<!--pay > index.vue-->
<template>
  <div class="pay">
    <van-nav-bar title="订单结算台" left-arrow @click-left="$router.back()" />
    <div class="top">
      <van-icon name="logistics" style="margin:0  20px" color="gray" />
      <div class="address" v-if="selectedAddress.address_id">
        <p>{{ selectedAddress.name}}{{ selectedAddress.phone }}</p>
        <p>{{ longAddress }}</p>
      </div>
      <div v-else>
        请选择配送地址
      </div>
      <van-icon name="arrow" color="gray" style="position: fixed; right: 15px;" />
    </div>
    <van-divider dashed :style="{ color: '#1989fa', padding: '0 16px' }"></van-divider>
    <div class="goods-item" v-for="item in order.goodsList" :key="item.goods_id">
      <div class="left">
        <img :src="item.goods_image" alt="">
      </div>
      <div class="right">
        <p class="text-ellipsis-2">{{ item.goods_name }}</p>
        <div class="num">
          <p class="count">× {{ item.total_num }} 件</p>
          <p class="new">¥{{ item.total_pay_price }}</p>
        </div>
      </div>
    </div>
    <p style="position: fixed;right: 10px;">共{{ order.orderTotalNum }}件商品,合计:<span class="new">¥{{ order.orderTotalPrice }}</span></p>
    <van-divider class="first" />
    <div class="one">
      <p>订单金额:</p>
      <p class="new" style="margin-right: 10px;">¥{{ order.orderTotalPrice }}</p>
    </div>
    <div class="one">
      <p>优惠券:</p>
      <p style="margin-right: 10px;">无优惠券可用</p>
    </div>
    <div class="one">
      <p>配送费用:</p>
      <p class="new" style="margin-right: 10px;">+¥{{ 0.00 }}</p>
    </div>
    <van-divider style="margin-bottom: 10px;" />
    <p style="margin-bottom: 10px;margin-left: 10px;">支付方式</p>
    <div class="money">
      <div class="ddd">
        <van-icon name="balance-o" style="margin-right: 5px;" />
        <p>余额支付(可用¥<span>{{ personal.balance}} </span>元)</p>
      </div>
      <van-icon name="certificate" color="red" style="margin-right: 10px;" />
    </div>
    <van-divider class="first" />
    <p style="color: gray;font-size: 12px;">选填:买家留言(50字内)</p>
    <div class="bottom">
      <p>实付款:<span class="new">¥{{ order.orderTotalPrice }}</span></p>
      <van-button type="primary" color="rgb(255,103,37)">提交订单</van-button>
    </div>
  </div>
</template>

<script>
import { getAddress } from '@/api/pay'
import { checkOrder } from '@/api/order'
export default {
  name: 'PayIndex',
  async created () {
    const { data: { list } } = await getAddress()
    this.addressList = list
    // 基于购物车进行结算
    if (this.mode === 'cart') {
      const { data: { order, personal } } = await checkOrder(this.mode, {
        cartIds: this.cartsId
      })
      this.order = order
      this.personal = personal
    }
    // 基于立即购买进行结算
    if (this.mode === 'buyNow') {
      const { data: { order, personal } } = await checkOrder(this.mode, {
        goodsId: this.goodsId,
        goodsNum: this.goodsNum,
        goodsSkuId: this.goodsSkuId
      })
      this.order = order
      this.personal = personal
    }
  },

  data () {
    return {
      addressList: [],
      order: {},
      personal: {}
    }
  },
  computed: {
    selectedAddress () {
      return this.addressList[0] || {}
    },
    longAddress () {
      const region = this.selectedAddress.region
      return region.province + region.city + region.region + this.selectedAddress.detail
    },
    mode () {
      return this.$route.query.mode
    },
    cartsId () {
      return this.$route.query.cartIds
    },
    goodsId () {
      return this.$route.query.goodsId
    },
    goodsNum () {
      return this.$route.query.goodsNum
    },
    goodsSkuId () {
      return this.$route.query.goodsSkuId
    }
  }
}
</script>

<style scoped>
.bottom{
  position: fixed;
  bottom: 0;
  background-color: #fff;
  height: 40px;
  width: 100vw;
  padding-left: 10px;
  line-height: 40px;
  display: flex;
  justify-content: space-between;
}
.money{
  display: flex;
  align-items: center;
  justify-content: space-between;
  .ddd{
    display: flex;
    align-items: center;
    margin-left: 10px;
  }
}
.one{
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
  margin-left: 10px;
}
.first{
  margin-top: 30px;
}
.new {
  color: red;
  font-size: 16px;
  margin-right: 20px;
}
.num{
  margin-top: 40px;
  display: flex;
  align-items: center;
  .new{
    margin-left: 160px;
  }
}
.address p{
  color: gray;
  font-size: 12px;
}
.top{
  display: flex;
  align-items: center;
  margin: 15px 0;
}
.goods-item {
  display: flex;
  height: 148px;
  width: 100vw;
  padding-top: 15px;

  .left {
    margin-right: 10px;
    margin-left: 10px;

    img {
      width: 120px;
      height: 120px;
    }
  }

  .right {
    .count {
      color: #999;
      font-size: 12px;
      margin-bottom: 10px;
      margin-top: 10px;
      width: 40px;
    }

    .old {
      color: #999;
      font-size: 16px;
      text-decoration: line-through;
    }
  }
}

.goods-item:last-child {
  border-bottom: none;
}
</style>
prodetail > index.vue
<template>
  <div class="prodetail">
    <van-nav-bar title="商品详情页" left-arrow @click-left="$router.back()" />
    <van-swipe :autoplay="3000" @change="onSwipeChange">
      <van-swipe-item v-for="(image, index) in images" :key="index">
        <img :src="image.external_url" />
      </van-swipe-item>
    </van-swipe>
    <van-tag round type="primary" color="rgb(230,230,230)">{{ index }}/{{ images.length }}</van-tag>
    <div class="info">
      <div class="price">
        <span class="new">¥{{ info.line_price_min }}</span>
        <span class="old">¥{{ info.line_price_max }}</span>
      </div>
      <p class="count">已售 {{ info.goods_sales }} 件</p>
    </div>
    <p class=".text-ellipsis-2">{{ info.goods_name }}</p>
    <van-goods-action>
      <van-goods-action-icon icon="wap-home-o" text="首页" @click="$router.push('/')" />
      <van-goods-action-icon icon="shopping-cart-o" text="购物车" @click="$router.push('/cart')" />
      <van-tag round type="primary" color="red" v-if="cartTotal>0">{{cartTotal}}</van-tag>
      <van-goods-action-button color="rgb(241,153,61)" @click="addFn" type="warning" text="加入购物车" />
      <van-goods-action-button color="rgb(229,84,60)" @click="buyFn" type="danger" text="立即购买" />
    </van-goods-action>
    <div class="power">
      <van-icon name="success" color="red" /><span style="margin-right: 10px;">七天无理由退货</span>
      <van-icon name="success" color="red" /><span>48小时发货</span>
    </div>
    <div class="comment">
      <p style="margin-left: 10px;">商品评价({{ total }}条)</p>
      <p class="more">查看更多<van-icon name="arrow" /></p>
    </div>
    <div class="list" v-for="item in commentList" :key="item.comment_id">
      <div class="top">
        <img :src="item.user.avatar_url || defaultImg" alt="">
        <span class="name">{{ item.user.nick_name }}</span>
        <van-rate :value="item.score / 2" :size="16" color="#ffd21e" void-icon="star" void-color="#eee" />
      </div>
      <p class="content">{{ item.content }}</p>
      <p class="time">{{ item.create_time }}</p>
    </div>
    <div class="desc" v-html="info.content"></div>

    <!-- 加入购物车/立即购买公用弹层 -->
    <van-action-sheet v-model="show" style="width: 100vw;" :title="mode === 'cart' ? '加入购物车' : '立即购买'">
      <div class="contentPannel">
        <div class="main">
          <div class="left">
            <img :src="info.goods_image" alt="">
          </div>
          <div class="right">
            <p class="price" style="color: rgb(240,149,60);font-size: 22px;">¥{{ info.line_price_min }}</p>
            <p class="nums">库存{{ info.stock_total }}</p>
          </div>
        </div>
        <div class="text">
          <p>数量</p>
          <p>
            <CountBox v-model="addCount"></CountBox>
          </p>
        </div>
        <div class="button" v-if="info.stock_total > 0">
          <van-goods-action-button type="danger" text="加入购物车" color="rgb(251,151,1)" @click="addCart"
            style="position: fixed; bottom: 0;left: 30px;width: 80vw;" />
        </div>
        <div class="button" v-if="mode == 'buy'">
          <van-goods-action-button type="danger" text="立即购买" color="rgb(229,84,60)" @click="goBuy"
            style="position: fixed; bottom: 0;left: 30px;width: 80vw;" />
        </div>
        <div class="button" v-if="info.stock_total <= 0">
          <van-goods-action-button text="该商品已抢完" color="rgb(204,204,204)"
            style="position: fixed; bottom: 0;left: 30px;width: 80vw;" />
        </div>
      </div>
    </van-action-sheet>
  </div>
</template>

<script>
import { getProDetail, getProComment } from '@/api/product'
import defaultImg from '@/assets/user.jpg'
import CountBox from '@/components/CountBox.vue'
import { addCart } from '@/api/cart'
export default {
  name: 'ProdetailIndex',
  components: {
    CountBox
  },
  computed: {
    goodsId () {
      // 动态路由获取参数
      return this.$route.params.id
    }
  },
  data () {
    return {
      cartTotal: 0,
      addCount: 5, // 数字框绑定的数据
      show: false,
      mode: 'cart',
      defaultImg: defaultImg,
      info: {},
      images: [],
      index: 1,
      total: 0, // 总评论数
      commentList: [] // 评论列表
    }
  },
  async created () {
    const { data: res } = await getProDetail(this.goodsId)
    this.info = res.detail
    this.images = res.detail.goods_images
    // 获取评价,只显示前3条评论
    const { data: { list, total } } = await getProComment(this.goodsId, 3)
    this.commentList = list
    this.total = total
  },
  methods: {
    onSwipeChange (index) {
      this.index = index + 1
    },
    addFn () {
      this.show = true
    },
    async addCart () {
      // 判断用户是否登录,是否具有token
      if (!this.$store.getters['user/token']) {
        // 没有登录,弹出对话框
        this.$dialog.confirm({
          message: '请先登录',
          title: '温馨提示',
          confirmButtonText: '去登录',
          cancelButtonText: '取消'
        }).then(() => {
          // 如果需要登录之后跳转回当前页面,需要携带参数
          // this.$router.fullPath(带查询参数)
          console.log('fullpath', this.$router.fullPath)
          this.$router.replace({
            path: '/login',
            query: {
              backUrl: this.$router.currentRoute.fullPath
            }
          })
        }).catch(() => {
        })
        return
      }
      const { data } = await addCart(this.goodsId, this.addCount, this.info.skuList[0].goods_sku_id)
      this.cartTotal = data.cartTotal
      this.$toast('添加购物车成功')
      this.show = false
    },
    buyFn () {
      this.mode = 'buy'
      this.show = true
    },
    goBuy () {
      // token判断
      // 判断用户是否登录,是否具有token
      if (!this.$store.getters['user/token']) {
        // 没有登录,弹出对话框
        this.$dialog.confirm({
          message: '请先登录',
          title: '温馨提示',
          confirmButtonText: '去登录',
          cancelButtonText: '取消'
        }).then(() => {
          // 如果需要登录之后跳转回当前页面,需要携带参数
          // this.$router.fullPath(带查询参数)
          this.$router.replace({
            path: '/login',
            query: {
              backUrl: this.$router.currentRoute.fullPath
            }
          })
        }).catch(() => {
        })
        return
      }
      this.$router.push({
        path: '/pay',
        query: {
          mode: 'buyNow',
          goodsId: this.goodsId,
          goodsNum: this.addCount,
          goodsSkuId: this.info.skuList[0].goods_sku_id
        }
      })
    }
  }
}
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}

.text {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}

.main {
  display: flex;
}

.left img {
  width: 100px;
  height: 100px;
  margin-right: 10px;
}

.button {
  width: 100vw;
  text-align: left;
  padding-left: -10px;
}

.contentPannel {
  margin: auto;
  padding: 16px 16px 160px;
  width: 100vw;
}

.more {
  color: #999;
  font-size: 12px;
  margin-bottom: 10px;
}

.info {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
}

.new {
  color: red;
  font-size: 16px;
  margin-right: 10px;
}

.old {
  color: #999;
  font-size: 16px;
  text-decoration: line-through;
}

.count {
  color: #999;
  font-size: 12px;
  margin-bottom: 10px;
  margin-top: 10px;
}

.van-goods-action {
  position: fixed;
  bottom: 0;
  width: 100vw;
  background-color: #fff;
}

.van-swipe {
  img {
    width: 100%;
    height: 400px;
  }
}

.van-tag {
  position: absolute;
  top: 430px;
  right: 0;
}

.power {
  line-height: 50px;
  background-color: rgb(251, 251, 251);
  vertical-align: middle;
  height: 50px;
}

.comment {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  margin-top: 10px;
}

.desc {
  width: 100vw;
}

.top img {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  margin-left: 10px;
}

.name {
  font-size: 14px;
  margin-right: 10px;
}

.content {
  font-size: 14px;
  margin-bottom: 10px;
  margin-top: 10px;
  margin-left: 10px;
}

.time {
  font-size: 12px;
  color: #999;
  margin-bottom: 10px;
  margin-top: 10px;
  margin-left: 10px;
}
</style>

image


Vue2封装复用技巧:

  • 组件内部复用:封装成一个方法进行多次调用

  • 跨组件复用:mixins混入

    export default{
      // 此处编写Vue实例的配置项,通过一定语法可以直接混入到组件内部
      // data methods computed 生命周期函数...
      // 如果此处和组件内具有同名的data或methods,那么组件中的优先级更高
      // 如果编写了生命周期函数,则mixins中的生命周期函数和页面的生命周期函数会用数组管理
      data(){
        return{
          title: '标题'
        }
      },
      methods:{
        sayHi(){
          console.log('你好')
        }
      }
    }
    

    导入使用:

    <script>
    import loginConfirm from '@/mixins/loginConfirm
      export default{
        // 如果是多个混入,则后面混入的优先级更高,会把前面的覆盖
        mixins: ['loginConfirm']
      }
    </script>
    

支付功能

步骤:

  1. 封装通用请求方法

    // 提交订单
    // mdoe: cart => obj { cartIds, remark}
    // mode: buyNow => obj { goodsId, goodsNum, goodsSkuId, remark }
    export const submitOrder = (mode, obj) => {
      return request.post('/checkout/submit', {
        mode, // 结算模式 cart/buyNow
        delivery: 10, // 配送方式 10: 快递 20: 自提
        couponId: 0, // 优惠券id
        isUsePoints: 0, // 是否使用积分
        payType: 10, // 支付方式 10: 余额支付
        ...obj // 其余参数
      })
    

  2. 买家留言绑定

  3. 注册事件,调用方法提交订单并支付

pay > index.vue
<template>
  <div class="pay">
    <van-nav-bar title="订单结算台" left-arrow @click-left="$router.back()" />
    <div class="top">
      <van-icon name="logistics" style="margin:0  20px" color="gray" />
      <div class="address" v-if="selectedAddress.address_id">
        <p>{{ selectedAddress.name}}{{ selectedAddress.phone }}</p>
        <p>{{ longAddress }}</p>
      </div>
      <div v-else>
        请选择配送地址
      </div>
      <van-icon name="arrow" color="gray" style="position: fixed; right: 15px;" />
    </div>
    <van-divider dashed :style="{ color: '#1989fa', padding: '0 16px' }"></van-divider>
    <div class="goods-item" v-for="item in order.goodsList" :key="item.goods_id">
      <div class="left">
        <img :src="item.goods_image" alt="">
      </div>
      <div class="right">
        <p class="text-ellipsis-2">{{ item.goods_name }}</p>
        <div class="num">
          <p class="count">× {{ item.total_num }} 件</p>
          <p class="new">¥{{ item.total_pay_price }}</p>
        </div>
      </div>
    </div>
    <p style="position: fixed;right: 10px;">共{{ order.orderTotalNum }}件商品,合计:<span class="new">¥{{ order.orderTotalPrice }}</span></p>
    <van-divider class="first" />
    <div class="one">
      <p>订单金额:</p>
      <p class="new" style="margin-right: 10px;">¥{{ order.orderTotalPrice }}</p>
    </div>
    <div class="one">
      <p>优惠券:</p>
      <p style="margin-right: 10px;">无优惠券可用</p>
    </div>
    <div class="one">
      <p>配送费用:</p>
      <p class="new" style="margin-right: 10px;">+¥{{ 0.00 }}</p>
    </div>
    <van-divider style="margin-bottom: 10px;" />
    <p style="margin-bottom: 10px;margin-left: 10px;">支付方式</p>
    <div class="money">
      <div class="ddd">
        <van-icon name="balance-o" style="margin-right: 5px;" />
        <p>余额支付(可用¥<span>{{ personal.balance}} </span>元)</p>
      </div>
      <van-icon name="certificate" color="red" style="margin-right: 10px;" />
    </div>
    <van-divider class="first" />
    <p style="color: gray;font-size: 12px;">选填:买家留言(50字内)</p>
    <textarea v-model="remark"></textarea>
    <div class="bottom">
      <p>实付款:<span class="new">¥{{ order.orderTotalPrice }}</span></p>
      <van-button type="primary" color="rgb(255,103,37)" @click="submitOrder">提交订单</van-button>
    </div>
  </div>
</template>

<script>
import { getAddress } from '@/api/pay'
import { checkOrder, submitOrder } from '@/api/order'
export default {
  name: 'PayIndex',
  async created () {
    const { data: { list } } = await getAddress()
    this.addressList = list
    // 基于购物车进行结算
    if (this.mode === 'cart') {
      const { data: { order, personal } } = await checkOrder(this.mode, {
        cartIds: this.cartsId
      })
      this.order = order
      this.personal = personal
    }
    // 基于立即购买进行结算
    if (this.mode === 'buyNow') {
      const { data: { order, personal } } = await checkOrder(this.mode, {
        goodsId: this.goodsId,
        goodsNum: this.goodsNum,
        goodsSkuId: this.goodsSkuId
      })
      this.order = order
      this.personal = personal
    }
  },

  data () {
    return {
      addressList: [],
      order: {},
      personal: {},
      remark: '' // 买家留言
    }
  },
  computed: {
    selectedAddress () {
      return this.addressList[0] || {}
    },
    longAddress () {
      const region = this.selectedAddress.region
      return region.province + region.city + region.region + this.selectedAddress.detail
    },
    mode () {
      return this.$route.query.mode
    },
    cartsId () {
      return this.$route.query.cartIds
    },
    goodsId () {
      return this.$route.query.goodsId
    },
    goodsNum () {
      return this.$route.query.goodsNum
    },
    goodsSkuId () {
      return this.$route.query.goodsSkuId
    }
  },
  methods: {
    async submitOrder () {
      if (this.mode === 'cart') {
        const res = await submitOrder(this.mode, {
          cartIds: this.cartsId,
          remark: this.remark
        })
        console.log(res)
      }
      if (this.mode === 'buyNow') {
        const res = await submitOrder(this.mode, {
          goodsId: this.goodsId,
          goodsNum: this.goodsNum,
          goodsSkuId: this.goodsSkuId,
          remark: this.remark
        })
        console.log(res)
      }
      this.$toast.success('支付成功')
      setTimeout(() => {
        this.$router.push('myorder')
      }, 1000)
    }
  }
}
</script>

<style scoped>
.bottom{
  position: fixed;
  bottom: 0;
  background-color: #fff;
  height: 40px;
  width: 100vw;
  padding-left: 10px;
  line-height: 40px;
  display: flex;
  justify-content: space-between;
}
.money{
  display: flex;
  align-items: center;
  justify-content: space-between;
  .ddd{
    display: flex;
    align-items: center;
    margin-left: 10px;
  }
}
.one{
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
  margin-left: 10px;
}
.first{
  margin-top: 30px;
}
.new {
  color: red;
  font-size: 16px;
  margin-right: 20px;
}
.num{
  margin-top: 40px;
  display: flex;
  align-items: center;
  .new{
    margin-left: 160px;
  }
}
.address p{
  color: gray;
  font-size: 12px;
}
.top{
  display: flex;
  align-items: center;
  margin: 15px 0;
}
.goods-item {
  display: flex;
  height: 148px;
  width: 100vw;
  padding-top: 15px;

  .left {
    margin-right: 10px;
    margin-left: 10px;

    img {
      width: 120px;
      height: 120px;
    }
  }

  .right {
    .count {
      color: #999;
      font-size: 12px;
      margin-bottom: 10px;
      margin-top: 10px;
      width: 40px;
    }

    .old {
      color: #999;
      font-size: 16px;
      text-decoration: line-through;
    }
  }
}

.goods-item:last-child {
  border-bottom: none;
}
</style>

订单管理

// 订单列表
export const getOrderList = (dataType, page) => {
  return request.get('/order/list', {
    params: {
      dataType,
      page
    }
  })
}
OrderListItem.vue
<template>
    <div class="orderListItem">
        <div class="container" v-for="(goods, index) in item.goods" :key="index">
            <div class="info">
                <p>{{ item.create_time }}</p>
                <p style="color: orange;">待支付</p>
            </div>
            <div class="goods-item">
                <div class="left">
                    <img :src="goods.goods_image" alt="">
                </div>
                <div class="right">
                    <p class="text-ellipsis-2" style="width: 150px;">{{ goods.goods_name }}</p>
                </div>
                <div class="box">
                    <p class="new">¥{{ goods.total_pay_price }}</p>
                    <p class="old">×{{ goods.total_num }}</p>
                </div>
            </div>
            <div class="total">
                <p style="margin: 10px 0;">共{{ item.total_num }}件商品,总金额¥{{ item.total_price }}</p>
                <button>申请取消</button>
            </div>
        </div>
    </div>
</template>

<style>

.total {
    font-size: 14px;
    margin-left: 45vw;
    margin-right: 10px;
    text-align: center;
    margin-bottom: 10px;
}

.info {
    display: flex;
    justify-content: space-between;
    font-size: 14px;
    margin: 20px 10px;
}

.goods-item {
    display: flex;
    height: 148px;
    width: 100vw;
    padding-top: 15px;

    .left {
        margin-right: 10px;
        margin-left: 10px;

        img {
            width: 100px;
            height: 100px;
        }
    }

    .right {
        .count {
            color: #999;
            font-size: 12px;
            margin-bottom: 10px;
            margin-top: 10px;
        }
    }
}

.goods-item:last-child {
    border-bottom: none;
}

.new,
.old {
    color: #999;
    font-size: 14px;
    margin-right: 10px;
}

.box {
    margin-left: 10px;
}
</style>

<script>
export default {
  name: 'OrderListItem',
  props: {
    item: {
      type: Object,
      default: () => {}
    }
  }
}
</script>
myorder > index.vue
<template>
  <div class="myorder">
    <van-nav-bar title="我的订单" left-arrow @click-left="$router.back()" />
    <van-tabs>
      <van-tab title="全部" name="all"></van-tab>
      <van-tab title="待支付" name="payment"></van-tab>
      <van-tab title="待发货" name="delivery"></van-tab>
      <van-tab title="待收货" name="received"></van-tab>
      <van-tab title="待评价" name="comment"></van-tab>
    </van-tabs>
    <!-- 商品项 -->
    <OrderListItem v-for="item in list" :key="item.order_id" :item="item"></OrderListItem>
  </div>
</template>

<script>
import { getOrderList } from '@/api/order'
import OrderListItem from '@/components/OrderListItem'
export default {
  name: 'MyorderIndex',
  components: {
    OrderListItem
  },
  data () {
    return {
      active: this.$route.query.dataType || 'all',
      page: 1,
      list: []
    }
  },
  methods: {
    async getList () {
      const { data: { list } } = await getOrderList(this.active, this.page)
      console.log('list', list)
      list.data.forEach(item => {
        item.total_num = 0
        item.goods.forEach(goods => {
          item.total_num += goods.total_num
        })
      })
      this.list = list.data
      console.log('this.list', this.list)
    }
  },
  watch: {
    active: {
      immediate: true,
      handler () {
        this.getList()
      }
    }
  }
}
</script>

<style scoped>
.total{
  font-size: 14px;
  float: right;
  margin-right: 10px;
  text-align: center;
}
.info{
  display: flex;
  justify-content: space-between;
  font-size: 14px;
  margin: 10px 10px;
}
.goods-item {
  display: flex;
  height: 148px;
  width: 100vw;
  padding-top: 15px;

  .left {
    margin-right: 10px;
    margin-left: 10px;

    img {
      width: 100px;
      height: 100px;
    }
  }

  .right {
    .count {
      color: #999;
      font-size: 12px;
      margin-bottom: 10px;
      margin-top: 10px;
    }
  }
}

.goods-item:last-child {
  border-bottom: none;
}
  .new, .old  {
    color: #999;
    font-size: 14px;
    margin-right: 10px;
  }
  .box{
    margin-left: 49vw;
  }
</style>

image


用户页

import request from '@/utils/request'

// 获取个人信息
export const getUser = () => {
  return request.get('/user/info')
}
<template>
  <div class="user">
    <div class="info1">
      <div class="userInfo">
        <img src="@/assets/user.jpg" alt="">
        <div class="box">
          <p style="color: goldenrod;font-size: 20px;font-weight: 700;">{{ userInfo.mobile }}</p>
          <p>会员</p>
        </div>
      </div>
    </div>
    <!-- <img class="bg" src="https://smart-shop.itheima.net/static/background/user-header2.png" alt=""> -->
    <div class="top">
      <div class="first">
        <p style="color: red;font-size: 20px;">{{ userInfo.balance }}</p>
        <p>账户余额</p>
      </div>
      <div class="second">
        <p style="color: red;font-size: 20px;">{{ userInfo.points }}</p>
        <p>积分</p>
      </div>
      <div class="third">
        <p style="color: red;font-size: 20px;">{{ 0 }}</p>
        <p>优惠券</p>
      </div>
      <div class="fourth">
        <p><van-icon name="balance-pay" size="25" /></p>
        <p>我的钱包</p>
      </div>
    </div>
    <van-divider />
    <div class="service">
      <div class="one">
        <p><van-icon name="bill-o" size="25" /></p>
        <p>全部订单</p>
      </div>
      <div class="two">
        <p><van-icon name="clock-o" size="25" /></p>
        <p>待支付</p>
      </div>
      <div class="three">
        <p><van-icon name="logistics" size="25" /></p>
        <p>代发货</p>
      </div>
      <div class="four">
        <p><van-icon name="bag-o" size="25" /></p>
        <p>待收货</p>
      </div>
    </div>
    <van-divider  />
    <div class="myService">
      <p style="margin: 10px;">我的服务</p>
      <van-grid style="border: none;">
        <van-grid-item icon="guide-o" text="收货地址" style="color: orange;font-size: 14px;" />
        <van-grid-item icon="point-gift-o" text="领券中心" style="color: orange;font-size: 14px;" />
        <van-grid-item icon="gift-card-o" text="优惠券" style="color: orange;font-size: 14px;" />
        <van-grid-item icon="question-o" text="我的帮助" style="color: orange;font-size: 14px;" />
        <van-grid-item icon="balance-o" text="我的积分" style="color: orange;font-size: 14px;" />
        <van-grid-item icon="refund-o" text="退还/售后" style="color: orange;font-size: 14px;" />
      </van-grid>
    </div>
    <div class="out">
      <van-button type="default" @click="logOut" style="width: 150px;margin-top: 10px;margin-left: 30%;">退出登录</van-button>
    </div>
  </div>
</template>

<script>
import { getUser } from '@/api/user'
export default {
  name: 'UserPage',
  async created () {
    this.getUser()
  },
  data () {
    return {
      userInfo: {}
    }
  },
  methods: {
    logOut () {
      this.$dialog.confirm({
        title: '温馨提示',
        message: '确定要退出登录吗?'
      }).then(() => {
        this.$store.dispatch('user/logOut')
        // 重新请求数据实现页面刷新
        this.getUser()
      }).catch({})
    },
    async getUser () {
      const { data: { userInfo } } = await getUser()
      this.userInfo = userInfo
    }
  }
}
</script>

<style scoped>
.out{
  width: 100%;
  height: 70px;
  background-color: rgb(248, 248, 248);
}
.info1{
  width: 100%;
  height: 150px;
  padding-top: 35px;
  background: url('https://smart-shop.itheima.net/static/background/user-header2.png') no-repeat center;
}
.userInfo{
  display: flex;
  align-items: center;
  margin-left: 10px;
  width: 100%;
  height: 80px;
  img{
    width: 60px;
    height: 60px;
    border-radius: 50%;
  }
}
.bg{
  width: 100%;
  height: 33vw;
}
.top{
  display: flex;
  text-align: center;
  justify-content: space-between;
  margin: 20px 10px;
  line-height: 30px;
}
.service{
    display: flex;
    text-align: center;
    justify-content: space-between;
    margin: 20px 10px;
    line-height: 30px;
}
.van-grid-item__icon{
  color: orange;
}
</style>

image


退出:

  actions: {
    logOut (context) {
      // 个人信息重置
      context.commit('setUserInfo', {})
      // 购物车信息重置 { root: true }开启全局模式,可以访问根级别的mutations
      context.commit('cart/setCartList', [], { root: true })
    }
  },

打包发布

Vue脚手架只参与开发,不参与上线

打包的作用

  • 将多个文件压缩合并成一个文件

  • 语法降级

  • less、sass、ts语法解析

  • ...

打包后,可以生成浏览器能够直接运行的网页

image

命令:npm run build

默认情况下,需要放到服务器的根目录打开

如果希望双击运行,需要在vue.config.js 中配置: publicPath: './'

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  publicPath: './',
  transpileDependencies: true
})

打包优化-路由懒加载

在打包构建应用时,JS包会变得非常大,影响页面加载

image

一打开网页会把所有的JS文件都加载,影响性能


优化思路:把不用路由对应的组件分割成不同的代码块,当路由被访问的时候才加载对应组件

步骤:将首页的子路由设置为默认加载;其他页面设置为按需加载

异步组件改造:将router文件中的组件加载方法改变,并将改变后的代码放置于import代码下方

import Login from '@/views/login'
import Myorder from '@/views/myorder'
import Pay from '@/views/pay'
import Prodetail from '@/views/prodetail'
import Search from '@/views/search/index.vue'
import SearchList from '@/views/search/list.vue'
const Login = () => import('@/views/login')
const Myorder = () => import('@/views/myorder')
const Pay = () => import('@/views/pay')
const Prodetail = () => import('@/views/prodetail')
const Search = () => import('@/views/search')
const SearchList = () => import('@/views/search/list.vue')

image

实现JS文件的拆分与按需加载

posted @ 2025-03-25 15:07  原语  阅读(263)  评论(0)    收藏  举报