一、自定义创建项目
1、基于 VueCli 自定义创建项目
Babel / Router / Vuex / CSS / Linter Vue2.x VueRouter hash模式 CSS预处理 Less ESlint: Standard config Lint on Save In dedicated config files (配置文件所在位置) Npm
2、ESlint 代码规范
1. 认识代码规范
代码规范:一套写代码的约定规则。
JavaScript Standard Style 规范说明: https://standardjs.com/rules-zhcn.html
字符串使用单引号:‘abc’ 无分号:const name = ‘zs’ 关键字后加空格:if (name = ‘ls’) { … } 函数名后加空格:function name (arg) { … } 坚持使用全等 === 摒弃 ==
2. 代码规范错误
目标:学会解决代码规范错误 如果你的代码不符合 standard 的要求,ESlint 会跳出来刀子嘴,豆腐心地提示你。 比如:在main.js中随意做一些改动,添加一些分号,空行。 两种解决方案:
手动修正
根据错误提示来一项一项手动修改纠正。 如果你不认识命令行中的语法报错是什么意思,根据错误代码去 [ESLint 规则表] 中查找其具体含义。 自动修正
基于 vscode 插件 ESLint 高亮错误,并通过配置 自动 帮助我们修复错误。
二、Vant-ui
开发手册:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/
1、Vue 组件库
一般会按照不同平台进行分类:
PC端: element-ui(element-plus)、ant-design-vue 移动端:vant-ui、Mint UI (饿了么)、Cube UI (滴滴
2、vant 全部导入
1. 安装 vant-ui
yarn add vant@latest-v2
2. 导入注册
import Vant from 'vant'
import 'vant/lib/index.css'
Vue. use ( Vant)
3. 测试使用
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
3、按需导入
1. 安装 vant-ui
yarn add vant@latest-v2
2. 安装插件
yarn add babel-plugin-import -D
3. 配置信息
module. exports = {
presets : [
'@vue/cli-plugin-babel/preset'
] ,
plugins : [
[ 'import' , {
libraryName : 'vant' ,
libraryDirectory : 'es' ,
style : true
} , 'vant' ]
]
}
4. 按需导入注册
import Vue from 'vue'
import { Button, Switch, Step, Steps, Tabbar, TabbarItem, NavBar, Toast, Search, Swipe, SwipeItem, Grid, GridItem, Icon, Lazyload, Rate, ActionSheet, Dialog, Checkbox, CheckboxGroup, Tab, Tabs } from 'vant'
Vue. use ( Button)
Vue. use ( Switch)
Vue. use ( Step)
Vue. use ( Steps)
Vue. use ( Tabbar)
Vue. use ( TabbarItem)
Vue. use ( NavBar)
Vue. use ( Toast)
Vue. use ( Search)
Vue. use ( Swipe)
Vue. use ( SwipeItem)
Vue. use ( Grid)
Vue. use ( GridItem)
Vue. use ( Icon)
Vue. use ( Lazyload)
Vue. use ( Rate)
Vue. use ( ActionSheet)
Vue. use ( Dialog)
Vue. use ( Checkbox)
Vue. use ( CheckboxGroup)
Vue. use ( Tab)
Vue. use ( Tabs)
5. 导入配置文件
import '@/plugins/vant-ui'
6. 测试使用
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
4、VW适配
1. 安装插件
yarn add postcss-px-to-viewport@1.1.1 -D
2. 添加配置
根目录新建 postcss.config.js 文件,填入配置
module. exports = {
plugins : {
'postcss-px-to-viewport' : {
viewportWidth : 375
}
}
}
5、配置底部导航
1. 按需引入导航组件
import { Tabbar, TabbarItem } from 'vant'
Vue. use ( Tabbar)
Vue. use ( TabbarItem)
2. 修改文字、图标、颜色
<template>
<div>
<!-- 二级路由出口,二级组件展示位置 -->
<router-view />
<!-- 底部导航栏:active-color,激活时的颜色 inactive-color,未激活时的颜色 -->
<van-tabbar route v-model="active" active-color="#ee0a24" inactive-color="#000">
<van-tabbar-item to="/home" name="home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item to="/category" name="search" icon="apps-o">分类</van-tabbar-item>
<van-tabbar-item to="/cart" name="friends" icon="cart-o">购物车</van-tabbar-item>
<van-tabbar-item to="/user" name="setting" icon="contact-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
name: 'LayoutIndex',
data () {
return {
// 默认展示首页
active: 'home'
}
}
}
</script>
<style>
</style>
6、Toast 轻提示
Toast 默认单例模式,后面 Toast 调用会将前一个覆盖,同一时间只能存在一个 Toast
1. 注册安装
import Vue from 'vue'
import { Button, Toast } from 'vant'
Vue. use ( Button)
Vue. use ( Toast)
2. 调用方式
<template>
<div class="login"></div>
</template>
<script>
// 导入 Toast轻提示 组件
import { Toast } from 'vant'
export default {
name: 'LoginPage',
created () {
Toast('提示内容')
}
}
</script>
<style lang="less" scoped>
</style>
this直接调用 本质:将方法,注册挂载到了Vue原型上 Vue.prototype.$toast = xxx
<template>
<div class="login"></div>
</template>
<script>
export default {
name: 'LoginPage',
created () {
this.$toast('提示内容')
}
}
</script>
<style lang="less" scoped>
</style>
三、Router
1、配置路由
目标:分析项目页面,设计路由,配置一级路由 但凡是单个页面,独立展示的,都是一级路由 @router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Layout from '@/views/layout'
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'
import SearchList from '@/views/search/list'
import Home from '@/views/layout/home'
import Category from '@/views/layout/category'
import Cart from '@/views/layout/cart'
import User from '@/views/layout/user'
Vue. use ( VueRouter)
const router = new VueRouter ( {
routes : [
{
path : '/' ,
component : Layout,
redirect : '/home' ,
children : [
{ path : '/home' , component : Home } ,
{ path : '/category' , component : Category } ,
{ path : '/cart' , component : Cart } ,
{ path : '/user' , component : User }
]
} ,
{ path : '/login' , component : Login } ,
{ path : '/myOrder' , component : MyOrder } ,
{ path : '/pay' , component : Pay } ,
{ path : '/proDetail/:id' , component : ProDetail } ,
{ path : '/search' , component : Search } ,
{ path : '/searchList' , component : SearchList }
]
} )
export default router
2、导入路由
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue. config. productionTip = false
new Vue ( {
router,
render : h => h ( App)
} ) . $mount ( '#app' )
3、页面访问拦截
目标:基于全局前置守卫,进行页面访问拦截处理 所有的路由一旦被匹配到,都会先经过全局前置守卫 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容 访问权限页面时,拦截或放行的关键点? → 用户是否有登录权证 token
1. 获取 token
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue. use ( Vuex)
export default new Vuex. Store ( {
getters : {
token ( state ) {
return state. user. userInfo. token
}
} ,
modules : {
user
}
} )
2. 全局前置导航守卫
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store'
Vue. use ( VueRouter)
const authUrls = [ '/user' , '/myOrder' , '/pay' ]
router. beforeEach ( ( to, from, next ) => {
console. log ( to, from, next)
if ( ! authUrls. includes ( to. path) ) {
next ( )
} else {
const token = store. getters. token
if ( token) {
next ( )
} else {
next ( '/login' )
}
}
} )
export default router
四、Axios 封装
使用 axios 来请求后端接口, 一般都会对 axios 进行 一些配置 (比如: 配置基础地址,请求响应拦截器等) 所以项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个 request 模块中, 便于维护使用 中文文档:https://www.axios-http.cn/docs/intro
1、安装 Axios
yarn add axios
2、新建 request 模块
@/utils/request.js 创建 axios 实例 配置导出势力 instance
import axios from 'axios'
const instance = axios. create ( {
baseURL : 'http://cba.itlike.com/public/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 ) {
return response. data
} , function ( error ) {
return Promise. reject ( error)
} )
export default instance
3、添加响应拦截器
import axios from 'axios'
import { Toast } from 'vant'
const instance = axios. create ( {
baseURL : 'http://cba.itlike.com/public/index.php?s=/api/' ,
timeout : 5000
} )
instance. interceptors. request. use ( function ( config ) {
Toast. loading ( {
message : '加载中...' ,
forbidClick : true ,
loadingType : 'spinner' ,
duration : 0
} )
return config
} , function ( error ) {
return Promise. reject ( error)
} )
instance. interceptors. response. use ( function ( response ) {
const res = response. data
if ( res. status !== 200 ) {
Toast ( res. message)
return Promise. reject ( res. message)
} else {
Toast. clear ( )
}
return res
} , function ( error ) {
return Promise. reject ( error)
} )
export default instance
4、封装 api 接口模块
import request from '@/utils/request'
export const getPicCode = ( ) => {
return request ( {
url : '/captcha/image' ,
method : 'get'
} )
}
5、添加请求 loading 效果
添加 loading 提示的好处:
节流处理:防止用户在一次请求还没回来之前,多次进行点击,发送无效请求 友好提示:告知用户,目前是在加载中,请耐心等待,用户体验会更好
1. 添加 loading 效果
instance. interceptors. request. use ( function ( config ) {
Toast. loading ( {
message : '加载中...' ,
forbidClick : true ,
loadingType : 'spinner' ,
duration : 0
} )
return config
} , function ( error ) {
return Promise. reject ( error)
} )
2. 关闭 loading 效果
响应拦截器中,每次响应,关闭 loading Toast 默认单例模式,后面 Toast 调用会将前一个覆盖,同一时间只能存在一个 Toast
instance. interceptors. response. use ( function ( response ) {
const res = response. data
if ( res. status !== 200 ) {
Toast ( res. message)
return Promise. reject ( res. message)
} else {
Toast. clear ( )
}
return res
} , function ( error ) {
return Promise. reject ( error)
} )
6、统一携带请求头
instance. interceptors. request. use ( function ( config ) {
Toast. loading ( {
message : '加载中...' ,
forbidClick : true ,
loadingType : 'spinner' ,
duration : 0
} )
const token = store. getters. token
if ( token) {
config. headers[ 'Access-Token' ] = token
config. headers. platform = 'H5'
}
return config
} , function ( error ) {
return Promise. reject ( error)
} )
五、Vuex
1、登录权证信息存储
1. 持久化存储
封装 storage 存储模块,利用本地存储,进行 vuex 持久化处理:storage.js
const INFO_KEY = 'shopping_info'
export const getUserInfo = ( ) => {
const defaultInfo = { tonken : '' , userId : '' }
const result = localStorage. getItem ( INFO_KEY )
return result ? JSON . parse ( result) : defaultInfo
}
export const setUserInfo = ( info ) => {
localStorage. setItem ( INFO_KEY , JSON . stringify ( info) )
}
export const removeUserInfo = ( ) => {
localStorage. removeItem ( INFO_KEY )
}
2. 构建 user 模块
import { getUserInfo, setUserInfo } from '@/utils/storage'
export default {
namespaced : true ,
state ( ) {
return {
userInfo : getUserInfo ( )
}
} ,
mutations : {
setUserInfo ( state, userInfo ) {
state. userInfo = userInfo
setUserInfo ( userInfo)
}
} ,
actions : { } ,
getters : { }
}
3. 引入模块
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue. use ( Vuex)
export default new Vuex. Store ( {
state : {
} ,
getters : {
} ,
mutations : {
} ,
actions : {
} ,
modules : {
user
}
} )
2、购物车信息存储
1. 构建 cart 模块
import { getCartList, updateCartNum, deleteCartGoods } from '@/api/cart'
import { Toast } from 'vant'
export default {
namespaced : true ,
state ( ) {
return {
cartTotal : 0 ,
cartList : [ ]
}
} ,
mutations : {
setcartTotal ( state, cartTotal ) {
state. cartTotal = cartTotal
} ,
setCartList ( state, cartList ) {
state. cartList = cartList
} ,
toggleCheck ( state, goodsId ) {
state. cartList. forEach ( item => {
if ( item. goods_id === goodsId) {
item. is_checked = ! item. is_checked
}
} )
} ,
toggleAllCheck ( state, flag ) {
state. cartList. forEach ( item => {
item. is_checked = flag
} )
}
} ,
actions : {
async getCartListAction ( context ) {
const { data : { cartTotal, list } } = await getCartList ( )
list. forEach ( item => {
item. is_checked = true
} )
context. commit ( 'setCartList' , list)
context. commit ( 'setcartTotal' , cartTotal)
} ,
async changeCountAction ( context, obj ) {
const { goodsNum, goodsId, goodsSkuId } = obj
await updateCartNum ( goodsNum, goodsId, goodsSkuId)
context. dispatch ( 'getCartListAction' )
} ,
async deleteCheckedAction ( context ) {
const cartIds = context. getters. getCheckedList. map ( item => item. id)
await deleteCartGoods ( cartIds)
await context. dispatch ( 'getCartListAction' )
context. commit ( 'toggleAllCheck' , false )
Toast. success ( '删除成功!' )
}
} ,
getters : {
getCheckedTotal ( state ) {
return state. cartList. reduce ( ( sum, item ) => sum + ( item. is_checked ? item. goods_num : 0 ) , 0 )
} ,
getCheckedList ( state ) {
return state. cartList. filter ( item => item. is_checked)
} ,
getCheckedTotalPrice ( state, getters ) {
return getters. getCheckedList. reduce ( ( sum, item ) => {
return sum + item. goods. goods_price_min * item. goods_num
} , 0 ) . toFixed ( 2 )
} ,
isAllChecked ( state ) {
return state. cartList. every ( item => item. is_checked) && state. cartList. length !== 0
}
}
}
2. 引入模块
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
Vue. use ( Vuex)
export default new Vuex. Store ( {
strict : true ,
state : {
} ,
getters : {
token ( state ) {
return state. user. userInfo. token
}
} ,
mutations : {
} ,
actions : {
} ,
modules : {
user,
cart
}
} )
六、登陆页面
1、获取图形验证码
1. 接口封装
import request from '@/utils/request'
export const getPicCode = ( ) => {
return request ( {
url : '/captcha/image' ,
method : 'get'
} )
}
2. 页面渲染
<template>
<div class="login">
<img v-if="picUrl" :src="picUrl" alt="" @click="getPicCode">
</div>
</template>
<script>
// 导入 获取图形验证码 请求接口
import { getPicCode } from '@/api/login'
export default {
name: 'LoginPage',
data () {
return {
picUrl: '', // 图形验证码地址
picKey: '' // 图形验证码唯一标识
}
},
created () {
this.getPicCode()
},
methods: {
// 获取图形验证码
async getPicCode () {
// 解构 接口返回参数
const { data: { base64, key } } = await getPicCode()
this.picUrl = base64
this.picKey = key
}
}
}
</script>
<style lang="less" scoped>
</style>
2、短信验证倒计时
1. 接口封装
import request from '@/utils/request'
export const getSmsCode = ( captchaCode, captchaKey, mobile ) => {
return request. post ( '/captcha/sendSmsCaptcha' , {
form : {
captchaCode,
captchaKey,
mobile
}
} )
}
2. 接口渲染
<template>
<div class="login">
<div class="form">
<div class="form-item">
<input v-model="smsCode" class="inp" placeholder="请输入短信验证码" type="text">
<button @click="getCode">
{{ secend === totalSecond ? '获取验证码' : secend + '秒后重新发送' }}
</button>
</div>
</div>
</div>
</template>
<script>
// 导入请求接口
import { getSmsCode } from '@/api/login'
export default {
name: 'LoginPage',
data () {
return {
phone: '',
smsCode: '', // 短信验证码
picCode: '', // 图形验证码
picUrl: '', // 图形验证码地址
picKey: '', // 图形验证码唯一标识
totalSecond: 10, // 总秒数
secend: 10, // 当前秒数,开定时器:secend ---
timer: null // 定时器
}
},
methods: {
// 获取短信验证码验证码
async getCode () {
// 获取验证码前进行校验
if (!this.validFn()) {
return
}
// 当前没有定时器,且 totalSecond 和 secend 一致才可以倒计时
if (!this.timer && this.totalSecond === this.secend) {
// 获取验证码
// 响应结果200继续执行,非200则通过拦截器获取错误信息,并提示用户
await getSmsCode(this.picCode, this.picKey, this.phone)
// 验证码获取成功
this.$toast.success('验证码发送成功!')
// 开启倒计时
this.timer = setInterval(() => {
this.secend--
// 倒计时结束
if (this.secend <= 0) {
// 销毁定时器
clearInterval(this.timer)
this.secend = this.totalSecond // 重置秒数
this.timer = null // 关闭定时器
}
}, 1000)
} else {
this.$toast.fail('请勿重复点击!')
}
},
// 校验手机号和验证码
validFn () {
if (!this.phone) {
this.$toast.fail('请输入手机号码!')
return false
}
if (!/^1[3-9]\d{9}$/.test(this.phone)) {
this.$toast.fail('请输入正确的手机号码!')
return false
}
if (!this.picKey) {
this.$toast.fail('请获取图形验证码!')
}
if (!this.picCode) {
this.$toast.fail('请输入图形验证码!')
return false
}
if (!/^\w{4}$/.test(this.picCode)) {
this.$toast.fail('请输入正确的图形验证码!')
return false
}
return true
},
},
destroyed () {
// 销毁定时器
clearInterval(this.timer)
}
}
</script>
<style lang="less" scoped>
</style>
3、登陆接口
1. 接口封装
import request from '@/utils/request'
export const login = ( mobile, smsCode ) => {
return request. post ( '/passport/login' , {
form : {
mobile,
smsCode,
isParty : false ,
partyData : { }
}
} )
}
2. 接口渲染
<template>
<div class="login">
<div class="login-btn" @click="login">登录</div>
</div>
</template>
<script>
// 导入 请求接口
import { login } from '@/api/login'
export default {
name: 'LoginPage',
data () {
return {
phone: '',
smsCode: '', // 短信验证码
picCode: '', // 图形验证码
picUrl: '', // 图形验证码地址
picKey: '', // 图形验证码唯一标识
totalSecond: 10, // 总秒数
secend: 10, // 当前秒数,开定时器:secend ---
timer: null // 定时器
}
},
methods: {
// 校验手机号和验证码
validFn () {
if (!this.phone) {
this.$toast.fail('请输入手机号码!')
return false
}
if (!/^1[3-9]\d{9}$/.test(this.phone)) {
this.$toast.fail('请输入正确的手机号码!')
return false
}
if (!this.picKey) {
this.$toast.fail('请获取图形验证码!')
}
if (!this.picCode) {
this.$toast.fail('请输入图形验证码!')
return false
}
if (!/^\w{4}$/.test(this.picCode)) {
this.$toast.fail('请输入正确的图形验证码!')
return false
}
return true
},
// 登录
async login () {
// 登陆校验
if (!this.validFn()) {
return
}
if (!/^\d{6}$/.test(this.smsCode)) {
this.$toast.fail('请输入正确的手机验证码!')
return
}
const res = await login(this.phone, this.smsCode)
// 登录成功
this.$toast.success(res.message)
// 跳转到首页
this.$router.push('/')
}
}
}
</script>
<style lang="less" scoped>
</style>
七、首页渲染
1、首页静态结构
1. 静态结构
引入组件:@/plugins/vant-ui.js
import Vue from 'vue'
import { Search, Swipe, SwipeItem, Grid, GridItem } from 'vant'
Vue. use ( Search)
Vue. use ( Swipe)
Vue. use ( SwipeItem)
Vue. use ( Grid)
Vue. use ( GridItem)
2. 接口封装
import request from '@/utils/request'
export const getHomeData = ( ) => {
return request. get ( '/page/detail' , {
params : {
pageId : 0
}
} )
}
3. 封装组件
@/components/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="tit text-ellipsis-2">{{ item.goods_name }}</p>
<p class="count">已售{{ item.goods_sales}}件</p>
<p class="price">
<span class="new">¥{{ item.goods_price_min }}</span>
<span class="old">¥{{ item.goods_price_max }}</span>
</p>
</div>
</div>
</template>
<script>
export default {
name: 'GoodsItem',
props: {
item: {
type: Object,
default: () => {}
}
}
}
</script>
<style lang="less" scoped>
</style>
4. 页面渲染
<template>
<div class="home">
<!-- 导航条 -->
<van-nav-bar :title="title" fixed />
<!-- 搜索框 -->
<van-search readonly shape="round" background="#f1f1f2" :placeholder='searchPrompt' @click="$router.push('/search')" />
<!-- 轮播图 -->
<van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<van-swipe-item v-for="item in bannerList" :key="item.imgName">
<img :src="item.imgUrl" alt="">
</van-swipe-item>
</van-swipe>
<!-- 导航 -->
<van-grid column-num="5" icon-size="40">
<van-grid-item v-for="item in navList" :key="item.imgUrl" :icon="item.imgUrl" text="新品首发" @click="$router.push('/category')" />
</van-grid>
<!-- 主会场 -->
<div class="main">
<img src="@/assets/main.png" alt="">
</div>
<!-- 猜你喜欢 -->
<div class="guess">
<p class="guess-title">—— 猜你喜欢 ——</p>
<div class="goods-list">
<GoodsItem v-for="item in goodsList" :item="item" :key="item.goods_id"></GoodsItem>
</div>
</div>
</div>
</template>
<script>
import GoodsItem from '@/components/GoodsItem.vue'
import { getHomeData } from '@/api/home'
export default {
name: 'HomePage',
components: {
GoodsItem
},
data () {
return {
title: '', // 标题
searchPrompt: '', // 搜索框提示值
bannerList: [], // 轮播图数据
navList: [], // 导航
goodsList: [] // 商品列表
}
},
created () {
this.getGoodsList()
},
methods: {
async getGoodsList () {
const {
data: { pageData }
} = await getHomeData()
this.title = pageData.page.params.title
this.searchPrompt = pageData.items[0].params.placeholder
this.bannerList = pageData.items[1].data
this.navList = pageData.items[3].data
this.goodsList = pageData.items[6].data
}
}
}
</script>
<style lang="less" scoped>
</style>
2、搜索功能
1. 历史记录管理
搜索历史基本渲染 点击搜索 (添加历史):点击 搜索按钮 或 底下历史记录,都能进行搜索
若之前 没有 相同搜索关键字,则直接追加到最前面 若之前 已有 相同搜索关键字,将该原有关键字移除,再追加 清空历史:添加清空图标,可以清空历史记录
<template>
<div class="search">
<van-nav-bar title="商品搜索" left-arrow @click-left="$router.go(-1)" />
<van-search v-model="keyword" show-action placeholder="请输入搜索关键词" clearable>
<template #action>
<div @click="goSearch(keyword)">搜索</div>
</template>
</van-search>
<!-- 搜索历史基本渲染 -->
<div class="search-history" v-if="history.length > 0">
<div class="title">
<span>最近搜索</span>
<van-icon name="delete-o" size="16" @click="clearHistory"/>
</div>
<div class="list">
<div v-for="item in history" :key="item" class="list-item" @click="goSearch(item)">
{{ item }}
</div>
</div>
</div>
</div>
</template>
<script>
import { getSearchHistory, setSearchHistory } from '@/utils/storage'
export default {
name: 'SearchIndex',
data () {
return {
history: getSearchHistory(), // 历史记录
keyword: '' // 搜索关键字
}
},
methods: {
goSearch (keyword) {
// 判断搜索关键字在历史记录中是否存在
if (this.history.indexOf(keyword) !== -1) {
// 存在,则将搜索关键字移除
this.history.splice(this.history.indexOf(keyword), 1)
}
// 将搜索关键字添加到历史记录数组中
this.history.unshift(keyword)
// 将搜索历史记录存储到本地
setSearchHistory(this.history)
// 跳转到搜索结果页面
this.$router.push(`/searchList?search=${keyword}`)
},
clearHistory () {
this.history = []
}
}
}
</script>
<style lang="less" scoped>
</style>
2. 历史记录持久化
持久化:搜索历史需要持久化,刷新历史不丢失 @/utils/storage.js
const HISTORY_KEY = 'shopping_history_search'
export const getSearchHistory = ( ) => {
const history = localStorage. getItem ( HISTORY_KEY )
return history ? JSON . parse ( history) : [ ]
}
export const setSearchHistory = ( history ) => {
localStorage. setItem ( HISTORY_KEY , JSON . stringify ( history) )
}
export const removeSearchHistory = ( ) => {
localStorage. removeItem ( HISTORY_KEY )
}
3、搜索列表
1. 接口渲染
import request from '@/utils/request'
export const getSearchData = ( searchObj ) => {
const { sortType, sortPrice, categoryId, goodsName, page } = searchObj
return request. get ( '/goods/list' , {
params : {
sortType,
sortPrice,
categoryId,
goodsName,
page
}
} )
}
2. 页面调用
<template>
<div class="search">
<van-nav-bar fixed title="商品列表" left-arrow @click-left="$router.go(-1)" />
<van-search :value="searchValue || '搜索商品'" readonly shape="round" background="#ffffff" show-action @click="$router.push('/search')">
<template #action>
<van-icon class="tool" name="apps-o" />
</template>
</van-search>
<!-- 排序选项按钮 -->
<div class="sort-btns">
<div class="sort-item" @click="getGoodsList('all', sortPrice)">综合</div>
<div class="sort-item" @click="getGoodsList('sale', sortPrice)">销量</div>
<div class="sort-item" @click="getGoodsList('price', sortPrice)">价格 </div>
</div>
<div class="goods-list">
<GoodsItem v-for="item in goods" :item="item" :key="item.goods_id"></GoodsItem>
</div>
</div>
</template>
<script>
import GoodsItem from '@/components/GoodsItem.vue'
import { getSearchData } from '@/api/product'
export default {
name: 'SearchIndex',
components: {
GoodsItem
},
data () {
return {
goods: [],
page: 1,
sortPrice: '0' // 0-价格从低到高, 1-价格从高到低
}
},
computed: {
searchValue () {
// 获取地址栏搜索关键字
return this.$route.query.search
}
},
created () {
this.getGoodsList('all', '0')
},
methods: {
getGoodsList (sortType, sortPrice) {
if (sortPrice) {
this.sortPrice = this.sortPrice === '0' ? '1' : '0'
}
getSearchData({
sortType,
sortPrice,
goodsName: this.searchValue,
page: this.page
}).then(({ data: { list } }) => {
this.goods = list.data
})
}
}
}
</script>
<style lang="less" scoped>
</style>
八、商品详情
1、商品详情展示
1. 封装接口
import request from '@/utils/request'
export const getSearchData = ( searchObj ) => {
const { sortType, sortPrice, categoryId, goodsName, page } = searchObj
return request. get ( '/goods/list' , {
params : {
sortType,
sortPrice,
categoryId,
goodsName,
page
}
} )
}
export const getGoodsDetail = ( goodsId ) => {
return request. get ( '/goods/detail' , {
params : {
goodsId
}
} )
}
export const getCommentTotal = ( goodsId ) => {
return request. get ( '/comment/total' , {
params : {
goodsId
}
} )
}
export const getComments = ( goodsId, limit ) => {
return request. get ( '/comment/listRows' , {
params : {
goodsId,
limit
}
} )
}
export const getGoodsService = ( goodsId ) => {
return request. get ( '/goods.service/list' , {
params : {
goodsId
}
} )
}
2. 页面渲染
<template>
<div class="prodetail">
<van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" />
<van-swipe :autoplay="3000" @change="onChange">
<van-swipe-item v-for="item in images" :key="item.file_id">
<img :src="item.external_url" />
</van-swipe-item>
<template #indicator>
<div class="custom-indicator">{{ current + 1 }} / {{ images.length }}</div>
</template>
</van-swipe>
<!-- 商品说明 -->
<div class="info">
<div class="title">
<div class="price">
<span class="now">¥{{ product.goods_price_min }}</span>
<span class="oldprice">¥{{ product.goods_price_max }}</span>
</div>
<div class="sellcount">已售{{ product.goods_sales }}件</div>
</div>
<div class="msg text-ellipsis-2">
{{ product.goods_name }}
</div>
<div class="service">
<div class="left-words">
<span v-for="item in goodsService" :key="item.service_id"><van-icon name="passed" />{{ item.name }}</span>
</div>
<div class="right-icon">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 商品评价 -->
<div class="comment">
<div class="comment-title">
<div class="left">商品评价 ({{ commentTotal.all }}条)</div>
<div class="right">查看更多 <van-icon name="arrow" /> </div>
</div>
<div class="comment-list">
<div class="comment-item" v-for="item in commentList" :key="item.comment_id">
<div class="top">
<img :src="item.user.avatar_url || defaultImg" alt="">
<div class="name">{{ item.user.nick_name }}</div>
<van-rate :size="16" :value="item.score/2" color="#ffd21e" void-icon="star" void-color="#eee"/>
</div>
<div class="content">{{ item.content }}</div>
<div class="time">{{ item.create_time }}</div>
</div>
</div>
</div>
<!-- 商品描述 -->
<div class="desc" v-html="product.content">
</div>
<!-- 底部 -->
<div class="footer">
<div class="icon-home">
<van-icon name="wap-home-o" />
<span @click="$router.push('/')">首页</span>
</div>
<div class="icon-cart">
<van-icon name="shopping-cart-o" />
<span>购物车</span>
</div>
<div class="btn-add">加入购物车</div>
<div class="btn-buy">立刻购买</div>
</div>
</div>
</template>
<script>
import { getGoodsDetail, getCommentTotal, getComments, getGoodsService } from '@/api/product'
import defaultImg from '@/assets/default-avatar.png'
export default {
name: 'ProDetail',
data () {
return {
images: [], // 商品图片
current: 0, // 当前图片索引
product: {}, // 商品详情
commentTotal: {}, // 评论统计
commentList: [], // 评论列表
goodsService: [], // 商品服务
defaultImg
}
},
computed: {
productId () {
return this.$route.params.id
}
},
created () {
this.getProductDetail()
this.getCommentTotal()
this.getComments()
this.getGoodsService()
},
methods: {
// 获取商品详情
getProductDetail () {
getGoodsDetail(this.productId).then(({ data: { detail } }) => {
this.product = detail
this.images = detail.goods_images
})
},
// 获取评论总数
getCommentTotal () {
getCommentTotal(this.productId).then(({ data: { total } }) => {
console.log(total)
this.commentTotal = total
})
},
// 获取评论列表
getComments () {
getComments(this.productId, 5).then(({ data: { list } }) => {
// this.commentTotal = total
this.commentList = list
})
},
// 获取商品保障服务
getGoodsService () {
getGoodsService(this.productId).then(({ data: { list } }) => {
this.goodsService = list
})
},
// 切换轮播图
onChange (index) {
this.current = index
}
}
}
</script>
<style lang="less" scoped>
</style>
2、加入购物车弹窗
1. 数字框组件
@/components/CountBox.vue
<template>
<div class="count-box">
<button @click="handleSub" class="minus">-</button>
<input :value="value" @change="handleChange" class="inp" type="text">
<button @click="handleAdd" class="add">+</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
} else {
this.$emit('input', num)
}
}
}
}
</script>
<style lang="less" scoped>
</style>
2. 加入购物车弹层
<template>
<div class="prodetail">
<van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" />
<van-swipe :autoplay="3000" @change="onChange">
<van-swipe-item v-for="item in images" :key="item.file_id">
<img :src="item.external_url" />
</van-swipe-item>
<template #indicator>
<div class="custom-indicator">{{ current + 1 }} / {{ images.length }}</div>
</template>
</van-swipe>
<!-- 商品说明 -->
<div class="info">
<div class="title">
<div class="price">
<span class="now">¥{{ product.goods_price_min }}</span>
<span class="oldprice">¥{{ product.goods_price_max }}</span>
</div>
<div class="sellcount">已售{{ product.goods_sales }}件</div>
</div>
<div class="msg text-ellipsis-2">
{{ product.goods_name }}
</div>
<div class="service">
<div class="left-words">
<span v-for="item in goodsService" :key="item.service_id"><van-icon name="passed" />{{ item.name }}</span>
</div>
<div class="right-icon">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 商品评价 -->
<div class="comment">
<div class="comment-title">
<div class="left">商品评价 ({{ commentTotal.all }}条)</div>
<div class="right">查看更多 <van-icon name="arrow" /> </div>
</div>
<div class="comment-list">
<div class="comment-item" v-for="item in commentList" :key="item.comment_id">
<div class="top">
<img :src="item.user.avatar_url || defaultImg" alt="">
<div class="name">{{ item.user.nick_name }}</div>
<van-rate :size="16" :value="item.score/2" color="#ffd21e" void-icon="star" void-color="#eee"/>
</div>
<div class="content">{{ item.content }}</div>
<div class="time">{{ item.create_time }}</div>
</div>
</div>
</div>
<!-- 商品描述 -->
<div class="desc" v-html="product.content">
</div>
<!-- 底部 -->
<div class="footer">
<div class="icon-home" @click="$router.push('/')">
<van-icon name="wap-home-o" />
<span>首页</span>
</div>
<div class="icon-cart" @click="$router.push('/cart')">
<van-icon v-if="$store.state.cart.cartTotal === 0" name="shopping-cart-o" />
<van-icon v-else :badge="$store.state.cart.cartTotal" name="shopping-cart-o" />
<span>购物车</span>
</div>
<div @click="addFn" class="btn-add">加入购物车</div>
<div @click="buyFn" class="btn-buy">立刻购买</div>
</div>
<!-- 加入购物车弹层 -->
<van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'">
<div class="product">
<div class="product-title">
<div class="left">
<img :src="product.goods_image" alt="">
</div>
<div class="right">
<div class="price">
<span>¥</span>
<span class="nowprice">{{ product.goods_price_min }}</span>
</div>
<div class="count">
<span>库存</span>
<span>{{ product.stock_total }}</span>
</div>
</div>
</div>
<div class="num-box">
<span>数量</span>
<count-box v-model="addCount"></count-box>
</div>
<div class="showbtn" v-if="product.stock_total > 0">
<div class="btn" v-if="mode === 'cart'" @click="addCart">加入购物车</div>
<div class="btn now" v-else>立刻购买</div>
</div>
<div class="btn-none" v-else>该商品已抢完</div>
</div>
</van-action-sheet>
</div>
</template>
<script>
import { getGoodsDetail, getCommentTotal, getComments, getGoodsService } from '@/api/product'
import defaultImg from '@/assets/default-avatar.png'
import CountBox from '@/components/CountBox.vue'
export default {
name: 'ProDetail',
components: {
CountBox
},
data () {
return {
images: [], // 商品图片
current: 0, // 当前图片索引
product: {}, // 商品详情
commentTotal: {}, // 评论统计
commentList: [], // 评论列表
goodsService: [], // 商品服务
defaultImg, // 默认头像
showPannel: false, // 加入购物车弹层
mode: 'cart',
addCount: 1 // 加入购物车数量
}
},
computed: {
productId () {
return this.$route.params.id
}
},
created () {
this.getProductDetail()
this.getCommentTotal()
this.getComments()
this.getGoodsService()
},
methods: {
// 获取商品详情
getProductDetail () {
getGoodsDetail(this.productId).then(({ data: { detail } }) => {
this.product = detail
this.images = detail.goods_images
})
},
// 获取评论总数
getCommentTotal () {
getCommentTotal(this.productId).then(({ data: { total } }) => {
this.commentTotal = total
})
},
// 获取评论列表
getComments () {
getComments(this.productId, 5).then(({ data: { list } }) => {
// this.commentTotal = total
this.commentList = list
})
},
// 获取商品保障服务
getGoodsService () {
getGoodsService(this.productId).then(({ data: { list } }) => {
this.goodsService = list
})
},
// 加入购物车
addFn () {
this.showPannel = true
this.mode = 'cart'
},
// 立即购买
buyFn () {
this.showPannel = true
this.mode = 'buyNow'
},
addCart () {
if (!this.$store.getters.token) {
// 未登录
this.$dialog.confirm({
title: '温馨提示',
message: '此操作只有登陆才能进行!',
confirmButtonText: '去登录',
cancelButtonText: '在逛逛'
}).then(() => {
// 确定前往登录页(replace:跳转页面不会添加到历史记录)
this.$router.replace({
path: '/login',
query: {
// 登录成功后跳转回来
backUrl: this.$route.fullPath
}
})
}).catch(() => {
// 取消
this.$toast('已取消')
})
} else {
this.showPannel = false
}
},
// 获取购物车列表
getCartList () {
if (!this.$store.getters.token) return
getCartList().then(({ data }) => {
this.$store.dispatch('cart/getCartListAction')
})
},
// 切换轮播图
onChange (index) {
this.current = index
}
}
}
</script>
<style lang="less" scoped>
.prodetail {
padding-top: 46px;
::v-deep .van-icon-arrow-left {
color: #333;
}
img {
display: block;
width: 100%;
}
.custom-indicator {
position: absolute;
right: 10px;
bottom: 10px;
padding: 5px 10px;
font-size: 12px;
background: rgba(0, 0, 0, 0.1);
border-radius: 15px;
}
.desc {
width: 100%;
overflow: scroll;
::v-deep img {
display: block;
width: 100%!important;
}
}
.info {
padding: 10px;
}
.title {
display: flex;
justify-content: space-between;
.now {
color: #fa2209;
font-size: 20px;
}
.oldprice {
color: #959595;
font-size: 16px;
text-decoration: line-through;
margin-left: 5px;
}
.sellcount {
color: #959595;
font-size: 16px;
position: relative;
top: 4px;
}
}
.msg {
font-size: 16px;
line-height: 24px;
margin-top: 5px;
}
.service {
display: flex;
justify-content: space-between;
line-height: 40px;
margin-top: 10px;
font-size: 16px;
background-color: #fafafa;
.left-words {
span {
margin-right: 10px;
}
.van-icon {
margin-right: 4px;
color: #fa2209;
}
}
}
.comment {
padding: 10px;
}
.comment-title {
display: flex;
justify-content: space-between;
.right {
color: #959595;
}
}
.comment-item {
font-size: 16px;
line-height: 30px;
.top {
height: 30px;
display: flex;
align-items: center;
margin-top: 20px;
img {
width: 20px;
height: 20px;
}
.name {
margin: 0 10px;
}
}
.time {
color: #999;
}
}
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 55px;
background-color: #fff;
border-top: 1px solid #ccc;
display: flex;
justify-content: space-evenly;
align-items: center;
.icon-home, .icon-cart {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 14px;
.van-icon {
font-size: 24px;
}
}
.btn-add,
.btn-buy {
height: 36px;
line-height: 36px;
width: 120px;
border-radius: 18px;
background-color: #ffa900;
text-align: center;
color: #fff;
font-size: 14px;
}
.btn-buy {
background-color: #fe5630;
}
}
}
.tips {
padding: 10px;
}
.product {
.product-title {
display: flex;
.left {
img {
width: 90px;
height: 90px;
}
margin: 10px;
}
.right {
flex: 1;
padding: 10px;
.price {
font-size: 14px;
color: #fe560a;
.nowprice {
font-size: 24px;
margin: 0 5px;
}
}
}
}
.num-box {
display: flex;
justify-content: space-between;
padding: 10px;
align-items: center;
}
.btn, .btn-none {
height: 40px;
line-height: 40px;
margin: 20px;
border-radius: 20px;
text-align: center;
color: rgb(255, 255, 255);
background-color: rgb(255, 148, 2);
}
.btn.now {
background-color: #fe5630;
}
.btn-none {
background-color: #cccccc;
}
}
</style>
九、购物车
1、构建 vuex cart 模块
构建模块 @/store/modules/cart.js
2、封装接口
import request from '@/utils/request'
export const addCart = ( goodsId, goodsNum, goodsSkuId ) => {
return request. post ( '/cart/add' , {
goodsId,
goodsNum,
goodsSkuId
} )
}
export const getCartTotalCount = ( ) => {
return request. get ( '/cart/total' )
}
export const getCartList = ( ) => {
return request. get ( '/cart/list' )
}
export const updateCartNum = ( goodsNum, goodsId, goodsSkuId ) => {
return request. post ( '/cart/update' , {
goodsNum,
goodsId,
goodsSkuId
} )
}
export const deleteCartGoods = ( cartIds ) => {
return request. post ( '/cart/clear' , {
cartIds
} )
}
3、动态渲染功能实现
封装 getters 实现动态统计 全选反选功能 数字框修改数量功能 编辑切换状态,删除功能 空购物车处理
<template>
<div class="cart">
<van-nav-bar title="购物车" fixed />
<div v-if="isLogin && cartTotal > 0 ">
<!-- 购物车开头 -->
<div class="cart-title">
<span class="all">共<i>{{ cartTotal }}</i>件商品</span>
<span class="edit" @click="isEdit = !isEdit">
<van-icon name="edit"/>
编辑
</span>
</div>
<!-- 购物车列表 -->
<div class="cart-list">
<div class="cart-item" v-for="item in cartList" :key="item.goods_id">
<van-checkbox :value="item.is_checked" @click="toggleCheck(item.goods_id)"></van-checkbox>
<div class="show">
<img :src="item.goods.goods_image" alt="">
</div>
<div class="info">
<span class="tit text-ellipsis-2">{{ item.goods.goods_name }}</span>
<span class="bottom">
<div class="price">¥ <span>{{ item.goods.goods_price_min }}</span></div>
<!-- 既保留原本的形参,又需要调用函数传参,可以通过箭头函数包装 -->
<count-box @input="(value) => changeCount(value, item.goods_id, item.goods_sku_id)" :value="item.goods_num" ></count-box>
</span>
</div>
</div>
</div>
<div class="footer-fixed">
<div @click="toggleAllCheck" class="all-check">
<van-checkbox :value="isAllChecked" icon-size="18"></van-checkbox>
全选
</div>
<div class="all-total">
<div class="price">
<span>合计:</span>
<span>¥ <i class="totalPrice">{{ checkedTotalPrice }}</i></span>
</div>
<div v-if="!isEdit" class="goPay" :class="{disabled: getCheckedTotal === 0}">结算({{ getCheckedTotal }})</div>
<div v-else @click="deleteCheckedGoods" class="delete" :class="{disabled: getCheckedTotal === 0}">删除</div>
</div>
</div>
</div>
<div class="empty-cart" v-else>
<img src="@/assets/empty.png" alt="">
<div class="tips">
您的购物车是空的, 快去逛逛吧
</div>
<div class="btn" @click="$router.push('/')">去逛逛</div>
</div>
</div>
</template>
<script>
import CountBox from '@/components/CountBox.vue'
import { mapState, mapGetters } from 'vuex'
export default {
name: 'CartPage',
components: {
CountBox
},
data () {
return {
isEdit: false
}
},
computed: {
...mapState('cart', ['cartList', 'cartTotal']),
...mapGetters('cart', ['getCheckedTotal', 'isAllChecked']),
isLogin () {
return this.$store.getters.token
},
checkedTotalPrice () {
return this.$store.getters['cart/getCheckedTotalPrice']
}
},
created () {
if (this.isLogin) {
this.$store.dispatch('cart/getCartListAction')
}
},
methods: {
toggleCheck (goodsId) {
this.$store.commit('cart/toggleCheck', goodsId)
},
toggleAllCheck () {
this.$store.commit('cart/toggleAllCheck', !this.isAllChecked)
},
changeCount (goodsNum, goodsId, goodsSkuId) {
this.$store.dispatch('cart/changeCountAction', {
goodsNum, goodsId, goodsSkuId
})
},
deleteCheckedGoods () {
if (this.getCheckedTotal === 0) return
this.$store.dispatch('cart/deleteCheckedAction')
}
},
watch: {
isEdit (value) {
if (value) {
this.$store.commit('cart/toggleAllCheck', false)
} else {
this.$store.commit('cart/toggleAllCheck', true)
}
}
}
}
</script>
<style lang="less" scoped>
</style>
十、订单结算
1、订单结算台
1. 封装接口
import request from '@/utils/request'
export const getAddress = ( ) => {
return request. get ( '/address/list' )
}
export const getDefaultAddress = ( ) => {
return request. get ( '/address/defaultId' )
}
export const checkoutOrder = ( mode, obj ) => {
return request. get ( '/checkout/order' , {
params : {
delivery : 10 ,
couponId : 0 ,
isUsePoints : 0 ,
mode,
... obj
}
} )
}
2. 订单页面渲染
<template>
<div class="pay">
<van-nav-bar fixed title="订单结算台" left-arrow @click-left="$router.go(-1)" />
<!-- 地址相关 -->
<div class="address">
<div class="left-icon">
<van-icon name="logistics" />
</div>
<div class="info" v-if="selectedAddress.address_id">
<div class="info-content">
<span class="name">{{ selectedAddress.name }}</span>
<span class="mobile">{{ selectedAddress.phone }}</span>
</div>
<div class="info-address">
{{ this.address }}
</div>
</div>
<div class="info" v-else>
请选择配送地址
</div>
<div class="right-icon">
<van-icon name="arrow" />
</div>
</div>
<!-- 订单明细 -->
<div class="pay-list" v-if="order.goodsList">
<div class="list">
<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="tit text-ellipsis-2">
{{ item.goods_name }}
</p>
<p class="info">
<span class="count">x{{ item.total_num }}</span>
<span class="price">¥{{ item.total_pay_price }}</span>
</p>
</div>
</div>
</div>
<div class="flow-num-box">
<span>共 {{ order.orderTotalNum }} 件商品,合计:</span>
<span class="money">¥{{ order.orderPrice }}</span>
</div>
<div class="pay-detail">
<div class="pay-cell">
<span>订单总金额:</span>
<span class="red">¥{{ order.orderTotalPrice }}</span>
</div>
<div class="pay-cell">
<span>优惠券:</span>
<span> {{ order.isUsePoints === '0' ? '无优惠券可用' : ''}}</span>
</div>
<div class="pay-cell">
<span>配送费用:</span>
<span v-if="!selectedAddress">请先选择配送地址</span>
<span v-else class="red">+¥0.00</span>
</div>
</div>
<!-- 支付方式 -->
<div class="pay-way">
<span class="tit">支付方式</span>
<div class="pay-cell">
<span><van-icon name="balance-o" />余额支付(可用 ¥ {{ personal.balance }} 元)</span>
<!-- <span>请先选择配送地址</span> -->
<span class="red"><van-icon name="passed" /></span>
</div>
</div>
<!-- 买家留言 -->
<div class="buytips">
<textarea placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea>
</div>
</div>
<!-- 底部提交 -->
<div class="footer-fixed">
<div class="left">实付款:<span>¥{{ order.orderPayPrice }}</span></div>
<div class="tipsbtn">提交订单</div>
</div>
</div>
</template>
<script>
import { getAddress, getDefaultAddress, checkoutOrder } from '@/api/order'
export default {
name: 'PayIndex',
data () {
return {
selectedAddressId: '', // 默认地址id
addressList: [], // 地址列表
order: {}, // 订单信息
personal: {}, // 个人信息
setting: {}// 系统设置
}
},
computed: {
selectedAddress () {
return this.addressList.find(item => item.address_id === this.selectedAddressId) || {}
},
address () {
const region = this.selectedAddress.region
return region.province + region.city + region.region + this.selectedAddress.detail
},
mode () {
return this.$route.query.mode
},
cartIds () {
return this.$route.query.cartIds
},
goodsId () {
return this.$route.query.goodsId
},
goodsNum () {
return this.$route.query.goodsNum
},
goodsSkuId () {
return this.$route.query.goodsSkuId
}
},
created () {
this.getAddress()
this.getSelectedAddressId()
this.checkoutOrder()
},
methods: {
getAddress () {
getAddress().then(res => {
this.addressList = res.data.list
})
},
getSelectedAddressId () {
getDefaultAddress().then(res => {
const selectedId = res.data.defaultId
if (selectedId) {
this.selectedAddressId = selectedId
}
})
},
checkoutOrder () {
checkoutOrder(this.mode, {
cartIds: this.cartIds,
goodsId: this.goodsId,
goodsNum: this.goodsNum,
goodsSkuId: this.goodsSkuId
}).then(res => {
const { order, personal, setting } = res.data
this.order = order
this.personal = personal
this.setting = setting
})
}
}
}
</script>
<style lang="less" scoped>
</style>
3. mixins 混入
如果此处和组件内,提供同名的 data 和 methods, 则会覆盖此处同名方法
export default {
methods : {
loginConfirm ( ) {
if ( ! this . $store. getters. token) {
this . $dialog. confirm ( {
title : '温馨提示' ,
message : '此操作只有登陆才能进行!' ,
confirmButtonText : '去登录' ,
cancelButtonText : '再逛逛'
} ) . then ( ( ) => {
this . $router. replace ( {
path : '/login' ,
query : {
backUrl : this . $route. fullPath
}
} )
} ) . catch ( ( ) => { } )
return true
}
return false
}
}
}
4. 立即购买
@/views/proDetail/index.vue
<template>
<div class="prodetail">
<!-- 加入购物车弹层 -->
<van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'">
<div class="product">
<div class="product-title">
<div class="left">
<img :src="product.goods_image" alt="">
</div>
<div class="right">
<div class="price">
<span>¥</span>
<span class="nowprice">{{ product.goods_price_min }}</span>
</div>
<div class="count">
<span>库存</span>
<span>{{ product.stock_total }}</span>
</div>
</div>
</div>
<div class="num-box">
<span>数量</span>
<count-box v-model="addCount"></count-box>
</div>
<div class="showbtn" v-if="product.stock_total > 0">
<div class="btn" v-if="mode === 'cart'" @click="addCart">加入购物车</div>
<div class="btn now" v-else @click="goBuyNow">立刻购买</div>
</div>
<div class="btn-none" v-else>该商品已抢完</div>
</div>
</van-action-sheet>
</div>
</template>
<script>
import { addCart, getCartList } from '@/api/cart'
import { Toast } from 'vant'
// 引入 mixins
import loginConfirm from '@/mixins/loginConfirm'
export default {
name: 'ProDetail',
// 引入 mixins
mixins: [loginConfirm],
methods: {
// 加入购物车
addCart () {
// 调用 mixins 方法判断是否登陆
if (this.loginConfirm()) {
return
}
addCart(this.productId, this.addCount, this.product.skuList[0].goods_sku_id).then(({ data, message }) => {
this.showPannel = false
this.getCartList()
Toast.success(message)
})
},
// 立即购买
goBuyNow () {
// 调用 mixins 方法判断是否登陆
if (this.loginConfirm()) {
return
}
if (this.addCount <= 0) return
this.$router.push({
path: '/pay',
query: {
mode: 'buyNow',
goodsId: this.product.goods_id,
goodsNum: this.addCount,
goodsSkuId: this.product.skuList[0].goods_sku_id
}
})
}
}
}
</script>
<style lang="less" scoped>
</style>
5. 购物车结算
<template>
<div class="cart">
<van-nav-bar title="购物车" fixed />
<div v-if="isLogin && cartTotal > 0 ">
<div class="footer-fixed">
<div class="all-total">
<div class="price">
<span>合计:</span>
<span>¥ <i class="totalPrice">{{ checkedTotalPrice }}</i></span>
</div>
<div v-if="!isEdit" class="goPay" @click="goPay" :class="{disabled: getCheckedTotal === 0}">结算({{ getCheckedTotal }})</div>
<div v-else @click="deleteCheckedGoods" class="delete" :class="{disabled: getCheckedTotal === 0}">删除</div>
</div>
</div>
</div>
</div>
</template>
<script>
import CountBox from '@/components/CountBox.vue'
import { mapState, mapGetters } from 'vuex'
export default {
name: 'CartPage',
computed: {
...mapState('cart', ['cartList', 'cartTotal']),
...mapGetters('cart', ['getCheckedTotal', 'getCheckedList', 'isAllChecked']),
},
methods: {
// 跳转到支付页面
goPay () {
// 判断是否勾选商品
if (this.getCheckedTotal === 0) return
this.$router.push({
path: '/pay',
query: {
mode: 'cart',
cartIds: this.getCheckedList.map(item => item.id).join(',')
}
})
}
}
}
</script>
<style lang="less" scoped>
</style>
2、提交订单并支付
1. 封装接口
import request from '@/utils/request'
export const submitOrder = ( mode, obj ) => {
return request. get ( '/checkout/submit' , {
params : {
delivery : 10 ,
couponId : 0 ,
isUsePoints : 0 ,
payType : 10 ,
mode,
... obj
}
} )
}
2. 页面渲染
<template>
<div class="pay">
<van-nav-bar fixed title="订单结算台" left-arrow @click-left="$router.go(-1)" />
<!-- 买家留言 -->
<div class="buytips">
<textarea v-model="remark" placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea>
</div>
<!-- 底部提交 -->
<div class="footer-fixed">
<div class="left">实付款:<span>¥{{ order.orderPayPrice }}</span></div>
<div class="tipsbtn" @click="submitOrder">提交订单</div>
</div>
</div>
</template>
<script>
import { submitOrder } from '@/api/order'
import { Toast } from 'vant'
export default {
name: 'PayIndex',
data () {
return {
selectedAddressId: '', // 默认地址id
addressList: [], // 地址列表
order: {}, // 订单信息
personal: {}, // 个人信息
setting: {}, // 系统设置
remark: ''
}
},
computed: {
mode () {
return this.$route.query.mode
},
cartIds () {
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 () {
const obj = {
remark: this.remark
}
if (this.mode === 'cart') {
Object.assign(obj, { cartIds: this.cartIds })
} else {
Object.assign(obj, { goodsId: this.goodsId })
Object.assign(obj, { goodsNum: this.goodsNum })
Object.assign(obj, { goodsSkuId: this.goodsSkuId })
}
await submitOrder(this.mode, obj)
Toast.success('支付成功')
this.$router.replace('/myOrder')
}
}
}
</script>
<style lang="less" scoped>
</style>
3、订单管理
1. 封装接口
import request from '@/utils/request'
export const getOrderList = ( dataType, page ) => {
return request. get ( '/order/list' , {
params : {
dataType,
page
}
} )
}
2. 订单组件
<template>
<div class="order-list-item">
<div class="tit">
<div class="time">{{ value.create_time }}</div>
<div class="status">
<span>{{ value.state_text }}</span>
</div>
</div>
<div class="list">
<div class="list-item" v-for="item in value.goods" :key="item.goods_id">
<div class="goods-img">
<img :src="item.goods_image" alt="">
</div>
<div class="goods-content text-ellipsis-2">
{{ item.goods_name }}
</div>
<div class="goods-trade">
<p>¥ {{ item.total_pay_price }}</p>
<p>x {{ item.total_num }}</p>
</div>
</div>
</div>
<div class="total">
共{{ totalNum }}件商品,总金额 ¥{{ value.total_price }}
</div>
<div class="actions">
<div v-if="value.order_status === 10">
<span v-if="value.pay_status === 10">立刻付款</span>
<span v-else-if="value.delivery_status === 10">申请取消</span>
<span v-else-if="value.delivery_status === 20 || value.delivery_status === 30">确认收货</span>
</div>
<span v-if="value.order_status === 30">评价</span>
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
required: true
}
},
computed: {
totalNum () {
return this.value.goods.reduce((sum, item) => sum + item.total_num, 0)
}
}
}
</script>
<style lang="less" scoped>
</style>
3. 页面渲染
<template>
<div class="order">
<van-nav-bar title="我的订单" left-arrow @click-left="$router.go(-1)" />
<van-tabs v-model="active" @click="onClick">
<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 orderList" :key="item.id" :value="item"></OrderListItem>
</div>
</template>
<script>
import OrderListItem from '@/components/OrderListItem.vue'
import { getOrderList } from '@/api/order'
export default {
name: 'OrderPage',
components: {
OrderListItem
},
data () {
return {
active: this.$route.query.dataType || 'all',
orderList: []
}
},
created () {
this.getOrderList()
},
methods: {
getOrderList () {
getOrderList(this.active, 1).then(res => {
this.orderList = res.data.list.data
})
},
onClick (name) {
console.log(name)
// this.getOrderList()
}
},
watch: {
active: {
immediate: true,
handler () {
this.getOrderList()
}
}
}
}
</script>
<style lang="less" scoped>
</style>
4、个人中心
1. 封装接口
import request from '@/utils/request'
export const getUserInfoDetail = ( ) => {
return request. get ( '/user/info' )
}
2. 页面渲染
<template>
<div class="user">
<div class="head-page" v-if="isLogin">
<div class="head-img">
<img src="@/assets/default-avatar.png" alt="" />
</div>
<div class="info">
<div class="mobile">{{ detail.mobile }}</div>
<div class="vip">
<van-icon name="diamond-o" />
普通会员
</div>
</div>
</div>
<div v-else class="head-page" @click="$router.push('/login')">
<div class="head-img">
<img src="@/assets/default-avatar.png" alt="" />
</div>
<div class="info">
<div class="mobile">未登录</div>
<div class="words">点击登录账号</div>
</div>
</div>
<div class="my-asset">
<div class="asset-left">
<div class="asset-left-item">
<span>{{ detail.balance || 0 }}</span>
<span>账户余额</span>
</div>
<div class="asset-left-item">
<span>0</span>
<span>积分</span>
</div>
<div class="asset-left-item">
<span>0</span>
<span>优惠券</span>
</div>
</div>
<div class="asset-right">
<div class="asset-right-item">
<van-icon name="balance-pay" />
<span>我的钱包</span>
</div>
</div>
</div>
<div class="order-navbar">
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=all')">
<van-icon name="balance-list-o" />
<span>全部订单</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=payment')">
<van-icon name="clock-o" />
<span>待支付</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=delivery')">
<van-icon name="logistics" />
<span>待发货</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=received')">
<van-icon name="send-gift-o" />
<span>待收货</span>
</div>
</div>
<div class="service">
<div class="title">我的服务</div>
<div class="content">
<div class="content-item">
<van-icon name="records" />
<span>收货地址</span>
</div>
<div class="content-item">
<van-icon name="gift-o" />
<span>领券中心</span>
</div>
<div class="content-item">
<van-icon name="gift-card-o" />
<span>优惠券</span>
</div>
<div class="content-item">
<van-icon name="question-o" />
<span>我的帮助</span>
</div>
<div class="content-item">
<van-icon name="balance-o" />
<span>我的积分</span>
</div>
<div class="content-item">
<van-icon name="refund-o" />
<span>退换/售后</span>
</div>
</div>
</div>
<div class="logout-btn">
<button>退出登录</button>
</div>
</div>
</template>
<script>
import { getUserInfoDetail } from '@/api/user.js'
export default {
name: 'UserPage',
data () {
return {
detail: {}
}
},
created () {
if (this.isLogin) {
this.getUserInfoDetail()
}
},
computed: {
isLogin () {
return this.$store.getters.token
}
},
methods: {
async getUserInfoDetail () {
const { data: { userInfo } } = await getUserInfoDetail()
this.detail = userInfo
console.log(this.detail)
}
}
}
</script>
<style lang="less" scoped>
</style>
3. 退出登陆
<template>
<div class="user">
<div class="logout-btn" v-if="isLogin">
<button>退出登录</button>
</div>
</div>
</template>
<script>
export default {
name: 'UserPage',
methods: {
logout () {
this.$dialog.confirm({
title: '温馨提示',
message: '你确认要退出么?'
})
.then(() => {
this.detail = {}
this.$store.dispatch('user/logout')
})
.catch(() => {
})
}
}
}
</script>
<style lang="less" scoped>
</style>
import { getUserInfo, setUserInfo } from '@/utils/storage'
export default {
namespaced : true ,
state ( ) {
return {
userInfo : getUserInfo ( )
}
} ,
mutations : {
setUserInfo ( state, userInfo ) {
state. userInfo = userInfo
setUserInfo ( userInfo)
}
} ,
actions : {
logout ( context ) {
context. commit ( 'setUserInfo' , { } )
context. commit ( 'cart/setCartList' , [ ] , { root : true } )
}
} ,
getters : { }
}
十一、打包
1、打包命令
yarn build
2、配置publicPath
const { defineConfig } = require ( '@vue/cli-service' )
module. exports = defineConfig ( {
publicPath : './' ,
transpileDependencies : true
} )
3、路由懒加载
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/layout/home'
import Category from '@/views/layout/category'
import Cart from '@/views/layout/cart'
import User from '@/views/layout/user'
import Layout from '@/views/layout'
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. use ( VueRouter)
const router = new VueRouter ( {
routes : [
{
path : '/' ,
component : Layout,
redirect : '/home' ,
children : [
{ path : '/home' , component : Home } ,
{ path : '/category' , component : Category } ,
{ path : '/cart' , component : Cart } ,
{ path : '/user' , component : User }
]
} ,
{ path : '/login' , component : Login } ,
{ path : '/myOrder' , component : MyOrder } ,
{ path : '/pay' , component : Pay } ,
{ path : '/proDetail/:id' , component : ProDetail } ,
{ path : '/search' , component : Search } ,
{ path : '/searchList' , component : SearchList }
]
} )
const authUrls = [ '/myOrder' , '/pay' ]
router. beforeEach ( ( to, from, next ) => {
if ( ! authUrls. includes ( to. path) ) {
next ( )
} else {
if ( store. getters. token) {
next ( )
} else {
next ( '/login' )
}
}
} )
export default router