25-Vue2.x 码路严选

11/17/2001

# 一,项目的搭建

项目参考:http://tubie.gitee.io/maluyanxuan

已经注册的用户名和密码:

用户名:18338812345

密码:123456

大家基于的项目骨架进行开发,骨架中帮我们做了如下的事件:

  • 把该安装的依赖都安装了,版本也确定好了,

  • 处理好了vant组件适配问题,

  • 重置样式问题,

  • flexiable动态计算html的font-size问题

  • vant自动按需加载等

大家基于这个空的项目进行开发,如果你要使用v3去写,自己创建项目。如下:


安装依赖,如下:

运行项目,如下:

效果如下 :

# 二,搭建五大组件,并配置路由

在views目录下面,创建如下五大组件:


对应的代码如下:

// Home.vue
<template>
  <div>
    <h1>Home</h1>
  </div>
</template>

<script>
export default {
  name:"Home",
  data() {
    return {
    };
  }
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Categary.vue
<template>
  <div>
    <h1>Categary</h1>
  </div>
</template>

<script>
export default {
  name:"Categary",
  data() {
    return {
    };
  }
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Cart.vue
<template>
  <div>
    <h1>Cart</h1>
  </div>
</template>

<script>
export default {
  name:"Cart",
  data() {
    return {
    };
  }
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// User.vue
<template>
  <div>
    <h1>User</h1>
  </div>
</template>

<script>
export default {
  name:"User",
  data() {
    return {
    };
  }
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Login.vue
<template>
  <div>
    <h1>Login</h1>
  </div>
</template>

<script>
export default {
  name:"Login",
  data() {
    return {
    };
  }
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

配置对应的路由如下:

// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [{
        path: '/',
        redirect: '/home'
    },
    {
        path: '/home',
        component: () => import( /*webpackChunkName:"home" */ '../views/Home/Home.vue'),
        meta: {
            isShowNav: true
        }
    },
    {
        path: '/categary',
        component: () => import( /*webpackChunkName:"categary" */ '../views/Categary/Categary.vue'),
        meta: {
            isShowNav: true
        }
    },
    {
        path: '/cart',
        component: () => import( /*webpackChunkName:"cart" */ '../views/Cart/Cart.vue'),
        meta: {
            isShowNav: true
        }
    },
    {
        path: '/user',
        component: () => import( /*webpackChunkName:"user" */ '../views/User/User.vue'),
        meta: {
            isShowNav: true
        }
    },
    {
        path: '/login',
        component: () => import( /*webpackChunkName:"login" */ '../views/Login/Login.vue'),
        meta: {
            isShowNav: false
        }
    }
]

const router = new VueRouter({
    routes
})

export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

在App.vue中配置出品,如下:

// App.vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<style lang="less">
</style>

1
2
3
4
5
6
7
8
9
10

在浏览器中测试路由,如下:

http://localhost:8080/#/home
http://localhost:8080/#/categary
http://localhost:8080/#/cart
http://localhost:8080/#/user
http://localhost:8080/#/login

# 三,封装TabBar组件,并实现点击跳转

在components目录下,创建TabBar.vue组件,如下:

从iconfont字体图标库中,下载对应的字体图标(大家直接使用我提供好的就OK),如下:

在入口文件中引入对应的字体图标样式,如下:

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 导入重置样式
import "./assets/reset.css"

// 引入flexible 适配
import "./assets/flexible.js"

// 引入字体图标的样式
import "./assets/fonts/iconfont.css"

Vue.config.productionTip = false

new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

对应的代码如下:

// TabBar.vue
<template>
  <div class="bot_nav_box">
    <router-link to="/home" class="item_box" tag="div">
      <div class="icon iconfont icon-shouye"></div>
      <div class="text">首页</div>
    </router-link>
    <router-link to="/categary" class="item_box" tag="div">
      <div class="icon iconfont icon-grouping"></div>
      <div class="text">分类</div>
    </router-link>
    <router-link to="/cart" class="item_box" tag="div">
      <div class="icon iconfont icon-gouwuche"></div>
      <div class="text">购物车</div>
    </router-link>
    <router-link to="/user" class="item_box" tag="div">
      <div class="icon iconfont icon-31wode"></div>
      <div class="text">我的</div>
    </router-link>
  </div>
</template>

<script>
export default {};
</script>

<style lang='less' scoped>
.bot_nav_box {
  position: fixed;
  bottom: 0;
  width: 100%;
  height: 50px;
  box-shadow: 0 0px 8px 7px #ccc;
  background: #fff;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  text-align: center;
  .item_box {
    flex: 1;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    &.router-link-exact-active {
      color: #1baeae;
    }
    .icon {
      .van-icon {
        font-size: 30px;
      }
    }
    .iconfont {
      font-size: 0.55rem;
    }
    .text {
      font-size: .24rem
    }
  }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

在App.vue组件中引入,并使用,如下:

// App.vue
<template>
  <div id="app">
    <router-view />
    <TabBar v-if='isShowNav'></TabBar>
  </div>
</template>
<script>
import TabBar from "./components/TabBar.vue";
export default {
  components: { TabBar },
  data() {
    return {
      isShowNav: false,
    };
  },
  created() {
  },
  watch: {
    $route: function (val) {
      this.isShowNav = val.meta.isShowNav;
    },
  },
};
</script>

<style lang="less">
body,
html,
#app {
  min-height: 100vh;
}
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  width: 375px;
}

#nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &.router-link-exact-active {
      color: #42b983;
    }
  }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

效果如下:

# 四,Vant组件使用

vant的官网:https://vant-contrib.gitee.io/vant/#/zh-CN/

如果是vue2,参考文档:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/

如果是vue3,参考文档:https://vant-contrib.gitee.io/vant/#/zh-CN

要使用vant中提供的组件,引入方式,分三种:

  • 自动按需引入(推荐)
  • 手动按需引入
  • 全部引入

如何实现自动按需引入?看看文档:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/quickstart

文档怎么说,你怎么做就OK。

在src目录下,创建utils目录,在utils目录下创建vant.js文件,如下:

导入必要的组件(不够的话,后面,再导入),如下:

// vant.js
import Vue from 'vue'

import {
    Button,
    Icon,
    Form,
    Field,
    NavBar,
    Swipe,
    SwipeItem,
    Grid,
    GridItem,
    GoodsAction,
    GoodsActionIcon,
    GoodsActionButton,
    Checkbox,
    Stepper,
    SubmitBar,
    AddressList,
    AddressEdit,
    Area,
    Tab,
    Tabs,
    List,
    Cell,
    SwipeCell,
    Card,
    Dialog,
    Toast,
    ContactCard,
    ActionSheet,
    Loading,
    divider,
    Search,
    sidebar,
    SidebarItem,
    grid
} from 'vant'

[
    Button,
    Icon,
    Form,
    Field,
    NavBar,
    Swipe,
    SwipeItem,
    Grid,
    GridItem,
    GoodsAction,
    GoodsActionIcon,
    GoodsActionButton,
    Checkbox,
    SubmitBar,
    AddressList,
    AddressEdit,
    Area,
    Stepper,
    Tab,
    Tabs,
    List,
    Cell,
    SwipeCell,
    Card,
    Dialog,
    Toast,
    ContactCard,
    ActionSheet,
    Loading,
    divider,
    Search,
    sidebar,
    SidebarItem,
    grid

].forEach(item => {
    Vue.use(item)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

在main.js中,导入对应上面vant.js模块,如下:

// 省略其它代码
// main.js  

// 导入vant.js
import "./utils/vant.js"
1
2
3
4
5

在Home组件中测试,如下:

<template>
  <div>
    <h1>Home</h1>
    <van-button type="danger">测试一下</van-button>
  </div>
</template>
1
2
3
4
5
6

效果如下:

# 五,登录(注册)界面搭建

构建Login.vue组件,参考代码如下:

<template>
    <div class="login">
        <!-- 头部导航栏 -->
        <header>
            <van-nav-bar :title="isTitle ? '登录' : '注册'" left-arrow @click-left="onClickLeft">
                <template #right>
                    <van-icon name="ellipsis" size="18" />
                </template>
            </van-nav-bar>
        </header>
        <!-- logo 图片 -->
        <img src="../../assets/logo.png" width="100px" class="logo" alt="">

        <!-- 登录表单 -->
        <div class="login-form">
            <van-form @submit="onSubmit">
                <van-field v-model="username" name="用户名" label="用户名" placeholder="手机号"
                    :rules="[{ required: true, message: '请填写手机号' }]" />
                <van-field v-model="password" type="password" name="密码" label="密码" placeholder="密码"
                    :rules="[{ required: true, message: '请填写密码' }]" />
                <!-- 验证码 -->
                <Verify ref="veri" :type="2" @error="alertFn(2)" @success="alertFn(1)" :showButton="false"></Verify>
                <div style="margin: 16px;">
                    <!-- 文本提示 -->
                    <p class="link-register" @click="isTitle = !isTitle">{{ isTitle ? '立即注册' : '已有账号,立即登录' }}</p>
                    <van-button round block type="info" native-type="submit">{{ isTitle ? '登录' : '注册' }}</van-button>
                </div>
            </van-form>
        </div>
    </div>
</template>

<script>
import Verify from 'vue2-verify'
export default {
    name: "Login",
    components: { Verify },
    data() {
        return {
            username: "", // 用户名
            password: "", // 密码
            isTitle: true, // 控制登录与注册切换的
        };
    },
    methods: {
        // 点击登录或注册
        onSubmit() {
            console.log("666");
        },
        // 验证码成功或失败
        alertFn(num) {

        },
        // 点击头部的左箭头
        onClickLeft() {

        }
    }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

效果如下:

样式如下:

<style scoped lang="less">
.login {
    height: 100%;

    header {
        width: 100%;
        padding: 0 0.26667rem;
        box-sizing: border-box;
        background: #fff;
        border-bottom: 0.02667rem solid #dcdcdc;
    }

    .logo {
        width: 3.2rem;
        height: 3.2rem;
        display: block;
        margin: 36px auto 0;
    }

    .login-form {
        padding: 0 0.53333rem;

        .van-form {
            display: block;
        }

        /deep/.cerify-code-panel {
            display: flex;
            margin-top: 0.42667rem;
            height: 100%;
            overflow: hidden;

            .verify-code {
                width: 40% !important;
                height: 40px !important;
                line-height: 40px !important;
                font-size: 16px !important;
            }

            .verify-code-area {
                width: 54%;
                margin-left: 0.37333rem;

                .verify-input-area .varify-input-code {
                    width: 2.4rem;
                    height: 1.01333rem;
                    border: 0.02667rem solid #e9e9e9;
                    padding-left: 0.26667rem;
                    font-size: 0.42667rem;
                }

                .verify-change-area {
                    line-height: 1.17333rem;
                    font-size: 12px;
                }
            }
        }
    }

    .link-register {
        float: left;
        font-size: 0.37333rem;
        margin-bottom: 0.53333rem;
        color: #1989fa;
        display: inline-block;
    }

    .van-button--round {
        background: rgb(27, 174, 174) !important;
        border-color: rgb(27, 174, 174) !important;
    }
}
</style> 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

效果如下:

# 六,axios的二次封装,配置代理,API接口的统一管理,注册登录实现

在src目录下,创建api文件夹,在api文件夹下创建http.js,这个http.js是对axios的二次封装,如下:

代码如下:

// http.js

import axios from 'axios'
import {
    Notify
} from 'vant'
import router from "../router"

let http = axios.create({

    //npm run build 时 生产模式,直接请求'http://backend-api-01.newbee.ltd/api/v1'
    //npm run serve 时 开发模式,需要配置代理,请求'/api/v1'
    baseURL: process.env.NODE_ENV == 'production' ? 'http://backend-api-01.newbee.ltd/api/v1' : '/api/v1'
})

//添加请求拦截器

http.interceptors.request.use(function(config) {
    config.headers.token = localStorage.getItem('xftoken')
    return config;
}, function(error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
http.interceptors.response.use(function(response) {
    // 对响应数据做点什么
    let data = response.data;
    if (data.resultCode != 200) {
        if (data.resultCode == 416) {
            // 代表未登录
            // 如果当前路径就是/login 又去使用push 就会报重复跳转的错误
            if (router.currentRoute.path != '/login') {
                router.push('/login?needback=1')
            }
        }
        // 证明后台给的不是我们要的数据

        Notify({
            type: 'danger',
            message: data.message || "系统繁忙"
        });

        return Promise.reject(data);
    }
    return response.data;
}, function(error) {
    // 对响应错误做点什么
    Notify({
        type: 'danger',
        message: "系统繁忙"
    });
    return Promise.reject(error);
});

export default http
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

在vue.config.js配置代理服务器,如下:

const {
    defineConfig
} = require('@vue/cli-service')
module.exports = defineConfig({
    transpileDependencies: true,
    // publicPath:"/xinfengshop",
    publicPath: "./",
    devServer: {
        // proxy:"http://backend-api-01.newbee.ltd"
        proxy: {
            '/api': {
                target: "http://backend-api-01.newbee.ltd"
            }
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在api文件夹下,创建index.js,对api接口进行统一管理,如下:

// index.js

import http from "./http.js"
//引用md5对密码进行加密
import md5 from 'js-md5'

// ======================= 注册接口
// 这样写,在组件中调用接口,就相当于调用方法
export function register(name, pwd) {
    return http.post("/user/register", {
        loginName: name,
        password: pwd
    })
}

// ======================= 登录接口
export function login(name, pwd) {
    return http.post("/user/login", {
        loginName: name,
        passwordMd5: md5(pwd)
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

校验验证码是否难过,逻辑如下:

<script>
    import Verify from 'vue2-verify'
    import {
        register,
        login
    } from "../../api/index.js"
    export default {
        name: "Login",
        components: {
            Verify
        },
        data() {
            return {
                username: "", // 用户名
                password: "", // 密码
                isTitle: true, // 控制登录与注册切换的
                flag: false, // 记录验证码是否通过,flag是true表示验证通过,否则表示没有通过
            };
        },
        methods: {
            // 点击登录或注册
            onSubmit() {
                // 调用接口之前,先验证验证码是否正确
                this.$refs.veri.checkCode();

                // console.log("666");
            },
            // 验证码成功或失败
            alertFn(num) {
                // console.log("--->", num);
                this.flag = num == 1 ? true : false;
            },
            // 点击头部的左箭头
            onClickLeft() {

            }
        }
    };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

如果验证码没有通过,给出提示,如下:

 // 点击登录或注册
 onSubmit() {
     // 调用接口之前,先验证验证码是否正确
     this.$refs.veri.checkCode();

     if (this.flag) {
         // 表示验证码通过
     } else {
         // 表示验证码没有通过
         this.$toast("验证码错误")
         this.$refs.veri.refresh(); // 刷新验证码
     }

     // console.log("666");
 },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果校验通过了,就需要调用接口,实现登录或注册,如下:

import {
    Notify
} from "vant";

onSubmit() {
    // 调用接口之前,先验证验证码是否正确
    this.$refs.veri.checkCode();

    if (this.flag) {
        // 表示验证码通过
        if (this.isTitle) {
            // 登录
            login(this.username, this.password).then(data => {
                if (data.resultCode == 200) {
                    // 登录成功
                    localStorage.setItem("xftoken", data.data)
                    this.$router.replace("/home")
                    Notify({
                        type: 'success',
                        message: "登录成功"
                    });
                }
            })
        } else {
            // 注册
            register(this.username, this.password).then(data => {
                if (data.resultCode == 200) {
                    Notify({
                        type: 'success',
                        message: "注册成功"
                    });
                    this.isTitle = true; // 注册成功后进行登录
                }
            })
        }
        this.$refs.veri.refresh(); // 刷新验证码
    } else {
        // 表示验证码没有通过
        this.$toast("验证码错误")
        this.$refs.veri.refresh(); // 刷新验证码
    }

    // console.log("666");
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

完整代码如下:

// Login.vue

<
template >
    <
    div class = "login" >
    <
    !--头部导航栏-- >
    <
    header >
    <
    van - nav - bar: title = "isTitle ? '登录' : '注册'"
left - arrow @click - left = "onClickLeft" >
    <
    template #right >
    <
    van - icon name = "ellipsis"
size = "18" / >
    <
    /template> < /
van - nav - bar > <
    /header> <!--logo 图片-- > <
img src = "../../assets/logo.png"
width = "100px"
class = "logo"
alt = "" >

    <
    !--登录表单-- >
    <
    div class = "login-form" >
    <
    van - form @submit = "onSubmit" >
    <
    van - field v - model = "username"
name = "用户名"
label = "用户名"
placeholder = "手机号": rules = "[{ required: true, message: '请填写手机号' }]" / >
    <
    van - field v - model = "password"
type = "password"
name = "密码"
label = "密码"
placeholder = "密码": rules = "[{ required: true, message: '请填写密码' }]" / >
    <
    !--验证码-- >
    <
    Verify ref = "veri": type = "2"
@error = "alertFn(2)"
@success = "alertFn(1)": showButton = "false" > < /Verify> <
div style = "margin: 16px;" >
    <
    !--文本提示-- >
    <
    p class = "link-register"
@click = "isTitle = !isTitle" > {
    {
        isTitle ? '立即注册' : '已有账号,立即登录'
    }
} < /p> <
van - button round block type = "info"
native - type = "submit" > {
    {
        isTitle ? '登录' : '注册'
    }
} < /van-button> < /
div > <
    /van-form> < /
div > <
    /div> < /
template >

    <
    script >
    import Verify from 'vue2-verify'
import {
    Notify
} from "vant";
import {
    register,
    login
} from "../../api/index.js"
export default {
    name: "Login",
    components: {
        Verify
    },
    data() {
        return {
            username: "", // 用户名
            password: "", // 密码
            isTitle: true, // 控制登录与注册切换的
            flag: false, // 记录验证码是否通过,flag是true表示验证通过,否则表示没有通过
        };
    },
    watch: {
        isTitle() {
            this.username = "";
            this.password = "";
            this.$refs.veri.refresh();
        }
    },
    methods: {
        // 点击登录或注册
        onSubmit() {
            // 调用接口之前,先验证验证码是否正确
            this.$refs.veri.checkCode();

            if (this.flag) {
                // 表示验证码通过
                if (this.isTitle) {
                    // 登录
                    login(this.username, this.password).then(data => {
                        if (data.resultCode == 200) {
                            // 登录成功
                            localStorage.setItem("xftoken", data.data)
                            this.$router.replace("/home")
                            Notify({
                                type: 'success',
                                message: "登录成功"
                            });
                        }
                    })
                } else {
                    // 注册
                    register(this.username, this.password).then(data => {
                        if (data.resultCode == 200) {
                            Notify({
                                type: 'success',
                                message: "注册成功"
                            });
                            this.isTitle = true; // 注册成功后进行登录
                        }
                    })
                }
                this.$refs.veri.refresh(); // 刷新验证码
            } else {
                // 表示验证码没有通过
                this.$toast("验证码错误")
                this.$refs.veri.refresh(); // 刷新验证码
            }

            // console.log("666");
        },
        // 验证码成功或失败
        alertFn(num) {
            // console.log("--->", num);
            this.flag = num == 1 ? true : false;
        },
        // 点击头部的左箭头
        onClickLeft() {
            this.$router.push("/home")
        }
    }
}; <
/script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

测试OK。

# 七,Home组件顶部处理

代码如下:

<template>
  <div class="home_box">
    <header class="header_box" :class="{ active: !isTop }">
      <div class="left_box"></div>
      <div class="middle_box"></div>
      <div class="right_box"></div>
    </header>
  </div>
</template>

<script>
export default {
  name: "Home",
  data() {
    return {
      isTop: true,
    };
  },
  methods: {
    changeTop() {
      console.log(666);
      let t = document.documentElement.scrollTop || document.body.scrollTop;
      if (t > 50) {
        // 添加背景色
        this.isTop = false;
      } else {
        // 移除背景色
        this.isTop = true;
      }
    },
  },
  mounted() {
    window.addEventListener("scroll", this.changeTop);
  },
  beforeDestroy() {
    window.removeEventListener("scroll", this.changeTop);
  },
};
</script>

<style lang='less' scoped>
.home_box {
  .header_box {
    position: fixed;
    width: 100%;
    height: 50px;
    display: flex;
    z-index: 200;
    top: 0;
    left: 0;
    .left_box {
      width: 50px;
      flex: none;
      background: rgba(0, 0, 0, 0.2);
    }
    .right_box {
      width: 50px;
      flex: none;
      background: rgba(0, 0, 0, 0.2);
    }
    .middle_box {
      flex: auto;
      background: rgba(255, 255, 0, 0.2);
    }
    &.active {
      background: #1baeae;
    }
  }
  .my-swipe .van-swipe-item {
    color: #fff;
    font-size: 20px;
    text-align: center;
    img {
      width: 100%;
      height: auto;
    }
  }
  padding-bottom: 2000px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

效果如下:

# 八,Home首页面数据请求及轮播图模块处理

data数据:

// Home.vue data
data() {
    return {
        isTop: true,
        carousels: [], // 轮播图数据
        hotGoodses: [], //热销商品的数据
        newGoodses: [], // 新品数据
        recommendGoodses: [], // 推荐 数据
    };
},
1
2
3
4
5
6
7
8
9
10

API接口封装:

// api/index.js

// ======================= 首页数据
export function getIndexInfo() {
    return http.get("/index-infos")
}
1
2
3
4
5
6

Home.vue中导入:

import {
    getIndexInfo
} from "../../api/index.js"
1
2
3

发送ajax请求:

mounted() {
        window.addEventListener("scroll", this.changeTop)
        this.getData(); // 发送ajax请求
    },
    methods: {
        // 发送ajax请求
        getData() {
            getIndexInfo().then(data => {
                // console.log("data:", data);
                this.carousels = data.data.carousels;
                this.hotGoodses = data.data.hotGoodses;
                this.newGoodses = data.data.newGoodses;
                this.recommendGoodses = data.data.recommendGoodses;
            })
        },
        changeTop() {
            ....
        }
    },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在调试工具中查看数据如下:

渲染轮播图数据:

<template>
    <div class="home_box">
        <!-- 首页面的头部 -->
        <header class="header_box" :class="{ active: !isTop }">
            <div class="left_box"></div>
            <div class="middle_box"></div>
            <div class="right_box"></div>
        </header>
        <!-- 轮播图 -->
        <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
            <van-swipe-item v-for='item in carousels' :key='item.carouselUrl'>
                <img :src="item.carouselUrl" alt="">
            </van-swipe-item>
        </van-swipe>
    </div>
</template>

<style lang="less" scoped>
.home_box {
    .header_box {
		....
    }

    .my-swipe .van-swipe-item {
        color: #fff;
        font-size: 20px;
        text-align: center;

        img {
            width: 100%;
            height: auto;
        }
    }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

效果如下:

导航模块处理:

<!-- 宫格 -->
<van-grid :column-num="5" :border="false">
    <van-grid-item v-for="value in 10" :key="value" icon="photo-o" text="文字">
        <template #icon>
<div class="icon">ml</div>
        </template>
        <template #text>
<div class="text">严选</div>
        </template>
    </van-grid-item>
</van-grid>

<style lang="less" scoped>
.van-grid-item {
    height: 77px;
    .text {
        font-size: 12px;
        text-align: center;
    }
    .icon {
        color: #1baeae;
        font-size: 40px;
        text-align: center;
        margin-bottom: 7px;
    }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

效果如下:

# 九,封装Goods组件

在main.js中定义一个全局过滤器,如下:

// main.js

// 全局过滤器(对价格保留两位小数)
Vue.filter('money', function(val) {
    return val.toFixed(2)
})
1
2
3
4
5
6

在components文件夹下,创建Goods文件夹,如下:

封装Goods.vue组件,如下:

// Goods.vue
<template>
    <div class="good">
        <div class="good-header">
            <slot name="title">默认标题</slot>
        </div>
        <ul class="good-box">
            <router-link v-for="item in dataItem" class="good-item" tag="li" :to="'/info'">
                <div class="img_box">
                    <img :src="item.goodsCoverImg" alt="">
                </div>
                <div class="good-desc">
                    <div class="title">{{ item.goodsName }}</div>
                    <div class="price">{{ item.sellingPrice | money }}</div>
                </div>
            </router-link>
        </ul>
    </div>
</template>

<script>
export default {
    name: "Goods",
    props: {
        dataItem: {
            type: Array,
            required: true
        }
    },
    data() {
        return {

        };
    },
    methods: {},
};
</script>

<style lang="less">
.good {
    width: 100%;
    background: rgb(243, 243, 243);

    .good-header {
        height: 50px;
        line-height: 50px;
        font-size: 16px;
        font-weight: 500;
        color: rgb(15, 196, 181);
    }

    .good-box {
        width: 100%;
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        align-content: space-between;

        .good-item {
            width: 49%;
            background: white;
            margin: 2px 0;
            box-sizing: border-box;

            .img_box {
                width: 90%;
                height: 200px;
                margin: 10px auto;
                overflow: hidden;
            }

            .good-desc {
                .title {
                    font-size: .32rem;
                }

                .price {
                    font-size: .32rem;
                    color: red;
                }
            }

            img {
                width: 100%;
                height: auto;
            }
        }
    }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

在Home组件中引入并使用之,如下:

<template>
  <div class="home_box">
    <header class="header_box" :class="{ active: !isTop }">
      ...
    </header>
    <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
     ...
    </van-swipe>
    <van-grid :column-num="5" :border="false">
		...
    </van-grid>

    <!-- 新品上线 -->
        <Goods :dataItem='newGoodses'>
            <template #title>
                <div>
                    <h3>新品上线</h3>
                </div>
            </template>
        </Goods>
        <!-- 热销商品 -->
        <Goods :dataItem="hotGoodses">
            <template #title>
                <div>
                    <h3>热销商品</h3>
                </div>
            </template>
        </Goods>
        <!-- 最新推荐 -->
        <Goods :dataItem="recommendGoodses">
            <template #title>
                <div>
                    <h3>最新推荐</h3>
                </div>
            </template>
        </Goods>
  </div>
</template>

<script>
import {
    getIndexInfo
} from "../../api";
import Goods from "../../components/Goods.vue"
export default {
  name: "Home",
  data() {
    return {
     ..
    };
  },
  methods: {
  	...
  },
  components:{
    Goods
  }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

效果如下:

# 十,点击跳转到详情页上

创建详情页组件Info.vue,如下:

对应的代码如下:

// Info.vue
<template>
  <div>
    <h1>Info</h1>
  </div>
</template>

<script>
export default {
  name:"Info",
  data() {
    return {
    };
  }
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

配置路由,如下:

// router/index.js

{
    path: '/info/:productId',
    component: () => import( /*webpackChunkName:"info" */ '../views/Info/Info.vue'),
    meta: {
        isShowNav: false // 控制的是是否展示下边的导航栏
    }
},
1
2
3
4
5
6
7
8
9

点击实现跳转:

// Goods.vue

<router-link v-for="item in dataItem" class="good-item" tag="li" 
             :to="('/info/'+item.goodsId)">
    <div class="img_box">
        <img :src="item.goodsCoverImg" alt="">
    </div>
    <div class="good-desc">
        <div class="title">{{ item.goodsName }}</div>
        <div class="price">{{ item.sellingPrice | money }}</div>
    </div>
</router-link>
1
2
3
4
5
6
7
8
9
10
11
12

封装获取商品详情的API接口,如下:

// api/index.js

// ======================= 根据商品ID获取详情
export function getInfoData(id) {
    return http.get("/goods/detail/" + id)
}
1
2
3
4
5
6

在详情页面中得到得到商品ID,发送ajax请求,获取商品的详情数据,如下:

// Info.vue

<template>
  <div>
    <h1>Info</h1>
  </div>
</template>

<script>
import { getInfoData } from "../../api";
export default {
  name: "Info",
  created() {
    this.getData();
  },
  data() {
    return {
        info: {}, 
    };
  },
   methods: {
    getData() {
      getInfoData(this.$route.params.productId).then((data) => {
        console.log(data);
        this.info = data.data;
      });
    },
  },
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

测试,得到服务器,响应结果,如下:

也就是说,调用此接口,需要在登录的情况下,登录成功后,服务器响应一个token,后面调用其它接口时,需要传递token。

登录成功后,需要存储token,如下:

// Login.vue

onSubmit(values) {
    this.$refs.veri.checkCode();
    if (this.flag) {
        if (this.isLogin) {
            // 证明是要登录的
            console.log("登录....");
            login(this.username, this.password).then((data) => {
                if (data.resultCode == 200) {
                    // 登录成功之后  把token存到本地  跳到首页
                    localStorage.setItem("xftoken", data.data);
                    this.$router.replace("/home");
                }
            });
        } else {
            // 证明是要注册的
            console.log("注册....");
            register(this.username, this.password).then((data) => {
                if (data.resultCode == 200) {
                    // 注册成功之后 跳到登录
                    this.isLogin = true;
                }
            });
        }
    } else {
        this.$refs.veri.refresh();
    }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

在请求拦截器上,带上token,如下:

// http.js

// 添加请求拦截器
http.interceptors.request.use(function(config) {
    // 在发送请求之前做些什么
    config.headers.token = localStorage.getItem('xftoken')
    return config;
}, function(error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});
1
2
3
4
5
6
7
8
9
10
11

登录后,存储token,如下:

再次获取商品详情,如下:

在vue调试工具中,查看如下:

把商品详情数据渲染到页面上:

// Info.vue  样式,各位自己调整一下

<template>
    <div>
        <van-nav-bar title="商品详情" left-arrow @click-left="$router.back()">
            <template #right>
                <van-icon name="ellipsis" />
            </template>
        </van-nav-bar>
        <!-- 商品详情 -->
        <div class="product-info">
            <img style="width: 100%" :src="info.goodsCoverImg" alt="" />
            <div class="info-goodsName">
                {{ info.goodsName }}
            </div>
            <p class="info-p">免邮费 顺丰快递</p>
            <!-- <p>{{ info.goodsIntro }}</p> -->
            <p class="info-price">¥{{ info.sellingPrice }}</p>
            <ul class="info-ul">
                <li>概述</li>
                <li>参数</li>
                <li>安装服务</li>
                <li class="info-ul-li">常见问题</li>
            </ul>
            <div v-html="info.goodsDetailContent" class="info-content"></div>
        </div>
    </div>
</template>

<script>
import { getInfoData } from "../../api/index.js"
export default {
    name: "Info",
    data() {
        return {
            info: {}
        };
    },
    created() {
        this.getData();
    },
    methods: {
        getData() {
            getInfoData(this.$route.params.productId).then(data => {
                console.log("data:", data);
                this.info = data.data
            })
        }
    },

};
</script>

<style  lang="less" scoped>
.van-nav-bar {
    position: fixed;
    width: 100%;
    height: 1.17333rem;
    top: 0;
    left: 0;
    z-index: 100;
    line-height: 1.17333rem;
    padding: 0 0.26667rem;
    box-sizing: border-box;
    background: #fff;
    border-bottom: 0.02667rem solid #dcdcdc;

}

.product-info {
    margin-top: 1.17333rem;
    padding: 0 0.26667rem;

    .info-goodsName {
        font-size: 0.48rem;
        text-align: left;
        color: #333;
    }

    .info-p {
        font-size: 0.37333rem;
        text-align: left;
        color: #999;
        padding: 0.13333rem 0;
    }

    .info-price {
        color: #f63515;
        font-size: 0.58667rem;
        text-align: left;
    }

    .info-ul {
        width: 100%;
        height: 30px;
        margin: 26px 0;
        display: flex;
        justify-content: space-between;

        li {
            flex: 1;
            padding: 0.13333rem 0;
            text-align: center;
            font-size: 0.4rem;
            border-right: 0.02667rem solid #999;
            box-sizing: border-box;
        }
    }

    .info-ul .info-ul-li {
        border-right: none;
    }

    .info-content {
        padding: 0 0.53333rem;
    }
}

.van-goods-action {
    .van-info {
        position: absolute;
        top: -17px;
        right: 13px;
    }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

效果如下:

渲染添加购物车模块,如下:

// Info.vue
<template>
  <div>
    <van-nav-bar title="商品详情" left-arrow @click-left="$router.back()">
      <template #right>
        <van-icon name="ellipsis" />
      </template>
    </van-nav-bar>
    <div>
      .....
    </div>

    <!-- 加入购物车 -->
    <van-goods-action>
        <van-goods-action-icon icon="chat-o" text="客服" dot />
        <van-goods-action-icon icon="cart-o" text="购物车" badge="5" @click="$router.push('/cart')" />
        <van-goods-action-button type="warning" text="加入购物车" @click="add" />
        <van-goods-action-button type="danger" text="立即购买" />
    </van-goods-action>
  </div>
</template>

<script>
import { getInfoData } from "../../api";
export default {
  name: "Info",
  created() {
    this.getData();
  },
  data() {
    return {
      info: {},
    };
  },
  methods: {
    getData() {
      getInfoData(this.$route.params.productId).then((data) => {
        console.log(data);
        this.info = data.data;
      });
    },
    add() {
     
    },
  },
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

效果如下:

封装添加购物车的接口API,如下:

// api/index.js

// ======================= 添加商品到购物车
export function addCart(options) {
    return http.post("/shop-cart", options)
}
1
2
3
4
5
6

在Info.vue中使用之,如下:

// Info.vue

import {
    getInfoData,
    addCart
} from "../../api/index.js"
import {
    Toast
} from "vant";
1
2
3
4
5
6
7
8
9

点击加入购物车,实现add方法,如下:

// Info.vue

data() {
        return {
            info: {}, // 商品详情
            flag: true, // 标识是否已经加入购物车
        };
    },

    // 点击加入购物车
    add() {
        if (!this.flag) return;
        this.flag = false;
        addCart({
            goodsId: this.$route?.params?.productId,
            goodsCount: 1
        }).then(data => {
            this.flag = true;
            Toast.success("添加成功")
        })
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

测试如下:

# 十一,购物车逻辑

没有登录,点击商品详情,出现416,就需要跳到登录页

// http.js

// 添加响应拦截器
http.interceptors.response.use(function(response) {
    // 对响应数据做点什么
    let data = response.data;
    if (data.resultCode != 200) {

        if (data.resultCode == 416) {
            // 代表未登录
            // 如果当前路径就是/login 又去使用push 就会报重复跳转的错误
            if (router.currentRoute.path != '/login') {
                // 416表示没有token,没有认证
                // 放行到登录页,带了一个参数:needback=1
                // 假如我们没有登录,去访问一个商品的详情,得到416,放行到登录页
                // 登录上之后,我们要去上个商品的详情页
                router.push('/login?needback=1')
            }
        }
        // 证明后台给的不是我们要的数据

        Notify({
            type: 'danger',
            message: data.message || "系统繁忙"
        });

        return Promise.reject(data);
    }
    return response.data;
}, function(error) {
    // 对响应错误做点什么
    Notify({
        type: 'danger',
        message: "系统繁忙"
    });
    return Promise.reject(error);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

完善登录,代码如下:

// Login.vue

// 证明是要登录的
login(this.username, this.password).then((data) => {
    if (data.resultCode == 200) {
        // 登录成功之后  把token存到本地  跳到首页
        localStorage.setItem("xftoken", data.data);
        if (this.$route.query.needback == 1) {
            this.$router.back();
        } else {
            this.$router.replace("/home");
        }
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

测试:在没有登录的情况下,去访问商品详情页面,然后直接跳到登录页,登录成功后,直接回到了对应的商品详情页面。测试OK。

封装获取购物车数据的API接口,如下:

// api/index.js

// ======================= 获取购物车列表
export function getCartList() {
    return http.get("/shop-cart")
}
1
2
3
4
5
6

将购物车数据放入到Vuex中,创建对应的仓库,Mutation,Action,Getter,如下:

// store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

import {
    getCartList
} from "../api/index.js"

export default new Vuex.Store({
    state: {
        cartList: [], // 购物车数据
    },
    getters: {},
    mutations: {
        changeCartList(state, list) {
            state.cartList = list.map(item => {
                item.checked = true;
                return item;
            })
        }
    },
    actions: {
        changeCartListAsync({
            commit
        }) {
            getCartList().then(data => {
                // console.log("data:", data);
                commit("changeCartList", data.data || [])
            })
        }
    },
    modules: {}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

在Info.vue中,添加购物车成功后,需要获取最新的购物车数据,如下:

 // 点击加入购物车
 add() {
     if (!this.flag) return;
     this.flag = false;
     addCart({
         goodsId: this.$route?.params?.productId,
         goodsCount: 1
     }).then(data => {
         this.flag = true;
         if (data.resultCode == 200) {
             Toast.success("添加成功")
             this.$store.dispatch("changeCartListAsync")
         } else {
             Toast.fail("商品已存在")
         }
     })
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

此时当我们点击添加商品到购物车,获取购物车的数据,放到vuex中,如下:

然后,我们计算购物车中商品的数量,如下:

 getters: {
     cartNum(state) {
         return state.cartList.reduce((prev, next) => {
             return prev + next.goodsCount
         }, 0)
     }
 },
1
2
3
4
5
6
7

在组件中使用之,如下:

  <!-- 加入购物车 -->
        <van-goods-action>
            <van-goods-action-icon icon="chat-o" text="客服" dot />
            <van-goods-action-icon icon="cart-o" text="购物车" 			 :badge="$store.getters.cartNum"
                @click="$router.push('/cart')" />
            <van-goods-action-button type="warning" text="加入购物车" @click="add" />
            <van-goods-action-button type="danger" text="立即购买" />
        </van-goods-action>
1
2
3
4
5
6
7
8

但是明明购物车中有数据,购物车数字小图标还是显示0,如下:

当我们刷新时,数量又变了0,如下:

原因是我们进入到详情页时,就需要获取购物车数据,如下:

// Info.vue

created() {
    this.getData();
    this.$store.dispatch("changeCartListAsync"); //进来就获取购物车的数据
},
1
2
3
4
5
6

效果如下:

对应的Vuex调试工具,如下:

点击购物车图标,去购物车页,如下:

购物车组件已创建完毕,对应的路由配置完毕。

进入到购物车页,就获取最新的购物车渲染,然后渲染,如下:

// Cart.vue

<template>
    <div>
        <h1>Cart</h1>
    </div>
</template>

<script>
export default {
    name: "Cart",
    created() {
        this.$store.dispatch("changeCartListAsync"); //进来就获取购物车的数据
    },
    computed: {
        // list就是我们手动映射的购物车数据
        list() {
            return this.$store.state.cartList
        }
    },
    data() {
        return {
        };
    }
};
</script>

<style scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

查看在购物车页面,是否可以获取数据,如下:

有了数据,就需要渲染数据,如下:

<template>
    <div>
        <van-nav-bar title="购物车" left-arrow @click-left='$router.back()'>
            <template #right>
                <van-icon name="ellipsis" />
            </template>
        </van-nav-bar>
        <ul>
            <li v-for="(item, i) in list" :key="item.goodsId">
                {{ item.checked }}
                <van-checkbox v-model='item.checked'></van-checkbox>
                <p>{{ item.goodsName }}</p>
                <p>{{ item.sellingPrice }}</p>
                <van-stepper v-model='item.goodsCount' />
            </li>
        </ul>
        <van-submit-bar :price="totalMoney" button-text="提交订单" @submit='$router.push("/addressList")'>
            <van-checkbox v-model="checkAll">全选</van-checkbox>
            <template #tip>
                你的收货地址不支持同城送, <span>修改地址</span>
            </template>
        </van-submit-bar>
    </div>
</template>

<script>
export default {
    name: "Cart",
    created() {
        this.$store.dispatch("changeCartListAsync"); //进来就获取购物车的数据
    },
    computed: {
        // list就是我们手动映射的购物车数据
        list() {
            return this.$store.state.cartList
        },
        // 总共多少钱
        totalMoney() {

        },
        // 全选和反选
        checkAll() {

        }
    },
    data() {
        return {
        };
    }
};
</script>

<style scoped>
.van-submit-bar {
    bottom: 50px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

效果如下:

把购物车的逻辑写一下,如下:

<template>
    <div>
        <van-nav-bar title="购物车" left-arrow @click-left='$router.back()'>
            <template #right>
                <van-icon name="ellipsis" />
            </template>
        </van-nav-bar>
        <ul>
            <li v-for="(item, i) in list" :key="item.goodsId">
                {{ item.checked }}
                <van-checkbox v-model='item.checked'></van-checkbox>
                <p>{{ item.goodsName }}</p>
                <p>{{ item.sellingPrice }}</p>
                <van-stepper v-model='item.goodsCount' />
            </li>
        </ul>
        <van-submit-bar :price="totalMoney" button-text="提交订单" @submit='$router.push("/addressList")'>
            <van-checkbox v-model="checkAll">全选</van-checkbox>
            <template #tip>
                你的收货地址不支持同城送, <span>修改地址</span>
            </template>
        </van-submit-bar>
    </div>
</template>

<script>
export default {
    name: "Cart",
    created() {
        this.$store.dispatch("changeCartListAsync"); //进来就获取购物车的数据
    },
    computed: {
        // list就是我们手动映射的购物车数据
        list() {
            return this.$store.state.cartList
        },
        // 总共多少钱
        totalMoney() {
            // 1号  1块    买了5     5块
            // 2号  2块    买了2     4块
            // 3号  3块    买了10块    30块
            // 总价:5+4+30 = 39
            // 计算总价:计算选中的商品
            return this.list.reduce((prev, cur) => {
                // 按照选中的商品计算的 (checked)
                return (
                    prev + (cur.checked ? cur.goodsCount * cur.sellingPrice * 100 : 0)
                );
            }, 0);
        },
        // 全选和反选
        checkAll: {
            get() {
                return this.list.every(item => item.checked)
            },
            set(val) {
                // console.log(val);
                this.list.forEach(item => item.checked = val)
            }
        }
    },
    data() {
        return {
        };
    }
};
</script>

<style scoped>
.van-submit-bar {
    bottom: 50px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

效果如下:

给Tabbar中的购物车也添加小图标,如下:

// Tabbar.vue

<template>
    <div class="bot_nav_box">
        <router-link to="/home" class="item_box" tag="div">
            <div class="icon iconfont icon-shouye"></div>
            <div class="text">首页</div>
        </router-link>
        <router-link to="/categary" class="item_box" tag="div">
            <div class="icon iconfont icon-grouping"></div>
            <div class="text">分类</div>
        </router-link>
        <router-link to="/cart" class="item_box" tag="div">
            <div class="icon iconfont icon-gouwuche">
                {{ $store.getters.cartNum }}
            </div>
            <div class="text">购物车</div>
        </router-link>
        <router-link to="/user" class="item_box" tag="div">
            <div class="icon iconfont icon-31wode"></div>
            <div class="text">我的</div>
        </router-link>
    </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

效果如下:

# 十二,地址管理

# 1,创建组件,配置路由,配置API接口

创建地址相关组件:

AddressEdit.vue即表示添加地址,也表示编辑地址。

代码如下:

// AddressList.vue

<template>
    <div>AddressList</div>
</template>

<script>
export default {
    name: "AddressList",
    props: [],
    data() {
        return {

        };
    },
    methods: {},
};
</script>

<style lang="less" scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// AddressEdit.vue

<template>
  <div>AddressEdit</div>
</template>

<script>
export default {
  name: "AddressEdit",
  props:[],
  data() {
    return {

    };
  },
  methods: {},
};
</script>

<style lang="less" scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

配置对应的路由:

// router/index.js

{
    path: '/addressList',
    component: () => import( /*webpackChunkName:"info" */ '../views/Address/AddressList.vue'),
    meta: {
        isShowNav: false // 控制的是是否展示下边的导航栏
    }
}, {
    path: '/addressEdit',
    component: () => import( /*webpackChunkName:"info" */ '../views/Address/AddressEdit.vue'),
    meta: {
        isShowNav: false // 控制的是是否展示下边的导航栏
    }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

封装对应的API接口,如下:

// api/index.js

// ======================= 获取地址列表
export function getAddressList() {
    return http.get("/address")
}

// ======================= 根据ID获取某个地址,实现数据回显
export function getAddressDetail(id) {
    return http.get("/address/" + id)
}

// ======================= 编辑地址
export function updateAddress(obj) {
    return http.put("/address", obj)
}

// ======================= 新增地址
export function addAddress(obj) {
    return http.post("/address", obj)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2,发ajax请求获取地址列表数据

在地址列表组件中,发送ajax请求,获取数据,如下:

<template>
    <div>AddressList</div>
</template>

<script>
import { getAddressList } from "../../api/index.js"
export default {
    name: "AddressList",

    props: [],
    data() {
        return {

        };
    },
    created() {
        this.getData();
    },
    methods: {
        // 获取地址数据列表
        getData() {
            getAddressList().then(data => {
                console.log("data==>", data);
            })
        }
    },
};
</script>

<style lang="less" scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

并没有获取到任何数据,因为当前用户没有添加地址,如下:

要么,你先去调用添加地址接口,添加一些数据,使用postman添加一些数据,如下:

在请求头中带个token,如下:

在浏览器中获取地址,如下:

地址列表组件,在vant中,也是提供现成的,如下:

人家的数据格式,如下:

显示地址列表,代码如下:

// AddressList.vue

<template>
    <van-address-list v-model="chosenAddressId" :list="list" default-tag-text="默认" @add="onAdd" @edit="onEdit" />
</template>

<script>
import { getAddressList } from "../../api/index.js"
export default {
    name: "AddressList",

    props: [],
    data() {
        return {
            list: [], // 地址列表的数据源
            chosenAddressId: "1"
        };
    },
    created() {
        this.getData();
    },
    methods: {
        // 点击新增地址
        onAdd() {

        },
        // 点击编辑地址
        onEdit() {

        },
        // 获取地址数据列表
        getData() {
            getAddressList().then(res => {
                this.list = res.data.map(item => {
                    return {
                        id: item.addressId,
                        name: item.userName,
                        tel: item.userPhone,
                        address:
                            item.provinceName +
                            item.cityName +
                            item.regionName +
                            item.detailAddress,
                        isDefault: item.defaultFlag,
                    }
                })

            })
        }
    },
};
</script>

<style lang="less" scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

效果如下:

# 3,点击编辑获取某个地址的详情

点击编辑或添加,要去AddressEdit页面,如下:

// 点击新增地址
onAdd() {
        this.$router.push("/addressEdit");
    },
    // 点击编辑地址
    onEdit(item, index) {
        this.$router.push("/addressEdit?addressId=" + item.id);
    },
1
2
3
4
5
6
7
8

在编辑或新增页面,就可以得到ID,在编辑或新增页面就可以得到ID,如果得到了ID,说明是点击编辑过来的,如果得不到ID,说明是点击新增过来的,如下:

<template>
    <div>AddressEdit</div>
</template>

<script>
import { getAddressDetail } from "../../api/index.js"
export default {
    name: "AddressEdit",
    props: [],
    created() {
        // this是vc 把id挂载到this上,其它地方也可以获取id
        this.id = this.$route.query.addressId
        if (this.id) {
            // 是点击编辑过来的
            // console.log("编辑...");
            // 获取地址详情,实现数据回显
            this.getDetail();
        }
    },
    data() {
        return {

        };
    },
    methods: {
        async getDetail() {
            // 根据ID请求地址详情
            let data = await getAddressDetail(this.id)
            console.log("data===>", data);
        }
    },
};
</script>

<style lang="less" scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

在地址列表页面,点击编辑,得到详情数据,如下:

我们要实现数据回显,你肯定需要一个表单,在vant中,有现在的地址表单,如下:

在上面的组件中,使用到了地区列表,数据需要我们单独安装,如下:

我们已经安装过了,在组件中引入之,如下:

然后使用地址编辑组件,如下:

<template>
    <div>
        <van-address-edit :area-list="areaList" show-postal show-delete show-set-default show-search-result
            :area-columns-placeholder="['请选择', '请选择', '请选择']" @save="onSave" @delete="onDelete" />
    </div>
</template>

<script>
import { getAddressDetail } from "../../api/index.js"
import { areaList } from '@vant/area-data';
export default {
    name: "AddressEdit",
    props: [],
    created() {
        // this是vc 把id挂载到this上,其它地方也可以获取id
        this.id = this.$route.query.addressId
        if (this.id) {
            // 是点击编辑过来的
            // console.log("编辑...");
            // 获取地址详情,实现数据回显
            this.getDetail();
        }
    },
    data() {
        return {
            areaList: Object.freeze(areaList)
        };
    },
    methods: {
        // 点击保存
        onSave() {

        },
        // 点击删除
        onDelete() {

        },
        async getDetail() {
            // 根据ID请求地址详情
            let data = await getAddressDetail(this.id)
            console.log("data===>", data);
        }
    },
};
</script>

<style lang="less" scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

效果如下:

# 4,实现添加地址功能

先实现地址添加,当点击保存时,需要实现地址添加,找到onSave方法,参数就是表单中的数据,如下:

 // 点击保存
 onSave(obj) {
     console.log(obj);
 },
1
2
3
4

打印如下:

但是不能直接去调用添加地址的接口,你要对比调用接口使用的数据和我们获取的数据是否一样的,看一下,调用接口需要传递的数据,如下:

需要把数据,转化成接口需要的数据格式,如下:

import {
    getAddressDetail,
    addAddress
} from "../../api/index.js"
import {
    areaList
} from '@vant/area-data';

// 点击保存
onSave(obj) {
    console.log(obj);
    let {
        addressDetail,
        areaCode,
        city,
        country,
        county,
        id,
        isDefault,
        name,
        postalCode,
        province,
        tel,
    } = obj;

    let option = {
        "cityName": city,
        "defaultFlag": isDefault ? 1 : 0,
        "detailAddress": addressDetail,
        "provinceName": province,
        "regionName": county,
        "userName": name,
        "userPhone": tel,
        "areaCode": areaCode,
        "postalCode": postalCode
    }

    if (this.id) {
        // 点击编辑进来的,点击保存,实现编辑
    } else {
        // 点击添加进来的,点击保存,实现添加
        addAddress({
            ...option
        }).then(() => {
            this.$router.push("/addressList")
        })
    }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

测试如下:

到此,添加地址,就实现了。如果是默认地址,前面勾需要选中,如下:

<template>
    <van-address-list v-model="chosenAddressId" :list="list" default-tag-text="默认" @add="onAdd" @edit="onEdit" />
</template>

<script>
import { getAddressList } from "../../api/index.js"
export default {
    name: "AddressList",

    props: [],
    data() {
        return {
            list: [], // 地址列表的数据源
            chosenAddressId: ""
        };
    },
    async created() {
        await this.getData();
        
        let res = this.list.filter(item => item.isDefault == true)
        this.chosenAddressId = res[0]?.id
    },
    methods: {
        // 点击新增地址
        onAdd() {
            this.$router.push("/addressEdit");
        },
        // 点击编辑地址
        onEdit(item, index) {
            this.$router.push("/addressEdit?addressId=" + item.id);
        },
        // 获取地址数据列表
        async getData() {
            await getAddressList().then(res => {
                this.list = res.data.map(item => {
                    return {
                        id: item.addressId + "",
                        name: item.userName,
                        tel: item.userPhone,
                        address:
                            item.provinceName +
                            item.cityName +
                            item.regionName +
                            item.detailAddress,
                        isDefault: item.defaultFlag ? true : false,
                    }
                })

            })
        }
    },
};
</script>

<style lang="less" scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

效果如下:

# 5,实现编辑功能

然后,实现地址编辑,点击编辑,也是去同一个组件,我们之前,已经得到地址详情信息如下:

我们要实现数据的回显,你的数据格式,需要转化成人家需要的数据格式,同时实现编辑,如下:

<template>
    <div>
        <van-address-edit :address-info="info" :area-list="areaList" show-postal show-delete show-set-default
            show-search-result :area-columns-placeholder="['请选择', '请选择', '请选择']" @save="onSave" @delete="onDelete" />
    </div>
</template>

<script>
import { getAddressDetail, addAddress, updateAddress } from "../../api/index.js"
import { areaList } from '@vant/area-data';
export default {
    name: "AddressEdit",
    props: [],
    created() {
        // this是vc 把id挂载到this上,其它地方也可以获取id
        this.id = this.$route.query.addressId
        if (this.id) {
            // 是点击编辑过来的
            // console.log("编辑...");
            // 获取地址详情,实现数据回显
            this.getDetail();
        }
    },
    data() {
        return {
            areaList: Object.freeze(areaList),
            info: {}, // 实现数据回显
        };
    },
    methods: {
        // 根据地址,得到地区编码
        getAreaCode(area) {
            area = area.replace(/区|县/, "");
            for (let k in areaList.county_list) {
                if (areaList.county_list[k].includes(area)) {
                    return k;
                }
            }
        },
        // 点击保存
        onSave(obj) {
            console.log(obj);
            let { addressDetail, areaCode, city, country, county, id, isDefault, name, postalCode, province, tel,
            } = obj;

            let option = {
                "cityName": city,
                "defaultFlag": isDefault ? 1 : 0,
                "detailAddress": addressDetail,
                "provinceName": province,
                "regionName": county,
                "userName": name,
                "userPhone": tel,
                "areaCode": areaCode,
                "postalCode": postalCode
            }

            if (this.id) {
                // 点击编辑进来的,点击保存,实现编辑
                // 编辑也需要把地址ID也传递给后端
                updateAddress({
                    addressId: this.id,
                    ...option
                }).then(() => {
                    this.$router.push("/addressList")
                })
            } else {
                // 点击添加进来的,点击保存,实现添加
                addAddress({ ...option }).then(() => {
                    this.$router.push("/addressList")
                })
            }
        },
        // 点击删除
        onDelete() {

        },
        async getDetail() {
            // 根据ID请求地址详情
            let data = await getAddressDetail(this.id)
            // console.log("data===>", data);
            let {
                addressId,
                cityName,
                defaultFlag,
                detailAddress,
                provinceName,
                regionName,
                userId,
                userName,
                userPhone,
            } = data.data;
            this.info = {
                id: addressId,
                name: userName,
                tel: userPhone,
                province: provinceName,
                city: cityName,
                county: regionName,
                addressDetail: detailAddress,
                postalCode: "100000",
                // 给你一个地址,需要得这个地区的编码
                areaCode: this.getAreaCode(regionName),
                isDefault: defaultFlag ? true : false,
            };
            console.log(this.info);
        }
    },
};
</script>

<style lang="less" scoped>
/deep/.van-switch--on {
    background-color: #fa1919;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

效果如下:

点击保存,如下:

# 十三,订单管理

# 1,在生成订单页面显示地址

当我们点击某个地址后,去生成订单页面,如下:

点击生成订单,如下:

点击支付,就支付成功了,到达订单页面,如下:

我需要准备两个页面,一个是生成订单页面,一个是我的订单页面,创建生成订单页面和我的订单页面,如下:

对应的代码如下:

// CraeteOrder.vue

<template>
    <div>
        CreateOrder
    </div>
</template>

<script>
export default {
    name: "CreateOrder",
    props: [],
    data() {
        return {

        };
    },
    methods: {},
};
</script>

<style lang="less" scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Order.vue

<template>
  <div>
    Order
  </div>
</template>

<script>
export default {
  name: "Order",
  props:[],
  data() {
    return {

    };
  },
  methods: {},
};
</script>

<style lang="less" scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

配置对应的路由如下:

// router/index.js

{
    path: '/create-order',
    component: () => import( /*webpackChunkName:"createOrder" */ '../views/CreateOrder/CreateOrder.vue'),
    meta: {
        isShowNav: false // 控制的是是否展示下边的导航栏
    }
}, {
    path: '/order',
    component: () => import( /*webpackChunkName:"order" */ '../views/Order/Order.vue'),
    meta: {
        isShowNav: false, // 控制的是是否展示下边的导航栏
        til: "订单页"
    }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

修改title,如下:

// router/index.js

router.beforeEach((to, from, next) => {
    document.title = to.meta.til || "码路严选"
    next()
})
1
2
3
4
5
6

生成订单页面,准备对应的接口,如下:

//===================获取生成订单的商品
export function getOrderCard(id) {
    return http.get('/shop-cart/settle?cartItemIds=' + id)
}
1
2
3
4

效果图如下:

我们是点击地址列表中的某个地址,才去的生成订单页面,点击不同的地址,chosenAddressId是变化,我们就可以监听chosenAddressId变化,如果变化,就要去生成订单页面,代码如下:

// AddressList.vue

watch: {
    chosenAddressId(val) {
        console.log("val:", val);
        // 跳到生成订单页面
        this.$router.push({
            path: "/create-order",
            query: {
                addressId: val,
            }
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在生成订单页面,获取ID,根据ID发送ajax请求,获取地址信息,如下:

<template>
    <div>
        <van-nav-bar title="生成订单" left-arrow @click-left="$router.back()">
            <template #right>
                <van-icon name="ellipsis" size="18" />
            </template>
        </van-nav-bar>
        <!-- 回显地址 -->
        <router-link to="/addressList">
            <div class="address-wrap">
                <div class="name">
                    <span>{{ this.name }}</span>
                    <span>{{ this.tel }}</span>
                </div>
                <van-icon name="arrow" />
                <div class="address">
                    <span>{{ this.address }}</span>
                </div>
            </div>
        </router-link>
    </div>
</template>

<script>
import { getAddressDetail } from "../../api/index.js"
export default {
    name: "CreateOrder",
    props: [],
    created() {
        this.addressId = this.$route.query.addressId;
        this.getAddress();
    },
    data() {
        return {
            name: "", // 收货人姓名
            tel: "", // 手机号
            address: "", // 地址
        };
    },
    methods: {
        // 获取某个地址数据
        async getAddress() {
            // 根据ID请求地址详情
            let data = await getAddressDetail(this.addressId)
            this.name = data.data.userName;
            this.tel = data.data.userPhone;
            this.address =
                data.data.provinceName +
                data.data.cityName +
                data.data.regionName +
                data.data.detailAddress;
        }
    },
};
</script>

<style lang="less" scoped>
.van-nav-bar {
    position: fixed;
    width: 100%;
    height: 1.17333rem;
    top: 0;
    left: 0;
    z-index: 100;
    line-height: 1.17333rem;
    padding: 0 0.26667rem;
    box-sizing: border-box;
    background: #fff;
    border-bottom: 0.02667rem solid #dcdcdc;
}

.address-wrap {
    margin-top: 1.17333rem;
    margin-bottom: 0.53333rem;
    background: #fff;
    position: relative;
    font-size: 0.37333rem;
    padding: 0.63rem 0.4rem 0.66667rem;
    color: #222333;

    .name {
        display: flex;
        justify-content: left;
        margin: 0.26667rem 0 0.27667rem;
        line-height: 0.37333rem;

        span:nth-child(1) {
            margin-right: 0.04rem;
        }
    }

    .address {
        display: flex;
        justify-content: left;
        line-height: 0.37333rem;
    }

    .van-icon {
        position: absolute;
        right: 0.26667rem;
        top: 50%;
        -webkit-transform: translateY(-50%);
        transform: translateY(-50%);
        font-size: 0.53333rem;
    }
}

.address-wrap::before {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 0.05333rem;
    background: -webkit-repeating-linear-gradient(135deg,
            #ff6c6c,
            #ff6c6c 20%,
            transparent 0,
            transparent 25%,
            #1989fa 0,
            #1989fa 45%,
            transparent 0,
            transparent 50%);
    background: repeating-linear-gradient(-45deg,
            #ff6c6c,
            #ff6c6c 20%,
            transparent 0,
            transparent 25%,
            #1989fa 0,
            #1989fa 45%,
            transparent 0,
            transparent 50%);
    background-size: 2.13333rem;
    content: "";
}

.van-card__content {
    display: flex;
    justify-content: center;

    .van-card__title {
        text-align: left;
    }

    .van-multi-ellipsis--l2 {
        margin-bottom: 22px;
    }

    .van-card__bottom {
        display: flex;
        justify-content: space-between;

        span {
            color: red;
        }
    }
}

.van-submit-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    background: #fff;
    padding: 0.26667rem 0;
    padding-bottom: 1.33333rem;
    border-top: 0.02667rem solid #e9e9e9;

    .van-submit-bar__text {
        display: flex;
        -webkit-box-pack: justify;
        -webkit-justify-content: space-between;
        justify-content: space-between;
        padding: 0 5%;
        margin: 0.26667rem 0;
        font-size: 0.37333rem;
    }

    .van-button {
        position: fixed;
        bottom: 0.18667rem;
        right: 0;
        left: 0;
        width: 90%;
        margin: 0 auto;
        border-radius: 5px;
    }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188

上面的样式中本页面中所有的样式,效果如下:

在点击地址时,有一个bug,点击地址后,去地址列表,但是又回到了生成订单页面,原因是去地址列表时,修改了chosenAddressId,给chosenAddressId赋值,被监听到了,又跳到生成订单页面了,解决办法,就是不使用watch,给地址列表绑定点击事件,点击时进行跳转,如下:

<template>
    <van-address-list v-model="chosenAddressId"   ...   @select="selectHandle" />
</template>

<script>
     methods: {
        // 点击某个地址 
        selectHandle(item, index) {
            console.log("item:", item);
            this.$router.push({
                path: "/create-order",
                query: {
                    addressId: item.id,
                }
            });
        }
     }
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

测试OK。

# 2,完善购物车模块

因为在购物车中的数据,在生成订单模块中也需要显示。之前,我们把购物车的数据是存储到了vuex中。如下:

现在先去完善一下,先把购物车页面,完善一下,如下:

<template>
    <div>
        <van-nav-bar title="购物车" left-arrow @click-left='$router.back()'>
            <template #right>
                <van-icon name="ellipsis" />
            </template>
        </van-nav-bar>
        <div class="cart-box">
            <!-- 购物车中有数据,显示购物车 -->
            <van-swipe-cell v-for="item in list" :key="item.goodsId" v-show="list.length">
                <van-checkbox v-model="item.checked"></van-checkbox>
                <van-card class="goods-card" :thumb="item.goodsCoverImg" :title="item.goodsName"
                    :price="item.sellingPrice" />
                <van-stepper v-model="item.goodsCount" @change="change(item)" />
                <template #right>
                    <van-button square text="删除" type="danger" class="delete-button"
                        @click="deleteClick(item.cartItemId)" />
                </template>
            </van-swipe-cell>
            <!-- 购物车没有东西,显示出来 -->
            <div class="empty" v-show="!list.length">
                <i class="van-icon van-icon-smile-o"></i>
                <div class="title">购物车空空空如也</div>
                <button @click="homeClick" class="
            van-button van-button--primary van-button--normal van-button--block
          " style="color: rgb(255, 255, 255); background: rgb(27, 174, 174)">
                    <span class="van-button__text">前往首页</span>
                </button>
            </div>
        </div>

        <!-- <ul>
            <li v-for="(item, i) in list" :key="item.goodsId">
                {{ item.checked }}
                <van-checkbox v-model='item.checked'></van-checkbox>
                <p>{{ item.goodsName }}</p>
                <p>{{ item.sellingPrice }}</p>
                <van-stepper v-model='item.goodsCount' />
            </li>
        </ul> -->
        <van-submit-bar :price="totalMoney" button-text="结算" @submit='onSubmit' v-show="list.length">
            <van-checkbox v-model="checkAll">全选</van-checkbox>
            <template #tip>
                你的收货地址不支持同城送, <span>修改地址</span>
            </template>
        </van-submit-bar>
    </div>
</template>

<script>
export default {
    name: "Cart",
    created() {
        this.$store.dispatch("changeCartListAsync"); //进来就获取购物车的数据
    },
    computed: {
        // list就是我们手动映射的购物车数据
        list() {
            return this.$store.state.cartList
        },
        // 总共多少钱
        totalMoney() {
            // 1号  1块    买了5     5块
            // 2号  2块    买了2     4块
            // 3号  3块    买了10块    30块
            // 总价:5+4+30 = 39
            // 计算总价:计算选中的商品
            return this.list.reduce((prev, cur) => {
                // 按照选中的商品计算的 (checked)
                return (
                    prev + (cur.checked ? cur.goodsCount * cur.sellingPrice * 100 : 0)
                );
            }, 0);
        },
        // 全选和反选
        checkAll: {
            get() {
                return this.list.every(item => item.checked)
            },
            set(val) {
                // console.log(val);
                this.list.forEach(item => item.checked = val)
            }
        }
    },
    methods: {
        // 点击结算
        onSubmit() {
            $router.push("/addressList")
        },
        // 点击去首页面
        homeClick() {

        },
        // 点击步进器
        change() {

        },
        // 点击删除按钮
        deleteClick() {

        }
    },
    data() {
        return {
        };
    }
};
</script>

<style scoped lang="less">
.van-nav-bar {
    position: fixed;
    width: 100%;
    height: 1.17333rem;
    top: 0;
    left: 0;
    z-index: 100;
    line-height: 1.17333rem;
    padding: 0 0.26667rem;
    box-sizing: border-box;
    background: #fff;
    border-bottom: 0.02667rem solid #dcdcdc;
}

.cart-box {
    margin: 1.6rem 0 2.66667rem 0;
    padding-left: 0.26667rem;

    // 购物车为空时,样式
    .empty {
        width: 50%;
        margin: 0 auto;
        text-align: center;
        margin-top: 5.33333rem;

        .van-icon-smile-o {
            font-size: 1.33333rem;
        }

        .title {
            font-size: 0.42667rem;
            margin-bottom: 0.53333rem;
        }

        .van-button--block {
            display: block;
            width: 100%;
        }

        .van-button--normal {
            padding: 0 0.4rem;
            font-size: 0.37333rem;
        }

        .van-button--primary {
            border: 0.02667rem solid #07c160;
        }
    }
}

.van-swipe-cell {
    overflow: hidden;
    position: relative;

    .van-stepper {
        position: absolute;
        bottom: 0;
        right: 20px;
    }

    .van-checkbox {
        width: 20px;
        height: 100%;
        position: absolute;
        top: 0;
        left: 0;
    }

    .van-card {
        padding: 0;
        margin-left: 20px;

        .van-card__content {
            width: 245px;
            height: 100%;
            justify-content: center;

            .van-card__bottom {
                text-align: left;
                margin-top: 7px;

                span {
                    color: red;
                }
            }
        }
    }

    .delete-button {
        height: 100%;
    }
}

.van-submit-bar {
    bottom: 50px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208

效果如下:

把里在的功能完善一下,删除购物车,准备两个接口,如下:

// ======================= 删除购物车列表
export function deleteCartList(id) {
    return http.delete('/shop-cart/' + id)
}

// ======================= 步进器
export function putCartList(id, gt) {
    return http.put('/shop-cart/', {
        cartItemId: id,
        goodsCount: gt
    })
}
1
2
3
4
5
6
7
8
9
10
11
12

在组件中引入这两个接口,如下:

// Cart.vue

import { deleteCartList, putCartList } from "../../api";
1
2
3

实现删除,如下:

// 点击删除按钮
deleteClick(val) {
    // val表示你要删除的商品ID
    // console.log(val);
    deleteCartList(val).then(data => {
        console.log(data);
        if (data.resultCode == 200) {
            this.$store.dispatch("changeCartListAsync");
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11

测试是OK的。

实现步进器,当点击步进器,得到商品信息,里面包含商品的数量,如下:

代码如下:

 // 点击步进器
 async change(val) {
     console.log(val);
     let res = await putCartList(val.cartItemId, val.goodsCount);
     if (data.resultCode == 200) {
         this.$store.dispatch("changeCartListAsync");
     }
 },
1
2
3
4
5
6
7
8

效果如下:

再看一下,vuex中的购物车的数据,如下:

定义一个状态cartItemIds,当点击结算时,把选中的商品存储到cartItemIds,然后,把cartItemIds存储到localStorage中,如下:

data() {
    return {
        cartItemIds: [], // 存储选中的商品中
    };
}

// 点击结算
onSubmit() {
    this.list.filter(item => {
        if (item.checked) {
            this.cartItemIds.push(item.cartItemId)
        }
    })
    // 存储的是数组,之所以看到的是字符串,原因是调用toStirng
    localStorage.setItem("cartItemIds", [1, 2, 3]);
    this.cartItemIds = [];
    this.$router.push("/addressList")
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

然后,我们去点击结算,直接到了生成订单页面,如下:

# 3,渲染商品数据

在生成订单页面中,就可以发送ajax请求,获取数据,如下:

// CreateOrder.vue

import {
    getAddressDetail,
    getOrderCard
} from "../../api/index.js"

created() {
        ....
        this.getOrder();
    },

    data() {
        return {
            ...
            orderList: [], // 购物车中选中的商品
            cartItemIds: [], // 购物车中选中商品的ID
        };
    },

    // 获取购物车中选中的商品
    getOrder() {
        getOrderCard(localStorage.getItem("cartItemIds")).then(data => {
            this.orderList = data.data;
        })
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

看一下,数据是否回来,如下:

渲染商品卡片,如下:

<!-- 商品卡片 -->
<van-card v-for="item in orderList" :key="item.cartItemId" :num="item.goodsCount" :price="item.sellingPrice" :title="item.goodsName" :thumb="item.goodsCoverImg" />
1
2

效果如下:

然后,渲染如下模块,如下:

上面的内容在vant中也是有现在的组件,如下:

代码如下:

 <!-- 生成订单 -->
 <
 van - submit - bar: price = "totalMoney"
 label = "商品金额"
 button - text = "提交订单"
 @submit = "onSubmit" / >
1
2
3
4
5
6

效果如下:

计算商品总价,如下:

computed: {
    totalMoney() {
        return this.orderList.reduce((prev, next) => {
            return (
                prev + next.goodsCount * next.sellingPrice * 100
            );
        }, 0);
    },
},
1
2
3
4
5
6
7
8
9

效果如下:

# 4,我的订单

提交订单时,就需要创建一个订单,如下:

给后端传递的数据,如下:

准备一个接口,如下:

找到提交按钮,绑定点击事件,准备要传递给服务器的数据,如下:

浏览器测试之,如下:

调用接口,如下:

调用之,如下:

服务器响应的数据如下:

把订单号存储起来,如下:

显示支付面板,如下:

当点击提交订单时,就需要让支付面板显示,如下:

点击不同的支付,需要调用接口,封装接口如下:

在组件中使用之,如下:

调用之,如下:

引入Toast,如下:

测试之,如下:

到达了我的订单页面,如下:

获取所有的订单,封装对应的接口如下:

分析list组件,如下:

绘制页面,如下:

<template>
  <div class="order-box">
    <van-nav-bar fixed title="我的订单" left-arrow @click-left="$router.back()">
      <template #right>
        <van-icon name="ellipsis" size="18" />
      </template>
    </van-nav-bar>

    <van-tabs>
      <van-tab v-for="item in tabs" :key="item.text" :title="item.text"></van-tab>
    </van-tabs>

    <!-- 订单列表 -->
    <van-list :offset="80" v-model="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
      <van-cell v-for="item in list" :key="item" :title="item" />
    </van-list>

  </div>
</template>

<script>
export default {
  name: "Order",
  props: [],
  data() {
    return {
      tabs: [
        {
          text: "全部",
          status: "",
        },
        {
          text: "待付款",
          status: "0",
        },
        {
          text: "待确认",
          status: "1",
        },

        {
          text: "代发货",
          status: "2",
        },
        {
          text: "已发货",
          status: "3",
        },
        {
          text: "交易完成",
          status: "4",
        },
      ],
      list: [],  // 列表中的数据
      loading: false,  // 
      finished: false, // 是否加载完毕
    };
  },
  methods: {
    onLoad() {
      console.log("onload...");
      // 模拟ajax
      setTimeout(() => {
        for (let i = 0; i < 10; i++) {
          this.list.push(this.list.length + 1);
        }

        // 加载状态结束
        // 当loading为false时,还会触发onLoad
        this.loading = false;

        // 数据全部加载完成
        if (this.list.length >= 40) {
          this.finished = true;
        }
      }, 1000);
    }
  },
};
</script>

<style lang="less" scoped>
.order-box {
  .van-tabs {
    position: fixed;
    top: 1.2rem;
    width: 100%;
    left: 0;
    z-index: 300
  }

  .van-list {
    padding-top: 2.2rem;
  }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

效果如下:

在页面中导入接口,如下:

发送ajax请求,获取数据,如下:

得到服务器响应的数据,如下:

看一下,list状态中有没有数据,如下:

list有数据了,要渲染数据,如下:

效果如下:

点击tabbar,需要获取不同状态的订单,给van-tabs绑定index,如下:

准备状态如下:

监听index变化,如下:

效果如下:

如果index变了,需要重新发请求,如下:

测试如下:

# 十四,我的管理

封装对应的API接口,如下:

// ================= 获取用户信息
export function getUserInfo() {
    return http.get('/user/info')
}
1
2
3
4
// User/User.vue

<template>
  <div>
    {{info}}
    我的页
  </div>
</template>

<script>
import { getUserInfo } from "../../api";
export default {
  data() {
    return {
      info: {},
    };
  },
  created() {
    this.getData();
  },
  methods: {
    async getData() {
      let data = await getUserInfo();
      console.log(data);
      this.info = data.data;
    },
  },
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

效果如下:

其它的API接口,如下:

// ================================================= 其它接口

// ================= 点击确认收货
export function OkOrderDetails(orderNo) {
    return http.put('/order/' + orderNo + '/finish')
}
// ================= 点击取消订单
export function NotOkOrderDetails(orderNo) {
    return http.put('/order/' + orderNo + '/cancel')
}

// ================= 点击订单跳转到确认收货页面拿数据
export function getOrderDetails(id) {
    return http.get('/order/' + id)
}
// ================= 保存更改
export function save(introduceSign, nickName, pwd) {
    return http.put('/user/info', {
        introduceSign: introduceSign,
        nickName: nickName,
        passwordMd5: md5(pwd)
    })
}
// ================= 退出登录
export function infos() {
    return http.get('/index-infos')
}
// ================= 点击搜索
export function search(pn, kw, ob) {
    return http.get('/search', {
        params: {
            pageNumber: pn,
            keyword: kw,
            orderBy: ob
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Last Updated: 12/25/2022, 10:02:14 PM