11-Vue3.x新版码路严选

11/17/2001

# 一,项目准备工作

  • 前台接口:https://www.apifox.cn/apidoc/shared-a11f6c11-eaa8-456a-85ae-0e2a97cb561c

  • 中后台接口:https://www.apifox.cn/apidoc/shared-38c2b1bf-75e7-4146-a849-507798f38fdb

# 1,创建项目

创建项目命令如下:

1677501305754

进入项目,安装依赖,运行项目,如下:

1677501524925

在浏览器中测试之,如下:

1677501566742

# 2,配置vant

**vant的地址:**https://vant-contrib.gitee.io/vant/#/zh-CN

  • 第一步:安装最新版的vant,安装自动引入插件,如下:

1677501675453

1677501722468

在vite.config.js中配置,如下:

1677501995886

Vant 中有个别组件是以函数的形式提供的,包括 ToastDialogNotifyImagePreview 组件。在使用函数组件时, unplugin-vue-components 无法自动引入对应的样式,因此需要手动引入样式。引入如下:

1677502079148

测试vant中的组件是否可以使用,如下:

1677502240807

浏览器中测试如下:

1677502259148

# 3,配置scss

安装:

1677502662224

测试如下:

1677502684672

浏览器中测试效果如下:

1677502699928

# 4,配置rem移动端适配

安装对应的依赖,如下:

1677502845074

创建postcss.config.js,并配置如下:

1677502904273

参考代码:

module.exports = {
    plugins: {
        /**
         * 自动px转rem
         */
        'postcss-pxtorem': {
            // 一个元素是75px   ===>   2rem
            rootValue: 37.5,
            propList: ['*'],
        },
        /**
         * 自动配置浏览器样式
         */
        'postcss-preset-env': {},
    },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

适配的js,如下:

1677503008384

参考代码如下:

// 首先是一个立即执行函数,执行时传入的参数是window和document
(function flexible(window, document) {
    var docEl = document.documentElement; // 返回文档的root元素
    var dpr = window.devicePixelRatio || 1;
    // 获取设备的dpr,即当前设置下物理像素与虚拟像素的比值
    // 调整body标签的fontSize,fontSize = (12 * dpr) + 'px'
    // 设置默认字体大小,默认的字体大小继承自body
    function setBodyFontSize() {
        if (document.body) {
            document.body.style.fontSize = 12 * dpr + 'px';
        } else {
            document.addEventListener('DOMContentLoaded', setBodyFontSize);
        }
    }
    setBodyFontSize();
    // set 1rem = viewWidth / 10
    // 设置root元素的fontSize = 其clientWidth / 10 + ‘px’
    function setRemUnit() {
        var rem = docEl.clientWidth / 10;
        docEl.style.fontSize = rem + 'px';
    }
    // 移动端的适配如何做
    // (1): 所有的css单位, rem    (vscode可以自动把px转成rem, pxtorem插件设置基准值37.5) - 1rem等于37.5px
    //  原理: rem要根据html的font-size换算
    //  目标: 网页宽度变小, html的font-size也要变小, ...网页变大, html的font-size变大.
    // (2): flexible.js (专门负责当网页宽度改变, 会修改html的font-size)
    setRemUnit();
    // 当我们页面尺寸大小发生变化的时候,要重新设置下rem 的大小
    window.addEventListener('resize', setRemUnit);
    // pageshow 是我们重新加载页面触发的事件
    window.addEventListener('pageshow', function(e) {
        // e.persisted 返回的是true 就是说如果这个页面是从缓存取过来的页面,也需要从新计算一下rem 的大小
        if (e.persisted) {
            setRemUnit();
        }
    });
    // 检测0.5px的支持,支持则root元素的class中有hairlines
    if (dpr >= 2) {
        var fakeBody = document.createElement('body');
        var testElement = document.createElement('div');
        testElement.style.border = '.5px solid transparent';
        fakeBody.appendChild(testElement);
        docEl.appendChild(fakeBody);
        if (testElement.offsetHeight === 1) {
            docEl.classList.add('hairlines');
        }
        docEl.removeChild(fakeBody);
    }
})(window, document);
export {};
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

重置样式,如下:

1677503064792

参考重置样式,如下:

body,
div,
dl,
dt,
dd,
ul,
ol,
li,
h1,
h2,
h3,
h4,
h5,
h6,
pre,
form,
fieldset,
input,
textarea,
p,
blockquote,
th,
td {
    padding: 0;
    margin: 0;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}

fieldset,
img {
    border: 0;
}

address,
caption,
cite,
code,
dfn,
em,
strong,
th,
var {
    font-weight: normal;
    font-style: normal;
}

ol,
ul {
    list-style: none;
}

caption,
th {
    text-align: left;
}

h1,
h2,
h3,
h4,
h5,
h6 {
    font-weight: normal;
    font-size: 100%;
}

q:before,
q:after {
    content: '';
}

abbr,
acronym {
    border: 0;
}
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中引入适配js和重置样式,如下:

1677503103753

测试rem适配是否起作用,如下:

1677503559210

浏览器中测试如下:

1677503532929

# 5,配置全局样式

创建全局样式,如下:

1683596669081

参考代码如下:

html,
body,
#app {
  height: 100%;
  width: 100%;
  background: #F6F6F6;
  // vant自带变量
  --van-card-background: #fff;
}
1
2
3
4
5
6
7
8
9

在main.js中引入全局样式,如下:

1683596709630

效果如下:

1683596751181

# 6,配置jsconfig

配置jsconfig.json,开启路径别名提示,如下:

1677503655905

参考代码如下:

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Node",
    "target": "ESNext",
    "jsx": "preserve",
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "baseUrl": ".",
    "allowJs": true,
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  },
  "include": [
    "src/**/*",
    "src/**/*.vue",
  ],
  "exclude": [
    "node_modules",
    "**/node_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

测试如下:

1677503714474

# 二,创建基本组件并配置路由

# 1,创建基本组件并配置路由

删除views下面的代码,并创建对应的组件,如下:

1677504212426

配置对应的路由,如下:

1677504245691

参考代码如下:

import {
    createRouter,
    createWebHashHistory
} from 'vue-router';

const router = createRouter({
    history: createWebHashHistory(
        import.meta.env.BASE_URL),
    routes: [{
            path: '/',
            name: 'root',
            redirect: '/home',
        },
        {
            path: '/home',
            name: 'home',
            component: () => import('@/views/Home/index.vue'),
            meta: {
                // 显示导航
                isShowNav: true,
            },
        },
        {
            path: '/recommend',
            name: 'recommend',
            component: () => import('@/views/Recommend/index.vue'),
            meta: {
                isShowNav: true,
                // 需要登录的
                login: true,
                title: '种草推荐',
            },
        },
        {
            path: '/cart',
            name: 'cart',
            component: () => import('@/views/Cart/index.vue'),
            meta: {
                isShowNav: true,
                title: '购物车',
            },
        },
        {
            path: '/user',
            name: 'user',
            component: () => import('@/views/User/index.vue'),
            meta: {
                isShowNav: true,
                title: '我的',
            },
        },
        // 访问没有的路由直接跳往首页
        {
            path: '/:toHome*',
            name: 'toHome',
            redirect: '/home',
        },
    ],
});

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
53
54
55
56
57
58
59
60
61

删除components下面的代码文件,创建Tabbar组件,如下:

1677504298195

在App.vue中引入,使用之,如下:

1677504325083

注释掉创建项目时的默认样式,如下:

1683599269090

浏览器测试如下:

1677504385045

# 三,首页面绘制

# 1,配置axios

安装axios,如下:

1677504636448

创建utils文件夹,在utils文件夹下创建request.js,如下:

1683601483918

baseURL:"http://8.218.112.99",

# 2,配置api接口

创建api文件夹,封装首页面的api接口,如下:

1677504781680

参考代码如下:

import request from '@/utils/request';

/**
 * @description 获取首页商品数据
 * @param {{keyword?: string,limit:number,page:number,sort?:"price_up"|"price_down"}} queryObj
 */
export const getProductList = queryObj =>
    request({
        method: 'POST',
        url: '/frontend/goods/list',
        params: queryObj,
    });

/**
 * @description 获取轮播图数据
 * @param {{limit:number,page:number}} queryObj
 */
export const getCarouselChartData = queryObj =>
    request({
        method: 'POST',
        url: '/frontend/rotation/list',
        data: queryObj,
    });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3,绘制轮播图数据

先解决跨域问题:

1683602263701

参考代码如下:

server: {
    proxy: {
        "/api": {
            target: "http://8.218.112.99",
            changeOrigin: true,
            rewrite: (path) => path.replace(/^\/api/, ""),
        },
    },
},
1
2
3
4
5
6
7
8
9

分析了接口地址如下:

1683602316789

改变baseURL,如下:

1683602344725

测试数据数据如下:

1683602372165

在首页面中获取数据,渲染数据,如下:

1677505296818

打开调试工具,如下:

1683602629699

参考代码如下:

<script setup>
import { onMounted, ref } from 'vue';
import { getCarouselChartData } from '@/api/home.js';

// 轮播图数据
const carouselChartData = ref([]);

onMounted(async () => {
  // 获取轮播图数据
  try {
    carouselChartData.value =
      (await getCarouselChartData({ limit: 10, page: 1 })).data.list || [];
  } catch (error) {
    console.error(error);
  }
});
</script>

<template>
  <div>
    <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
      <van-swipe-item v-for="item in carouselChartData" :key="item.id">
        <img :src="item.pic_url" :alt="item.pic_url" />
      </van-swipe-item>
    </van-swipe>
  </div>
</template>

<style lang="scss" scoped>
.my-swipe .van-swipe-item {
  width: 100%;
  height: 168px;
  img {
    width: 100%;
  }
}
</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

浏览器中测试如下:

1677505325532

# 4,渲染首页面中的商品列表

在components目录下创建Goods组件,并书写代码,如下:

1677505453975

参考代码如下:

<script setup>
const props = defineProps({
  dataItem: {
    type: Array,
    default: [],
    required: true,
  },
});
</script>

<template>
  <div class="good">
    <div class="good-header">
      <slot name="title">默认标题</slot>
    </div>
    <ul class="good-box">
      <router-link v-for="item in props.dataItem" :key="item.id" class="good-item" tag="li" :to="`/goods/${item.id}`">
        <div class="img_box">
          <img :src="item.pic_url" :alt="item.goodsName" />
        </div>
        <div class="good-desc">
          <div class="title">{{ item.name }}</div>
          <div class="price">
            {{ (item.price / 100).toFixed(2) }}
          </div>
        </div>
      </router-link>
    </ul>
  </div>
</template>

<style lang="scss" scoped>
.good {
  width: 100%;
  background: rgb(243, 243, 243);
  margin-bottom: 50px;

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

  .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: 188px;
        margin: 10px auto;
        overflow: hidden;
      }

      .good-desc {
        text-align: center;

        .title {
          font-size: 0.378rem;
          color: #222333;
          -webkit-line-clamp: 2;
          display: -webkit-box;
          -webkit-box-orient: vertical;
          overflow: hidden;
          text-overflow: ellipsis;
          word-wrap: break-word;
        }

        .price {
          font-size: 0.32rem;
          color: #1baeae;
          margin: 6px 0;
        }
      }

      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
91
92
93
94
95

在首页面中使用Goods组件,如下:

1677505682562

1677505706162

效果如下:

1677505728187

开启请求显示进度条,安装对应的依赖,如下:

1677505889052

在路由前置守卫中开启,在后置守卫中关闭,如下:

1677505984073

浏览器中测试如下:

1677506043615

# 四,登录注册

# 1,配置路由

创建一个登录页面,如下:

1683617682762

配置对应的路由,如下:

1683617754211

参考代码如下:

 {
     path: '/login',
     name: 'login',
     component: () => import('@/views/User/Login.vue'),
     meta: {
         // 标题
         title: '登录',
     },
 },
1
2
3
4
5
6
7
8
9

访问之,如下:

1683617804857

# 2,api接口封装

封装api接口,如下:

1683618749740

参考代码如下:

import request from "@/utils/request"

/**
 * @description 登录
 * @param {string} name 用户
 * @param {string} password 密码    login({name:wc,password:123})
 */
export const login = ({
        name,
        password
    }) =>
    request({
        method: 'POST',
        url: '/frontend/sso/login',
        data: (() => {
            const loginData = new FormData();
            loginData.append("name", name)
            loginData.append("password", password)
            return loginData;
        })()
    });

/**
 * @description 注册
 * @param {{name:string, password:string,avatar:string,sex:number,sign:string,secret_answer:string}} userInfo 用户信息sign是个性签名
 * register({name:"wc",password:123,avatar:1.png, sex:1, sign: 666,secret_answer:1 })
 */
export const register = (userInfo) =>
    request({
        method: 'POST',
        url: '/frontend/sso/register',
        data: (() => {
            const registerData = new FormData();
            Object.keys(userInfo).forEach(key => {
                formData.set(key, userInfo[key]);
            })
            return registerData
        })()
    });
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

# 4,默认头像实现

创建avatar.js,是用来随机一个头像的。和注册QQ一样。如下:

1683619177657

参考代码如下:

// 默认头像地址
const avatar = [
    'https://w.wallhaven.cc/full/o5/wallhaven-o5363p.jpg',
    'https://w.wallhaven.cc/full/kx/wallhaven-kxwgvd.png',
    'https://w.wallhaven.cc/full/p9/wallhaven-p98xkp.jpg',
    'https://w.wallhaven.cc/full/5g/wallhaven-5gwq17.png',
    'https://w.wallhaven.cc/full/yx/wallhaven-yxmrkd.jpg',
    'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/88/h/88',
    'https://w.wallhaven.cc/full/l8/wallhaven-l8mx1l.jpg',
    'https://w.wallhaven.cc/full/yx/wallhaven-yxmo5k.png',
    'https://w.wallhaven.cc/full/6d/wallhaven-6dogj7.png',
    'https://w.wallhaven.cc/full/d6/wallhaven-d6g33m.jpg',
    'https://w.wallhaven.cc/full/x6/wallhaven-x68kjl.jpg',
    'https://w.wallhaven.cc/full/yx/wallhaven-yxm3zx.png',
    'https://w.wallhaven.cc/full/gp/wallhaven-gpjj9q.jpg',
    'https://w.wallhaven.cc/full/we/wallhaven-weyz9r.png',
    'https://w.wallhaven.cc/full/l8/wallhaven-l839zy.jpg',
    'https://img1.baidu.com/it/u=2029513305,2137933177&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=472',
];

// 从默认头像地址中取随机一个
const getAvatar = () => avatar[Math.round(Math.random() * (avatar.length - 1))];

export default getAvatar;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在登录组件中就需要获取一个头像了,如下:

1683619286816

在浏览器中测试之,如下:

1683619301805

把用户注册的相关的信息定义出来,如下:

1683620698876

把样式处理之,如下:

1683620749126

参考代码如下:

.container {
  margin-top: 40px;
  .user_img {
    height: 100px;
    padding-top: 58px;
    margin-bottom: 28px;
    display: flex;
    justify-content: center;
    align-items: center;
    img {
      width: 88px;
      height: 88px;
      border-radius: 50%;
      object-fit: cover;
    }
  }
  .registerBtn {
    margin-top: 12px;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

每次刷新页面,头像都不一样,效果如下:

image-20230303150727039

# 5,登录表单绘制

在头像下面,开始绘制表单,如下:

1683620974183

参考代码如下:

<template>
  <main>
    <van-nav-bar
      title="登录"
      left-arrow
      @click-left="$router.replace('/home')"
      fixed
    />
    <div class="container">
      <!-- 头像 -->
      <div class="user_img">
        <img :src="userInfo.avatar" alt="" />
      </div>
      <!-- 表单 -->
      <van-form>
        <van-cell-group inset>
          <van-field
            v-model.lazy.trim="userInfo.username"
            name="name"
            label="用户名"
            placeholder="用户名"
            :rules="[
              { required: true, message: '请填写用户名' },
              {
                pattern: /^[-_a-zA-Z0-9]{4,16}$/,
                message: '只能包含4-16位字母数字下划线减号',
              },
            ]"
          />
          <van-field
            v-model.lazy.trim="userInfo.password"
            type="password"
            name="password"
            autocomplete
            label="密码"
            placeholder="密码"
            :rules="[
              { required: true, message: '请填写密码' },
              {
                pattern: /^[-_a-zA-Z0-9]{6,16}$/,
                message: '只能包含6-16位字母数字下划线减号',
              },
            ]"
          />
        </van-cell-group>
        <div style="margin: 16px">
          <van-button round block type="success" native-type="submit">
            登录
          </van-button>
        </div>
      </van-form>
    </div>
  </main>
</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
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

效果如下

1683621035414

# 6,表单提交事件

image-20230303153729002

实现onSubmit方法,这个方法的形参就是收集到的数据,如下:

1683621297312

浏览器中效果如下:

1683621362062

实现登录,看服务器返回的结果,如下:

1683621479495

实现提示,实现跳转,如下 :

1683621580618

效果如下

image-20230303153813642

# 7,注册

注册的表单也是写在登录组件中的,分析如下:

1683680834243

定义一个状态,这个状态来控制上面多的表单和按钮是否显示,如下:

1683680931739

然后去定义一些表单,如下:

1683681788695

参考代码如下:

<template>
  <main>
    <van-nav-bar
      title="登录"
      left-arrow
      @click-left="$router.replace('/home')"
      fixed
    />
    <div class="container">
      <!-- 头像 -->
      <div class="user_img">
        <img :src="userInfo.avatar" alt="" />
      </div>
      <!-- 表单 -->
      <van-form @submit="onSubmit">
        <van-cell-group inset>
          <van-field
            v-model.lazy.trim="userInfo.username"
            name="name"
            label="用户名"
            placeholder="用户名"
            :rules="[
              { required: true, message: '请填写用户名' },
              {
                pattern: /^[-_a-zA-Z0-9]{4,16}$/,
                message: '只能包含4-16位字母数字下划线减号',
              },
            ]"
          />
          <van-field
            v-model.lazy.trim="userInfo.password"
            type="password"
            name="password"
            autocomplete
            label="密码"
            placeholder="密码"
            :rules="[
              { required: true, message: '请填写密码' },
              {
                pattern: /^[-_a-zA-Z0-9]{6,16}$/,
                message: '只能包含6-16位字母数字下划线减号',
              },
            ]"
          />
          <van-field
            v-if="isRegistered"
            v-model.lazy.trim="userInfo.avatar"
            type="url"
            name="avatar"
            label="头像"
            placeholder="图片地址"
            :rules="[
              { required: true, message: '请填写图片地址' },
              {
                pattern:
                  /^(http|ftp|https):\/\/([\w\-]+(\.[\w\-]+)*\/)*[\w\-]+(\.[\w\-]+)*\/?(\?([\w\-\.,@?^=%&:\/~\+#]*)+)?/,
                message: '请输入有效的网址',
              },
            ]"
          />
          <van-field v-if="isRegistered" name="sex" label="性别">
            <template #input>
              <van-radio-group v-model="userInfo.sex" direction="horizontal">
                <van-radio :name="1"></van-radio>
                <van-radio :name="0"></van-radio>
              </van-radio-group>
            </template>
          </van-field>
          <van-field
            v-if="isRegistered"
            v-model.lazy.trim="userInfo.sign"
            type="text"
            name="sign"
            label="签名"
            placeholder="个性签名"
            :rules="[{ required: true, message: '请填写个性签名' }]"
          />
          <van-field
            v-if="isRegistered"
            v-model.lazy.trim="userInfo.secretAnswer"
            type="text"
            name="secret_answer"
            label="密保"
            placeholder="验证密保"
            :rules="[{ required: true, message: '请填写密保' }]"
          />
        </van-cell-group>
        <div style="margin: 16px">
          <van-button
            round
            block
            :type="isRegistered ? 'primary' : 'success'"
            native-type="submit"
          >
            {{ isRegistered ? "注册" : "登录" }}
          </van-button>
          <van-button
            v-if="!isRegistered"
            round
            block
            class="registerBtn"
            type="primary"
            @click="isRegistered = true"
          >
            注册
          </van-button>
          <van-button
            v-if="isRegistered"
            plain
            round
            block
            class="registerBtn"
            type="primary"
            @click="isRegistered = false"
          >
            返回登录
          </van-button>
        </div>
      </van-form>
    </div>
  </main>
</template>

<style lang="scss" scoped>
.container {
  margin-top: 40px;
  padding-bottom: 40px;
  .user_img {
    height: 100px;
    padding-top: 58px;
    margin-bottom: 28px;
    display: flex;
    justify-content: center;
    align-items: center;
    img {
      width: 88px;
      height: 88px;
      border-radius: 50%;
      object-fit: cover;
    }
  }
  .registerBtn {
    margin-top: 12px;
  }
}
</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

效果如下

image-20230303162359302

# 8,处理注册

昨天有一个地方写错了,如下:

1683681934878

在Login.vue中导入接口,去调用接口,如下:

1683682453163

效果如下:

1683682386818

# 五,完善登录流程

# 1,分析需求

打开上线的项目,注册几个账号,观察cookie,和loacalstorege,如下:

1683682654744

点击注册,去观察,如下:

1683682726792

去登录,再测试之,如下:

1683682852676

1683683027075

然后,退出登录,使用之前注册的账号demo01,去登录,再分析,如下:

1683683093188

点击后,如下:

1683683126961

1683683178958

然后,使用demo01去登录,如下:

1683683214764

登录后,分析,如下:

1683683269642

1683683303231

现在去网页关闭掉,如下:

1683683387182

1683685428719

# 2,用户信息存储到仓库中

通常情况下,需要把用户信息进行集中管理,要么使用vuex4,要么使用pinia2,在当前项目中已经集成了pinia2,我们就使用pinia2。pinia去创建仓库有两种方案,参考地址:https://blog.csdn.net/broken_promise/category_11953807.html?spm=1001.2014.3001.5482

看一下,创建仓库有两种实现方式如下:https://blog.csdn.net/broken_promise/article/details/126321769

1683685829371

1683685902672

创建的项目默认就是使用的第二种方式,如下:

1683685976831

在这个项目中,我们使用第二种方式。如下:

1683686158223

参考代码如下:

import {
    ref
} from 'vue';
import {
    defineStore
} from 'pinia';

export const useUserStore = defineStore('user', () => {
    // 用户名
    const name = ref('');
    // 头像
    const avatar = ref('');
    // 性别
    const sex = ref(1);
    // 签名
    const sign = ref('');

    /**
     * 设置用户数据
     */
    const setInfo = info => {
        name.value = info.name;
        avatar.value = info.avatar;
        sex.value = info.sex;
        sign.value = info.sign;
    };
    /**
     * 重置用户信息
     */
    const reset = () => {
        name.value = '';
        avatar.value = '';
        sex.value = 1;
        sign.value = '';
    };

    return {
        name,
        avatar,
        sex,
        sign,
        setInfo,
        reset
    };
});
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

到此,仓库就状态好了。

# 2,封装操作token的工具函数

vue-element-admin这个项目中就把token存储到cookie,那我们就要操作cookie,使用第三方的库来操作cookie,叫js-cookie。

需要安装第三方库 js-cookie,如下:

1683686247785

把操作cookie进行封装,封装工具函数,如下:

1683686700611

参考代码如下:

import Cookies from 'js-cookie';

/**
 * token key
 */
const TokenKey = 'userToken';

/**
 * @description 获取本地token
 * @return {string}
 */
export const getToken = () => Cookies.get(TokenKey);

/**
 * @description 设置本地token
 * @param {string} token
 * @param {number} times 时间/秒  token的过期时间
 */
export const setToken = (token, times) =>
    Cookies.set(TokenKey, token, {
        expires: new Date(Date.now() + times * 1000)
    });

/**
 * @description 删除本地token
 */
export const removeToken = () => Cookies.remove(TokenKey);
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

# 3,封装用户信息存储到本地工具函数

我们上面封装了存储token的工具函数,还没有使用这些工具函数,我们上面还准备好了仓库,也没有使用。接下来我们封装把用户信息存储到本地的工具函数。如下(getData中少了一个return):

1683688175686

参考代码如下:

export const keyword = "user"

// 设置数据
// value是用户信息,用户信息是对象,对象不能直接存,
// 先利用JSON.stringify(value || [])转成普通的字符串
// encodeURI把字符串变成url编码,达到编码中文
// btoa url编码->base64编码
export const setData = (key, value) => {
    localStorage.setItem(`${keyword}-${key}`, btoa(encodeURI(JSON.stringify(value || []))))
}

// 获取数据
export const getData = key => {
    return JSON.parse(decodeURI(atob(localStorage.getItem(`${keyword}-${key}`) || '') || '') || '[]')
}

// 删除数据
export const deleteData = key => localStorage.removeItem(`${keyword}-${key}`);

// 获取上次登录者数据
export const lastLoginUser = () => {
    const uname = Object.keys(localStorage)
        .filter(it => it.startsWith(keyword))
        .find(it => getData(it?.replace(`${keyword}-`, '')).lastLogin)
        ?.replace(`${keyword}-`, '');
    const userInfo = getData(uname);
    userInfo.name = uname;
    return userInfo;
}

// 重置本地记录的所有的登录者中的标识   lastLogin
// 把所有的lastLogin变在false,当当前登录者用户的lastLoin变成true
// 退出登录也要调用resetLogin,
// user-demo01   UIFDUFLDSKFHUSHFLFNDSJHFSDHFDSFHJH  8  lastLogin = false
// user-malu001  FDFUEYWEGDGUIFDUFLDSKFHUSHFLFNDSHF  9  lastLogin = false
// user-malu002  FFDFSDFSDGDSGSDFUFLDSKFHUSHFLFNDSH  10  lastLogin = true
// cart-001  GUIFDUFLDSKFHUSHFLFND
export const resetLogin = () => {
    Object.keys(localStorage).filter(it => it.startsWith(keyword)).forEach(it => {
        const resetData = geteData(it.replace(`${keyword}-`, ''))
        resetData.lastLogin = false;
        setData(it.replace(`${keyword}-`, ''), resetData)
    })
}
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

# 4,完善登录细节(把状态存储到pinia中)

之前已经实现登录成功了,如下:

1683688290914

登录成功后,要做什么呢?如下:

1683688576444

1683688918475

浏览器测试之,如下 :

1683689154835

1683689173179

1683689375766

1683689400180

# 5,已登录的头像回显

和QQ登录一样,输入QQ号之后,显示已经登录的QQ号头像。实现监听用户名的变化,如下:

1683768764764

浏览器中测试之,如下:

1683768783061

用户名变了,根据用户名,调用getData,得到用户信息,有了用户信息,就可以得到头像,有了头像就可以回显了。代码如下:

1683768894154

在浏览器中输入一个a,如下:

1683768964662

如果没有头像就给默认头像,如下:

1683769100631

浏览器测试之,如下:

1683769166474

参考代码如下:

let isHas = false;
watch(toRef(userInfo, 'username'), val => {
    const avatar = getData(val).avatar;
    if (isHas) {
        userInfo.avatar = avatarUrl;
        isHas = false;
    } else if (avatar) {
        userInfo.avatar = avatar;
        isHas = true;
    }
});
1
2
3
4
5
6
7
8
9
10
11

# 6,导航标题动态变换

1683769288465

效果如下

1683769313419

登录或注册页面不需要tabbar,隐藏tabbar,如下:

1683769404050

效果如下:

1683769389349

# 六,axios二次封装

# 1,请求拦截器

写项目基本上都会对axios进行封装,其中有一荐,就是封装带个token,前面已经把token存储到cookie中了,在请求拦截器中,就需要带个token,把token放在什么地方传给服务器,能放的地方都有哪些?

  • url 传参
  • 请求体
  • 请求头,一般情况下,token,就是放在请求头中

对axios进行二次封装,如下:

import axios from "axios"
import {
    getToken
} from './auth';
import router from '@/router';
import {
    showNotify
} from 'vant';
import {
    useUserStore
} from '@/stores/user';

const request = axios.create({
    // 服务器基本地址
    // baseURL: "http://8.218.112.99",
    baseURL: "/api/ml/ml-mall",
    // 超时处理
    timeout: 5000,
})

//添加请求拦截器
request.interceptors.request.use(config => {
    config.headers['Authorization'] = 'Bearer '.concat(getToken());
    return config;
}, error => Promise.reject(error))

// 添加响应拦截器
request.interceptors.response.use(response => {
    console.log("---response:", response);
    const data = response.data;
    if (data.code !== 1) {
        if (data.code === 0) {
            // 去调用其它接口,如果没有登录,就没有token
            // 服务器返回code是0,需要去登录,去/login
            // 如果你本来要去的页面就是/login  
            if (router.currentRoute.value.path !== '/login') {
                router.push('/login');
            }
        }
        if (data.code === 3) {
            // 通知
            showNotify({
                type: 'danger',
                message: '用户名或密码错误',
                duration: 1500,
            });
        } else {
            // 过期或未登录
            if (data.message === '请登录') {
                const userStore = useUserStore();
                // 过期或未登录重置用户仓库数据
                userStore.reset();
                // 重置本地数据
                resetLogin();
                // 删除token
                removeToken();
            }
            // 通知
            showNotify({
                type: 'danger',
                message: data.message || '系统繁忙',
                duration: 1500,
            });
        }
        return Promise.reject(data);
    }
    return response.data;
}, error => Promise.reject(error))

export default request
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

# 七,路由前置守卫配置

当路由切换就会走前置守卫,在前置守卫,判断是否登录(判断cookie中是否有token),如果没有token,让它去登录页,但是有的页面需要登录后(需要有token)才能访问,有的页面不需要登录就可以访问,在meta中配置了login,如果login是true,表示此路由需要登录。如果有token,表示登录了,你登录了,就不能去登录了,从哪来还去哪。代码如下:

1683772608448

浏览器测试之,如下:

1683772748239

1683772775060

登录之,如下:

1683772886810

参考代码如下:

import {
    createRouter,
    createWebHistory
} from 'vue-router'

import {
    getToken
} from '@/utils/auth';

import NProgress from "nprogress"
import "nprogress/nprogress.css"

const router = createRouter({
    history: createWebHistory(
        import.meta.env.BASE_URL),
    routes: [{
            path: '/',
            name: 'root',
            redirect: "/home"
        },
        {
            path: '/home',
            name: 'home',
            component: () => import('@/views/Home/index.vue'),
            meta: {
                // 显示导航
                isShowNav: true,
            },
        },
        {
            path: '/recommend',
            name: 'recommend',
            component: () => import('@/views/Recommend/index.vue'),
            meta: {
                isShowNav: true,
                // 需要登录的   
                login: true,
                title: '种草推荐',
            },
        },
        {
            path: '/cart',
            name: 'cart',
            component: () => import('@/views/Cart/index.vue'),
            meta: {
                isShowNav: true,
                // 需要登录的   
                login: true,
                title: '购物车',
            },
        },
        {
            path: '/user',
            name: 'user',
            component: () => import('@/views/User/index.vue'),
            meta: {
                isShowNav: true,
                title: '我的',
            },
        },
        // 访问没有的路由直接跳往首页
        {
            path: '/:toHome*',
            name: 'toHome',
            redirect: '/home',
        },
        {
            path: '/login',
            name: 'login',
            component: () => import('@/views/User/Login.vue'),
            meta: {
                // 标题
                title: '登录',
            },
        },
    ]
})

NProgress.configure({
    showSpinner: false
})

// 刚才我们封装的axios的响应拦截器,只有你去调用api接口才会走
// beforeEach是在路由切换时,才会走,可能没有调用api接口
// 路由切换时,也想判断一下有没有登录,如果登录了做了什么?
//   1)cookie中有token
//   2)vuex中用户信息(vuex中的数据,你一刷新就没有了)
//   3)localStorage中也用户信息(编码) (没有登录时)
router.beforeEach((to, from, next) => {
    NProgress.start();
    const hasToken = getToken();
    if (hasToken) {
        // 有token  表示已登录
        if (to.path === "/login") {
            return next({
                path: from.path
            })
        }
    } else {
        // 没有token, 按理说需要去登录页面
        // 但是我们要知道,有些页面,不需要登录,不需要token也可以访问
        if (to.meta.login) {
            return next({
                path: '/login'
            });
        }
    }

    return next();
})

router.afterEach(() => {
    NProgress.done();
})

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
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,创建商品详情组件并配置路由

创建商品组件,如下:

1683853546641

配置路由,如下:

1683853618359

参考代码:

{
    path: '/goods/:goodsId',
    name: 'goodsDetail',
    component: () => import('@/views/Home/GoodsDetail.vue'),
    meta: {
        title: '商品详情',
    },
},
1
2
3
4
5
6
7
8

浏览器测试之,如下:

1683853655727

点击商品,代码如下:

1683853713834

1683853735023

# 2,api接口配置

封装根据id获取商品详情的接口,如下:

1683853848713

参考代码如下:

import request from '@/utils/request';

/**
 * @description 获取商品详情
 * @param {number} id 商品id
 */
export const getGoodsDetail = id =>
    request({
        method: 'POST',
        url: '/frontend/goods/detail',
        params: {
            id,
        },
    });
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3,绘制页面

# 1,标题实现

1683853944632

# 2,获取数据

根据ID,调用接口,获取商品详情的数据,如下:

1683854234126

参考代码如下:

<script setup>
import { getGoodsDetail } from "@/api/goods";
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();

// 从路由获取商品id
const goodsId = route.params.goodsId;

// 商品详情
const goodsDetail = ref({});

onMounted(async () => {
  const res = await getGoodsDetail(goodsId);
  goodsDetail.value = res.data;
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3,渲染页面

数据如下:

1683854365073

绘制页面,显示数据,如下:

1683854424688

参考代码如下:

 <div class="container">
     <div class="body_box">
         <div class="img_box">
             <img :src="goodsDetail.pic_url" :alt="goodsDetail.pic_url" />
         </div>
         <p class="price">¥{{ (goodsDetail.price / 100).toFixed(2) }}</p>
         <div class="goods_name">{{ goodsDetail.name }}</div>
         <p class="brands">品牌商:{{ goodsDetail.brand }}</p>
         <div class="express_prompt">
             <span>
                 库存:<span>{{ goodsDetail.stock }}</span>
             </span>
             <span>免邮费 顺丰快递</span>
         </div>
         <div class="detail">
             <p class="title">商品详情</p>
             <p class="content">{{ goodsDetail.detail_info }}</p>
         </div>
     </div>
 </div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

效果如下:

1683854438032

书写对应的样式,如下:

1683854502522

浏览器效果如下:

1683854518122

参考代码如下:

<style lang="scss" scoped>
// 内边距离
$pad: 8px;
.body_box {
  background-color: white;
  margin: 0 10px;
  .img_box {
    width: 100%;
    text-align: center;
    img {
      width: 100%;
    }
  }
  .goods_name {
    padding: 0 $pad;
    background-color: white;
    font-size: 18px;
  }
  .brands,
  .express_prompt {
    padding: 0 $pad;
    display: flex;
    justify-content: space-between;
    color: #999999;
    font-size: 14px;
    margin: 6px 0;
  }
  .price {
    padding: 0 $pad;
    color: #f63515;
    font-size: 22px;
    margin: 6px 0;
  }
  .detail {
    .title {
      margin: 10px 0;
      font-weight: bold;
      font-size: 14px;
      color: black;
      text-align: center;
    }
    .content {
      padding: 0 12px;
      font-size: 16px;
      color: rgb(84, 84, 84);
      text-indent: 2em;
    }
  }
}
</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

# 4,动作栏实现

书写代码如下:

1683854681170

参考代码如下:

<!-- 动作栏 -->
    <van-action-bar placeholder>
      <van-action-bar-icon
        icon="cart-o"
        text="购物车"
        @click="$router.push('/cart')"
      />
      <van-action-bar-icon icon="star-o" text="收藏" color="#ff5000" />
      <van-action-bar-button type="warning" text="加入购物车" />
      <van-action-bar-button type="danger" text="立即购买" />
    </van-action-bar>
1
2
3
4
5
6
7
8
9
10
11

效果如下

image-20230304100001337

# 3,购物车逻辑实现

# 1,购物车api配置

封装添加商品到购物车的api,如下:

1683855316536

参考代码如下:

import request from '@/utils/request';

/**
 * @description 添加到购物车
 * @param {{goods_id:string,count:string}} form
 */
export const addCart = form =>
    request({
        method: 'POST',
        url: '/frontend/cart/add/',
        data: (() => {
            const formData = new FormData();
            Object.keys(form).forEach(key => {
                formData.set(key, form[key]);
            });
            return formData;
        })(),
    });

/**
 * @description 获取购物车数据
 * @param {{page:number,limit:number}} queryObj
 */
export const getCartList = queryObj =>
    request({
        method: 'POST',
        url: '/frontend/cart/list/',
        params: queryObj
    });
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

# 2,购物车全局状态

把购物车的商品,也需要存储到pinia中,如下:

1683855499041

参考代码如下:

import {
    ref
} from 'vue';
import {
    defineStore
} from 'pinia';
import {
    getCartList
} from '@/api/cart';

export const useCartStore = defineStore('cart', () => {
    // 购物车商品数量
    const count = ref(0);
    // 购物车商品
    const data = ref([]);
    // 从服务获取数据
    const changeCart = async () => {
        const res = await getCartList({
            page: 1,
            limit: 100
        });
        if (res.code === 1) {
            count.value = res.data.count;
            data.value = res.data.list;
        }
    };
    // 重置数据
    const reset = () => {
        count.value = 0;
        data.value = [];
    };

    return {
        count,
        data,
        changeCart,
        reset
    };
});
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

需要在商品详情页面中调用仓库中的changeCart,changeCart中调用了api接口,获取购物车数据,如下:

1683855698891

当点击加入购物车时,就需要调用方法,给加入购物车绑定点击事件,如下:

1683855776718

实现此方法,如下:

1683855927049

浏览器测试之,如下:

1683856135063

到此,pinia中就有数据了,如下:

1683856176123

前面我们点击了加入购物车,我们调用了仓库中的changeCart方法,这个方法中,调用了api接口,获取购物车数据了。但是并没有实现添加购物车的功用,也就是说当我们点击了加入购物车,分两种情况下:1)这个商品压根就没有在购物车中,需要调用addCart() 2)购物车中有此商品,就仅仅是让当前商品的数量+1,调用editCart。所以我们再封装一个编辑购物车,如下:

1683856514142

参考代码如下:

/**
 * @description 编辑购物车数据
 * @param {{goods_id:string,count:string,id:string}} form
 */
export const editCart = form =>
    request({
        method: 'POST',
        url: '/frontend/cart/update/',
        data: (() => {
            const formData = new FormData();
            Object.keys(form).forEach(key => {
                formData.set(key, form[key]);
            });
            return formData;
        })(),
    });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如何知道,商品是否在购物车呢?我们需要获取购物车中的所有数据,前面我们已经把购物车中的数据获取了放到了pinia中了,现在只需要从pinia中获取数据遍历对比,就可以知道购物车有没有此商品了,如下:

1683856775296

浏览器测试之,如下:

1683856837769

1683856863796

1683856900319

1683856976550

到此,加入购物车的逻辑就实现了。

# 3 ,加入购物车逻辑完善(难)

需求:

1683858076938

直接在pinia中取数量就OK了,如下:

1683858130478

浏览器测试之,如下:

1683858144822

pinia中的数据一刷新没有了,因为pinia中的数据是存储在内存中的。测试如下:

1683858207872

现在的问题是一刷新pinia中的数据就没有了,除了这个问题,还有一个问题,如果换一个用户重新登录,另一个用户和当前用户的购物车数据是不一样的,也就是说换用户了,需要重新获取购物车数据,如果换用户了,会走setInfo,如下:

1683858453565

浏览器测试之,如下:

1683858855269

进入到商品详情,解决一下之前的一个不bug,如下:

1683858941681

一进入详情,就需要调用,只有一进来就调用,仓库中才有数据,如下:

1683858990309

测试如下:

1683859064843

然后,我们换一个用户登录,换一个用户登录,就会走setInfo,测试如下:

1683859168432

重新使用malu001用户登录之,如下:

1683859213364

1683859232625

1683859266278

1683859402223

1683859445005

进入详情,如下:

1683859713586

再使用demo01测试之,再进入到详情,如下:

1683859740504

当我们把浏览器关了,再打开这个项目,肯定需要找到最后登录的用户,之前封装过一个方法,可以找到最后登录的用户,需要调用setInfo,在setInfo中调用了获取购物车数据,先去封装一个api接口,如下:

1683859998673

1683860281600

关闭项目,如下:

1683860318706

重新打开项目,会走App中的onMounted,在onMounted中判断是否登录(根据是否有token),如果

登录了,到底是哪个用户登录了,调用lastloginUser,得到最后登录者的用户,再调用setInfo,在setInfo中重新获取购物车数据,如下:

1683860442794

# 4,tabbar购物车图标数量显示

需求:

1683860586628

实现父传子,如下:

1683861559959

子接收之,如下:

1683861640307

到此,详情页面中,添加购物车的逻辑就写完了。

# 4,收藏实现

# 1,分析

本项目中,可以商品,也可以收藏文章,点击了收藏,如下 :

1684113017483

在我的模块中,就可以查看收藏的商品了,如下:

1684113051197

点击我的收藏,如下:

1684113071612

再点击收藏按钮,表示取消收藏,如下:

1684113134078

打开接口文档,如下:

1684113358739

# 1,api接口配置

上面分析了api接口文档,封装四个api接口,如下:

1684113644288

参考代码:

import request from '@/utils/request';

/**
 * @description 添加收藏
 * @param {{type: string,object_id:string}} info
 */
export const addCollection = info =>
    request({
        method: 'POST',
        url: '/frontend/collection/add/',
        data: (() => {
            const formData = new FormData();
            Object.keys(info).forEach(key => {
                formData.set(key, info[key]);
            });
            return formData;
        })(),
    });

/**
 * @description 删除收藏
 * @param {string} id
 */
export const deleteCollectionById = id =>
    request({
        method: 'POST',
        url: '/frontend/collection/delete/',
        data: (() => {
            const formData = new FormData();
            formData.set('id', id);
            return formData;
        })(),
    });

/**
 * @description 删除收藏
 * @param {{type: string,object_id:string}} info
 */
export const deleteCollectionByType = info =>
    request({
        method: 'POST',
        url: '/frontend/collection/delete/',
        data: (() => {
            const formData = new FormData();
            Object.keys(info).forEach(key => {
                formData.set(key, info[key]);
            });
            return formData;
        })(),
    });

/**
 * @description 获取收藏
 * @param {{type:string,page:string,limit:string}} queryObj
 */
export const getCollectionList = queryObj =>
    request({
        method: 'POST',
        url: '/frontend/collection/list/',
        params: queryObj,
    });
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

# 2,实现添加收藏的逻辑

找到添加收藏的按钮,给添加收藏按钮上绑定点击事件,如下:

1684113854880

然后,实现toggleStar方法,如下:

1684114048688

引用对应的接口,如下:

1684114095740

调用接口,如下:

1684114276119

在浏览器中测试之,如下:

1684114392302

1684114438275

解决bug,如下:

1684114706734

浏览器测试之,如下:

1684114755260

1684114790458

# 5,购买实现

分析:

1684116450143

找到立即购买,绑定点击事件,如下:

1684116540070

点击立即购买,去地址选择页面,导入router,如下:

1684116736951

实现对应的方法,如下:

1684117086583

浏览器测试之,如下:

1684117243721

# 九,地址管理页

# 1,配置路由

创建地址列表组件,如下:

1684117355901

配置路由,如下:

1684117407427

参考代码如下:

{
    path: '/address-list',
    name: 'addressList',
    component: () => import('@/views/Address/index.vue'),
    meta: {
        login: true,
        title: '收货地址',
    },
},
1
2
3
4
5
6
7
8
9

# 2,api接口配置

查看api接口文档,如下:

1684117487027

封装地址管理的api接口,如下:

1684117581925

参考代码:

import request from '@/utils/request';

/**
 * @description 获取收货地址列表
 * @param {{page: string,limit: string}} queryObj
 */
export const getAddressList = queryObj =>
    request({
        method: 'POST',
        url: '/frontend/consignee/list/',
        params: queryObj
    });

/**
 * @description 新增收货地址
 * @param {{is_default:string,name:string,phone:string,province:string,city:string,town:string,street:string,detail:string}} info
 */
export const addAddress = info =>
    request({
        method: 'POST',
        url: '/frontend/consignee/add/',
        data: (() => {
            const formData = new FormData();
            Object.keys(info).forEach(key => {
                formData.set(key, info[key]);
            });
            return formData;
        })(),
    });

/**
 * @description 编辑地址
 * @param {{id:string,is_default:string,name:string,phone:string,province:string,city:string,town:string,street:string,detail:string}} info
 */
export const updateAddress = info =>
    request({
        method: 'POST',
        url: '/frontend/consignee/update/',
        data: (() => {
            const formData = new FormData();
            Object.keys(info).forEach(key => {
                formData.set(key, info[key]);
            });
            return formData;
        })(),
    });

/**
 * @description 删除地址
 * @param {string} id
 */
export const deleteAddress = id =>
    request({
        method: 'POST',
        url: '/frontend/consignee/delete/',
        data: (() => {
            const formData = new FormData();
            formData.set('id', id);
            return formData;
        })(),
    });
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

# 3,绘制页面

绘制地址列表的页面的navbar,如下:

1684117738338

浏览器中测试如下:

1684117787612

然后就需要绘制地址列表,地址列表在vant中有现成的组件,如下:

1684117844047

分析:

1684118000664

看一下地址列表的数据格式如下:

1684118129398

1684118167629

去给demo01这个用户添加一些测试地址数据,如下:

1684118227838

然后,我们就需要调用接口,获取地址列表相关的数据,如果数据非常多,要实现上拉加载下一页,下拉刷新(重新调接口,获取最新的数据)。此时就需要使用到下面的组件,如下:

1684118454157

1684118474366

分析list组件,如下:

1684119002422

书写对应的代码,如下:

1684119235839

参考代码如下:

<script setup>
import { ref } from "vue";
const list = ref([]);
const loading = ref(false);
const finished = ref(false);

// onLoad事件一上来就会触发一次,
const onLoad = () => {
  // 异步更新数据
  // setTimeout 仅做示例,真实场景中一般为 ajax 请求
  setTimeout(() => {
    for (let i = 0; i < 10; i++) {
      list.value.push(list.value.length + 1);
    }

    // 加载状态结束
    loading.value = false;

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

<template>
  <div>
    <van-nav-bar
      :title="$route.meta.title"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
  </div>
  <van-list
    v-model:loading="loading"
    :finished="finished"
    finished-text="没有更多了"
    @load="onLoad"
  >
    <van-cell v-for="item in list" :key="item" :title="item" />
  </van-list>
</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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

效果如下:

1684119265859

开始绘制地址组件,定义相关的状态,如下 :

1684120417646

调接口,获取数据,如下:

1684120571837

浏览器中测试如下:

1684120555041

使用数据之前,需要进行数据格式转化,如下:

1684120693997

开始转化,使用之,如下:

1684121035863

浏览器中测试之,如下:

1684121073273

还有一个数据,处理之,如下:

1684121220915

浏览器测试之,如下:

1684121249532

到此,地址组件相关的两个状态就处理好了,使用组件,如下:

1684121349682

浏览器测试之,如下:

1684121432359

处理之如下:

1684121525755

浏览器测试之,如下:

1684121607811

到此,显示地址列表就OK了。

# 十,地址修改页

# 1,创建组件,配置路由

创建一个Edit组件,Edit组件即表示新增,也表示编辑,如下:

1684199060912

配置路由,如下:

1684199111044

参考代码如下:

    {
        path: '/edit-address',
        name: 'edit',
        component: () => import('@/views/Address/Edit.vue'),
        meta: {
            login: true,
            title: '编辑',
        },
    },
1
2
3
4
5
6
7
8
9

现在我们登录了,浏览器访问之,如下:

1684199165155

# 2,页面绘制

开始绘制编辑 或 新增的页面,这个页面中,有一个选项地区的popup,如下:

1684199314463

1684199427843

安装vant地区包,如下:

1684199456816

开始绘制页面,如下:

1684199650315

参考代码如下:

<script setup>
import { areaList } from '@vant/area-data';

</script>

<template>
  <div class="container">
    <van-nav-bar
      title="新增地址"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
    <van-address-edit
      :area-list="areaList"
      show-set-default
      :area-columns-placeholder="['请选择', '请选择', '请选择']"
    />
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

浏览器效果如下

image-20230304190724400

# 3,实现新增逻辑

绑定save事件,如下:

1684199743327

实现对应的onSave方法,打印收集的表单数据,如下:

1684199889834

浏览器测试如下:

1684200055110

还需要分析,调用接口,给接口传递的数据如下:

1684200168611

直接可以发请求了,如下 :

1684200407792

浏览器测试之,如下:

1684200502902

1684200540297

我们去访问地址列表页面,如下:

1684200586476

到此,添加地址就OK了。

参考代码如下:

<script setup>
import { areaList } from "@vant/area-data";
import { useRoute, useRouter } from "vue-router";
import { addAddress } from "@/api/address";
import { ref } from "vue";
import { showSuccessToast } from "vant";

const router = useRouter();

// info 表示表单中填写的数据
const onSave = async (info) => {
  let res = await addAddress({
    is_default: info.isDefault ? "1" : "0",
    name: info.name,
    phone: info.tel,
    province: info.province,
    city: info.city,
    town: info.county,
    street: null,
    detail: info.addressDetail,
  });
  if (res.code === 1) {
    showSuccessToast({
      message: "添加成功",
      duration: 1500,
    });
    // 回到地址列表
    // router.replace("/address-list");
  }
};
</script>

<template>
  <div class="container">
    <van-nav-bar
      title="新增地址"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
    <van-address-edit
      :area-list="areaList"
      show-set-default
      :area-columns-placeholder="['请选择', '请选择', '请选择']"
      @save="onSave"
    />
  </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
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

还需要在地址列表页面中,给新增地址绑定点击事件,如下:

1684201991902

# 4,地址编辑功能

然后,实现编辑地址的功能,与地址新增功能可以复用同一个组件,在地址列表页面中,点击编辑时,触发的事件如下:

1684202032196

实现onEdit方法,如下:

1684202306597

浏览器测试之,如下:

1684202383943

在编辑组件中,就可以获取地址信息了,测试之,如下:

1684202582698

浏览器中测试之,是否可以获取用户信息,如下:

1684202610799

然后,实现数据的回显,先定义状态,给状态赋值,如下:

1684202769698

浏览器测试是否实现了数据回显,如下:

1684202793109

然后,我可以修改表单中的数据,修改完后,再点击保存,作用就是实现编辑,如下:

1684202965257

直接上参考代码:

<script setup>
import { areaList } from "@vant/area-data";
import { useRoute, useRouter } from "vue-router";
import { addAddress, updateAddress } from "@/api/address";
import { ref } from "vue";
import { showSuccessToast } from "vant";

const router = useRouter();
const route = useRoute();

// isEdit表示是否处于编辑模式
const isEdit = ref(false);
const addressList = ref({
  id: 0,
  name: "",
  tel: "",
  province: "",
  city: "",
  county: "",
  addressDetail: "",
  isDefault: false,
});

// 如果route.query.addressInfo有值,表示是点击的编辑过来的
if (route.query.addressInfo) {
  isEdit.value = true;
  const addressData = JSON.parse(
    decodeURI(atob(route.query.addressInfo || "") || "") || "[]"
  );
  addressList.value = {
    id: addressData.id,
    name: addressData.name,
    tel: addressData.phone,
    province: addressData.province,
    city: addressData.city,
    county: addressData.town,
    addressDetail: addressData.detail,
    isDefault: !!addressData.is_default,
  };
}

// info 表示表单中填写的数据
const onSave = async (info) => {
  if (!isEdit.value) {
    // 新增
    let res = await addAddress({
      is_default: info.isDefault ? "1" : "0",
      name: info.name,
      phone: info.tel,
      province: info.province,
      city: info.city,
      town: info.county,
      street: null,
      detail: info.addressDetail,
    });
    if (res.code === 1) {
      showSuccessToast({
        message: "添加成功",
        duration: 1500,
      });
    }
  } else {
    // 编辑
    const res = await updateAddress({
      id: info.id, // 编辑相对于新增多了一个id参数
      is_default: info.isDefault ? "1" : "0",
      name: info.name,
      phone: info.tel,
      province: info.province,
      city: info.city,
      town: info.county,
      street: null,
      detail: info.addressDetail,
    });
    if (res.code === 1) {
      showSuccessToast({
        message: "修改成功",
        duration: 1500,
      });
    }
  }
  // 回到地址列表
  router.replace("/address-list");
};
</script>

<template>
  <div class="container">
    <van-nav-bar
      title="新增地址"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
    <van-address-edit
      :address-info="addressList"
      :area-list="areaList"
      show-set-default
      :area-columns-placeholder="['请选择', '请选择', '请选择']"
      @save="onSave"
    />
  </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
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

浏览器测试之,如下:

16842031038521684203150959

1684203168918

修改一下van-nav-bar的title,如下:

1684203215995

浏览器测试之,如下:

1684203228971

# 5,地址的删除功能

要显示删除按钮,如下:

1684203493866

浏览器效果如下:

1684203511592

实现删除的逻辑,如下:

1684203548261

1684203585706

浏览器测试之,如下:

1684203620465

到此,删除地址的逻辑就实现了。

# 十一,订单数据的传递

# 1,分析流程

1684204970444

1684205289422

1684205457919

# 1,订单数据传递

点击购买按钮时把商品信息通过路由传参到了地址管理页,在地址列表页面,接收数据,如下:

1684205667557

点击新增或编辑,传递数据,如下:

1684205810209

在新增或编辑组件中,编辑或新增完成时,回到地址列表时,还需要把数据传递回去,如下:

1684206004485

点击删除时,也需要传递回去,如下:

1684206043469

点击返回箭头时,也需要传递回去,如下:

1684206098298

浏览器中测试如下:

1684206130419

1684206190913

1684206222322

16842062824451684206380951

1684206426235

# 2,地址选中

现在我们有很多地址,选中某个地址,要去生成订单页面,首先需要打印出我们选中哪个地址,如下:

1684206563807

实现代码如下:

1684206605971

实现对应的方法,如下:

1684206876873

浏览器测试之,如下:

1684206942198

参考代码如下:

// 地址选中事件
const selectAddress = form => {
    if (orderInfo.value) {
        // 把商品数据和地址信息发到订单生成页
        router.replace({
            name: 'createOrder',
            query: {
                orderInfo: btoa(
                    encodeURI(
                        JSON.stringify({
                            ...orderInfo.value,
                            consignee_name: form.name,
                            consignee_phone: form.tel,
                            consignee_address: form.address,
                            isDefault: form.isDefault,
                        })
                    )
                ),
            },
        });
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 十二,订单生成页

# 1,创建组件并配置路由

创建生成订单组件,如下:

1684285494725

配置路由,如下:

1684285540973

浏览器访问之,如下:

1684285577585

点击地址,测试是否可以到达生成订单页面,如下:

1684285626419

1684285672971

参考代码:

 {
     path: '/create-order',
     name: 'createOrder',
     component: () => import('@/views/Order/Create.vue'),
     meta: {
         title: '生成订单',
         login: true,
     },
 },
1
2
3
4
5
6
7
8
9

# 2,配置api

查看接口文章,看一下关于订单的相关的接口,如下:

1684285738984

然后,封装api,如下:

1684285791201

参考代码如下:

import request from '@/utils/request';

/**
 * @description 我的订单
 * @param {{page:number;limit:number;status:string}} queryObj
 */
export const getOrder = queryObj =>
    request({
        method: 'POST',
        url: '/frontend/order/list/',
        params: queryObj,
    });

/**
 * @description 创建订单
 * @param {{ order_goods_infos: { count: string;goods_id: string}[],consignee_phone:string;consignee_name:string; consignee_address:string;pay_type: number,price: number,remark: string,status: string}} info
 */
export const createOrder = info =>
    request({
        method: 'POST',
        url: '/frontend/order/add/',
        data: info,
    });

/**
 * @description
 * @param {{id: string,pay_type:string,pay_at:string,status:string}} info
 */
export const editOrderStatus = info =>
    request({
        method: 'POST',
        url: '/frontend/order/add/',
        data: info,
    });
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

# 3,标题绘制

先把标题绘制出来,如下:

1684286095557

浏览器测试之,如下:

1684286118430

点击确认,我们是没有/order这个路由,按我们之前讲的,会报错,报错的原因是通过name来跳转的。但是现在我们是通过/order这个路径来跳转的,没有报错,原因是我们在router.j,配置了如下代码:

1684286357721

所以说,此时我们点击确认,就去Home页面了。

# 4,接收路由参数

到达生成订单页面,人家是传递参数过来的,参数中包含商品信息和收货人信息,如下:

1684286496142

此时,我们就需要接收参数,解析参数,如下:

1684286609874

浏览器测试如下:

1684286695447

把上面的数据定义成响应式,如下:

1684286924861

参考代码如下:

< script setup >
    import {
        ref
    } from "vue";
import {
    showConfirmDialog
} from "vant";
import {
    useRoute,
    useRouter
} from "vue-router";

const route = useRoute();
const router = useRouter();

if (!route.query.orderInfo) {
    // url中没有orderInfo这个参数
    router.replace('/cart');
}

// orderInfo包含商品信息和收货人信息
const orderInfo = ref(
    JSON.parse(decodeURI(atob(route.query.orderInfo || "") || "") || "[]")
);

const back = () => {
    // 点击了确定,去订单列表了,现在没有订单列表路由
    showConfirmDialog({
            message: "退出生成订单?",
        })
        .then(() => {
            router.replace("/order");
        })
        .catch(() => {});
}; <
/script>

<
template >
    <
    div class = "container" >
    <
    van - nav - bar
left - arrow
@click - left = "back": title = "$route.meta.title"
placeholder
fixed
    /
    >
    <
    /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
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

# 5,地址展示和商品信息展示

开始绘制页面,显示数据,如下:

1684287496671

1684287511607

效果如下:

1684287524441

书写一些样式,如下:

1684287750819

参考代码:

<style lang="scss" scoped>
.container {
  // vant自带变量
  --van-submit-bar-z-index: 100;
  --van-address-list-add-button-z-index: 1;
  .address {
    padding-top: 20px;
    background-color: #fff;
    .address_list {
      // vant自带变量
      --van-address-list-padding: 0;
      :deep(.van-badge__wrapper) {
        display: none;
      }
    }
  }
  .submit_box {
    background-color: #fff;
    bottom: 0;
    left: 0;
    right: 0;
    position: fixed;
    padding: 10px;
  }
}
</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

效果如下:

1684287771869

除了显示收货人信息,还需要显示商品信息,如下:

1684287901241

效果如下:

1684287918273

到此,生成订单页面中的收货人信息和商品信息展示就绘制OK了。

# 6,绘制 提交订单 和 备注

开始绘制,如下:

1684289213506

准备onSubmit方法和remark备注,如下:

1684289282595

在浏览器中看一下效果如下:

1684289337009

# 7,绘制支付弹出框

定义一个状态,控制是否显示弹窗,当我们点击生成订单时,就需要把弹窗显示出来,如下:

1684289494545

绘制弹窗,如下:

1684289599226

实现上面的两个方法,如下:

1684289749510

效果如下:

1684289625634

# 8,支付逻辑实现

点击了某一种支付方式,要实现支付,分析接口,如下:

1684289984246

商品相关的信息在哪里?如下:

1684290048714

准备数据,发送ajax请求,如下:

1684290239723

1684290365590

浏览器测试之,如下:

1684290487860

1684290512003

参考代码如下:

// 选择的支付方式
const onSelect = async (item) => {
    // 这里要发送ajax请求
    const order_goods_infos = orderInfo.value.goods.map((it) => ({
        goods_id: it.goods_id,
        count: it.count,
    }));
    // 判断是否有商品没有就返回订单查看页
    if (!order_goods_infos.length) {
        router.replace("/order");
        return;
    }
    // 订单数据item.pay_type对应配置的pay_type
    const order = {
        pay_type: item.pay_type,
        status: 2, // 已支付
        remark: remark.value,
        price: orderInfo.value.price,
        consignee_name: orderInfo.value.consignee_name,
        consignee_phone: orderInfo.value.consignee_phone,
        consignee_address: orderInfo.value.consignee_address,
        order_goods_infos: order_goods_infos, // 商品信息
    };
    // 发送创建订单请求
    await createOrder(order);
    // 回到订单列表页面
    router.replace("/order");
};
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

# 9,支付框关闭事件

点击取消,表示也要生成订单,只不过没有支付,逻辑如下:

1684290657427

参考代码如下:

// 关闭事件
const onCancel = async () => {
    // 获取每个商品的id/数量
    const order_goods_infos = orderInfo.value.goods.map(it => ({
        goods_id: it.goods_id,
        count: it.count,
    }));
    // 判断是否有商品没有就返回订单查看页
    if (!order_goods_infos.length) {
        router.replace('/order');
        return;
    }

    // 订单数据item.pay_type对应配置的pay_type
    const order = {
        pay_type: 0,
        status: 1, // 未支付
        remark: remark.value,
        price: orderInfo.value.price,
        consignee_name: orderInfo.value.consignee_name,
        consignee_phone: orderInfo.value.consignee_phone,
        consignee_address: orderInfo.value.consignee_address,
        order_goods_infos: order_goods_infos,
    };
    // 发送创建订单请求
    await createOrder(order);

    // 回到订单页
    router.replace('/order');
};
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

到此,我们就完成了生在订单了。so easy~

# 十三,订单查看页

# 1,创建组件并配置路由

创建组件如下:

1684292713216

配置路由,如下:

1684292748370

参考代码:

    {
        path: '/order',
        name: 'order',
        component: () => import('@/views/Order/index.vue'),
        meta: {
            title: '我的订单',
            login: true,
        },
    },
1
2
3
4
5
6
7
8
9

浏览器访问之,如下:

1684292776523

# 2,标题绘制

代码如下:

1684292819419

浏览器效果如下 :

1684292831316

# 3,tab导航

找到vant中的tab导航,如下 :

1684292895378

定义状态,使用tab组件,如下:

1684293049791

浏览器效果如下:

1684293073467

# 4,封装OrderList组件

在components下面封装一个组件,叫OrderList,如下 :

1684293222442

在订单查看页面中使用之,如下:

1684293672690

参考代码如下:

<script setup>
import { getOrder } from "@/api/order";
import { ref, onMounted, defineAsyncComponent } from "vue";
const active = ref(0);
const orderData = ref([]);

const OrderList = defineAsyncComponent(() =>
  import("@/components/OrderList.vue")
);

onMounted(async () => {
  const res = await getOrder({ status: 0, limit: 30, page: 1 });
  if (res.code === 1) {
    orderData.value = res.data.list?.reverse?.() ?? [];
  }
});
</script>

<template>
  <div class="container">
    <van-nav-bar
      left-arrow
      @click-left="$router.replace('/user')"
      :title="$route.meta.title"
      placeholder
      fixed
    />
    <van-tabs v-model:active="active" offset-top="45px" sticky swipeable>
      <van-tab title="全部">
        <Suspense>
          <template #default>
            <OrderList :orderData="orderData" />
          </template>
          <template #fallback> Loading... </template>
        </Suspense>
      </van-tab>
      <van-tab title="待支付">
        <OrderList :orderData="orderData" />
      </van-tab>
      <van-tab title="待发货">
        <OrderList :orderData="orderData" />
      </van-tab>
      <van-tab title="待收货">
        <OrderList :orderData="orderData" />
      </van-tab>
      <van-tab title="待评价">
        <OrderList :orderData="orderData" />
      </van-tab>
    </van-tabs>
  </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
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

在浏览器中测试之,如下:

1684293800608

接收到数据,只需要写html+css去渲染数据,如下 :

1684293932750

浏览器效果如下:

1684293950873

参考代码如下:

<script setup>
// 谁要用OrderList组件,需要传递orderData
const props = defineProps({
  orderData: {
    type: Array,
    default: [],
  },
});
const transformStatus = (status) => {
  switch (status) {
    case 1:
      return "待支付";
    case 2:
      return "待发货";
    case 3:
      return "待收货";
    case 4:
      return "待评价";
  }
};

const transformPayType = (type) => {
  switch (type) {
    case 1:
      return "微信";
    case 2:
      return "支付宝";
    case 3:
      return "云闪付";
  }
};
</script>

<template>
  <div class="container">
    <div class="order_item" v-for="order in $props.orderData" :key="order.id">
      <div class="header">
        <p class="create_time">订单时间:{{ order.created_at }}</p>
        <p class="status">{{ transformStatus(order.status) }}</p>
      </div>
      <van-card
        :key="goods.id"
        class="goods_item"
        v-for="goods in order.order_goods_infos"
        :num="goods.count"
        :price="((goods.goods_info?.price || 0) / 100).toFixed(2)"
        :desc="goods.goods_info?.detail_info"
        :title="goods.goods_info?.name"
        :thumb="
          goods.goods_info?.pic_url ||
          'https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg'
        "
      />
      <div class="footer">
        <div
          :style="{
            visibility: order.status === 1 ? 'hidden' : 'visible',
          }"
        >
          支付方式:{{ transformPayType(order.pay_type) }}
        </div>
        <div>总金额:¥{{ ((order.price || 0) / 100).toFixed(2) }}</div>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.container {
  .order_item {
    padding: 10px;
    margin: 10px;
    background: #fff;
    border-radius: 12px;
    .header {
      display: flex;
      justify-content: space-between;
      font-size: 12px;
      .create_time {
        color: #2f3640;
      }
      .status {
        color: #353b48;
      }
    }
    .goods_item {
      margin: 5px 0;
      background: #ffffff;
      border-radius: 10px;
    }
    .footer {
      font-size: 10px;
      color: #2f3640;
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin: 2px;
    }
  }
}
</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

#

到此,就完成了显示全部订单。

# 6,改变tab时变更数据

点击tab时,会触发一个事件,如下:

1684294056151

实现changeTab方法,如下:

1684294108760

浏览器中测试之,如下:

1684294136584

完成逻辑,如下:

1684294217612

浏览器测试之,如下:

1684294260474

1684294269015

# 十四,购物车页面

# 1,标题显示

购物车页面的路由,之前已经配置好了,对应的组件也创建好了,现在绘制一下头部,如下:

1684372078441

浏览器效果如下:

1684372091328

# 2,未登陆时显示

如果没有登录,访问购物车,是不能访问的,处理之,如下:

1684372497602

1684372526977

参考代码如下:

< script setup >
    import {
        getToken
    } from "@/utils/auth";
import {
    ref,
    computed
} from "vue";

// 控制登录状态
const isLogin = ref(false);
if (getToken()) {
    isLogin.value = true;
} <
/script>

<
template >
    <
    div class = "container" >
    <
    van - nav - bar: title = "$route.meta.title"
placeholder
fixed
left - arrow
@click - left = "$router.back()" /
    >
    <
    div v -
    if = "!isLogin" class = "no_login_content" >
    <
    van - empty class = "thumbnail"
description = "未登录" >
    <
    van - button
type = "primary"
class = "login_btn"
@click = "$router.push('/login')" >
    登录 < /van-button > <
    /van-empty> < /
    div > <
    /div> < /
    template >

    <
    style lang = "scss"
scoped >
    .container {
        .login_btn {
            width: 180 px;
        }
        main.content {
                margin - bottom: var (--van - submit - bar - height);
                .goods_list {
                    .goods_item {
                        position: relative;
                        .selected {
                            position: absolute;
                            z - index: 2;
                            left: 8 px;
                            top: 58 px;
                        }
                        .goods_box {
                            background: #f7f8fa;
                            margin - top: 10 px;
                            .goods_card {
                                margin - left: 30 px;
                            }
                        }
                        .delete - button {
                            width: 100 % ;
                            height: 100 % ;
                        }
                    }
                }
                .is_null {
                    display: flex;
                    flex - direction: column;
                    justify - content: center;
                    align - items: center;
                    height: 55 vh;
                    text - align: center;
                    p.cart_tip {
                        font - size: 18 px;
                    }
                    .to_btn {
                        margin - top: 16 px;
                        width: 180 px;
                    }
                }
            }
            .submit {
                bottom: var (--van - submit - bar - height);
            }
    } <
    /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

清除token,效果如下:

1684372590498

# 3,商品列表

如果已经登录了,需要获取商品的列表,我们要知道,购物车的数据存储在pinia中,之前已经调用过获取购物车数据的接口了,得到数据后,把数据存储到了pinia中了,如下:

1684372715781

现在切换到购物车商品,重新调用接口,获取数据,如下:

1684372986026

浏览器中测试之,如下:

1684373006610

现在购物车中是有数据的,数据在cartStore中,但是有可能购物车中没有数据,没有数据要显示购物车空空如也。如果有数据,需要把数据渲染出来,如下:

1684373291898

此时此时是有数据的,渲染数据如下,渲染数据,用到了如下的组件:

1684373597229

使用之,如下:

1684374530378

参考代码如下:

<script setup>
import { getToken } from "@/utils/auth";
import { ref, computed } from "vue";
import { useCartStore } from "@/stores/cart";

const cartStore = useCartStore();

const loading = ref(false);
const finished = ref(false);

// 控制登录状态
const isLogin = ref(false);
if (getToken()) {
  isLogin.value = true;
  // 当登录时获取购物车数据
  cartStore.changeCart();
}

const onLoad = async () => {};

const changeSelected = () => {};

const deleteGoods = () => {};
</script>

<template>
  <div class="container">
    <van-nav-bar
      :title="$route.meta.title"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
    <main v-if="isLogin" class="content">
      <!-- 购物车中可能有数据,也可能没有数据 -->
      <van-list
        class="goods_list"
        v-model:loading="loading"
        :finished="finished"
        v-if="cartStore.data?.length > 0"
        finished-text="没有更多了"
        @load="onLoad"
      >
        <van-swipe-cell class="goods_item" v-for="it in cartStore.data">
          <van-checkbox
            class="selected"
            v-model="it.checked"
            @select="changeSelected"
          />
          <div class="goods_box">
            <van-card
              class="goods_card"
              :key="it.id"
              :num="it.count"
              :price="((it.goods_info?.price / 100) * it.count).toFixed(2)"
              :desc="it.goods_info?.detail_info"
              :title="it.goods_info?.name"
              :thumb="it.goods_info?.pic_url"
            >
              <template #tags>
                <van-tag plain type="primary">{{
                  it.goods_info?.tags
                }}</van-tag>
              </template>
              <template #footer>
                <van-stepper @change="changeGoodsSum(it)" v-model="it.count" />
              </template>
            </van-card>
          </div>
          <template #right>
            <van-button
              square
              text="删除"
              type="danger"
              class="delete-button"
              @click="deleteGoods(it.id)"
            />
          </template>
        </van-swipe-cell>
      </van-list>
      <div class="is_null" v-else>
        <van-icon name="smile-o" size="50px" />
        <p class="cart_tip">购物车空空空如也</p>
        <van-button class="to_btn" type="primary" to="/home">去首页</van-button>
      </div>
    </main>
    <div v-if="!isLogin" class="no_login_content">
      <van-empty class="thumbnail" description="未登录">
        <van-button
          type="primary"
          class="login_btn"
          @click="$router.push('/login')"
          >登录</van-button
        >
      </van-empty>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.container {
  .login_btn {
    width: 180px;
  }
  main.content {
    margin-bottom: var(--van-submit-bar-height);
    .goods_list {
      .goods_item {
        position: relative;
        .selected {
          position: absolute;
          z-index: 2;
          left: 8px;
          top: 58px;
        }
        .goods_box {
          background: #f7f8fa;
          margin-top: 10px;
          .goods_card {
            margin-left: 30px;
          }
        }
        .delete-button {
          width: 100%;
          height: 100%;
        }
      }
    }
    .is_null {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      height: 55vh;
      text-align: center;
      p.cart_tip {
        font-size: 18px;
      }
      .to_btn {
        margin-top: 16px;
        width: 180px;
      }
    }
  }
  .submit {
    bottom: var(--van-submit-bar-height);
  }
}
</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

效果如下:

1684374558564

# 4,动作栏

绘制动作栏,如下:

1684375659383

参考代码如下:

 <van-submit-bar
      v-if="isLogin && cartStore.data?.length > 0"
      class="submit"
      price="100"
      button-text="提交订单"
    >
      <van-checkbox>全选</van-checkbox>
    </van-submit-bar>
    <van-back-top v-if="isLogin" right="20px" bottom="4rem" />
1
2
3
4
5
6
7
8
9

效果如下:

1684375696260

# 5,全选状态

上面已经把页面绘制差不多了,开始实现业务功能,先实现全选,给全选按钮绑定v-model,如下:

1684375894764

实现对应的计算属性,如下:

1684375918305

参考代码:

// 全选状态
const selectedAll = computed({
    get: () => cartStore.data?.every?.(it => it.checked),
    set: val => {
        cartStore.data.forEach?.(it => {
            it.checked = val;
        });
    },
});
1
2
3
4
5
6
7
8
9

浏览器测试之,如下:

1684375950368

# 6,总价

开始计算总价,绑定计算属性,如下:

1684376052606

实现totalPrice,如下:

1684376095004

参考代码如下:

// 总价
const totalPrice = computed(() =>
    cartStore.data
    ?.filter(it => it.checked)
    .reduce((pre, cur) => pre + cur.goods_info.price * cur.count, 0)
);
1
2
3
4
5
6

浏览器效果如下:

1684376134062

# 7,更改数量

实现商品的累加,给van-stepper绑定change事件,如下:

1684376439954

实现对应的方法,如下:

1684376459977

浏览器打印测试之,如下:

1684376478746

引入接口,调用接口,如下:

1684376500629

1684376583520

浏览器测试之,如下:

1684376665910

1684376697506

# 8,删除商品

实现删除商品,先封装api接口,如下:

1684376764766

在组件中,导入之,如下:

1684376790229

给删除按钮绑定点击事件,如下:

1684376826962

实现删除逻辑,如下:

1684376877546

参考代码如下:

// 删除商品时事件
const deleteGoods = async id => {
    const res = await deleteCart([id]);
    if (res.code == 1) {
        cartStore.changeCart();
    }
};
1
2
3
4
5
6
7

效果如下:

1684376933793

# 9,提交订单事件

分析如下:

1684376982997

绑定提交事件,如下:

1684377026736

实现对应的方法,如下:

1684377215490

1684377147201

参考代码如下:

// 提交事件
const onSubmit = () => {
    // 格式化数据
    const goods = cartStore.data
        .filter?.(it => it.checked)
        .map(it => ({
            goods_id: it.goods_id,
            count: it.count,
            cartId: it.id,
            goodsInfo: it.goods_info,
        }));
    if (goods.length > 0) {
        router.push({
            name: 'addressList',
            query: {
                orderInfo: btoa(
                    encodeURI(JSON.stringify({
                        goods,
                        price: totalPrice.value
                    }))
                ),
            },
        });
    }
};
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

浏览器测试之,如下:

1684377249178

# 十五,种草页面

src\views\Recommend\index.vue

# 1,标题

image-20230305213336662

效果

image-20230305213507582

# 2,api配置

src\api\recommend.js

import request from '@/utils/request';

/**
 * @description 文章列表
 * @param {{limit:number,page:number}} queyObj
 */
export const getArticleList = (queyObj, id) =>
    request({
        method: 'POST',
        url: '/frontend/article/list',
        params: queyObj,
        data: (() => {
            const data = new FormData();
            data.set('id', id);
            return data;
        })(),
    });

/**
 * @description 添加种草文章
 * @param {{title: string,desc:string,pic_url:string,detail:string}} articleInfo 文章信息
 */
export const addArticle = articleInfo =>
    request({
        method: 'POST',
        url: '/frontend/article/add',
        data: (() => {
            const formData = new FormData();
            Object.keys(articleInfo).forEach(key =>
                formData.set(key, articleInfo[key])
            );
            return formData;
        })(),
    });

/**
 * @description 我的文章列表
 * @param {{limit:number,page:number}} queryObj
 */
export const myArticleList = queryObj =>
    request({
        method: 'POST',
        url: '/frontend/article/list',
        params: queryObj,
    });

export const getArticleInfo = id =>
    request({
        method: 'POST',
        url: '/frontend/article/detail/',
        params: {
            id,
        },
    });
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

# 3,封装上滑加载函数

src\hooks\usePageLoad.js

import {
    ref
} from 'vue';

// 传入一个
export const usePageLoad = (apiCallback, queryParams) => {
    // 数据
    const data = ref([]);
    // 总数
    const count = ref(0);
    // 加载状态
    const loading = ref(false);
    // 完成状态
    const finished = ref(false);
    // 加载事件
    const onLoad = async () => {
        try {
            // 调用api
            const res = await apiCallback(queryParams);
            if (res.code === 1) {
                // 数量
                count.value = res.data.count;
                // 加载状态
                loading.value = false;
                if (res.data.list) {
                    // 新增数据
                    data.value.push(...res.data.list);
                }
            }
        } catch (error) {
            console.error(error);
        }
        // 每次判断总数和每次获取数之比,当已请求数page数大于总数和每次获取数之比时就停止加载
        if (queryParams.page++ > parseInt(count.value / queryParams.limit) + 1) {
            finished.value = true;
            return;
        }
    };
    return {
        data,
        onLoad,
        loading,
        finished,
        count
    };
};
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

# 4,封装ArticleList组件

src\components\ArticleList.vue

<script setup>
import { usePageLoad } from '@/hooks/usePageLoad.js';
// 接收参数
const props = defineProps({
  articleData: {
    type: Array,
    default: [],
  },
  apiFn: {
    type: Function,
    default: () => [],
  },
  params: {
    type: Object,
    default: {},
  },
  status: {
    type: Number,
    default: 2,
  },
});

// 把传进来的参数传入
const { data, onLoad, loading, finished } = usePageLoad(
  props.apiFn,
  props.params
);
</script>

<template>
  <div>
    <van-list
      class="content"
      v-model:loading="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
      offset
    >
      <van-card
        class="item"
        v-for="article in data"
        :thumb="article.pic_url"
        @click="
          props.status == 2
            ? $router.push({
                path: '/article-detail',
                query: {
                  articleId: article.id,
                },
              })
            : $router.push({
                name:'goodsDetail',
                params: {
                  goodsId: article.id,
                },
              })
        "
      >
        <template #title>
          <p class="title">
            {{ article.title }}
          </p>
        </template>
        <template #desc
          ><p class="detail">{{ article.detail }}</p></template
        >
        <template #price>
          <p class="desc">
            {{ article.desc }}
          </p>
        </template>
        <template #num>
          <p>{{ article.created_at }}</p>
        </template>
      </van-card>
    </van-list>
  </div>
</template>

<style lang="scss" scoped>
.content {
  .item {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    .title {
      font-size: 16px;
      color: black;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .detail {
      margin-top: 6px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      color: rgb(105, 105, 105);
    }
    .desc {
      color: rgb(105, 105, 105);
      font-weight: normal;
    }
  }
}
</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

# 5,绘制页面

src\views\Recommend\index.vue

<script setup>
import { getArticleList } from '@/api/recommend';
import ArticleList from '@/components/ArticleList.vue';
</script>

<template>
  <div class="container">
    <van-nav-bar
      :title="$route.meta.title"
      placeholder
      fixed
      @click-left="$router.back()"
      left-arrow
      @click-right="$router.push('/add-article')"
    >
      <template #right>
        <van-icon name="add-o" size="18" />
      </template>
    </van-nav-bar>
    <ArticleList
      class="list"
      :api-fn="getArticleList"
      :params="{ page: 1, limit: 10 }"
    />
    <van-back-top right="20px" bottom="60px" />
  </div>
</template>

<style lang="scss" scoped>
.list {
  margin: 10px;
}
</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

# 十六,文章详情

# 1,配置路由

{
    path: '/article-detail',
    name: 'articleDetail',
    component: () => import('@/views/Recommend/Detail.vue'),
    meta: {
        login: true,
        title: '文章详情',
    },
},
1
2
3
4
5
6
7
8
9

# 2,api接口配置

src\api\cattle.js 点赞接口

import request from '@/utils/request';

/**
 * @description 获取点赞列表
 * @param {{type:string,page:string,limit:string}} queryObj
 */
export const getCattleByType = queryObj =>
    request({
        method: 'POST',
        url: '/frontend/praise/list/',
        params: queryObj,
    });

export const addCattle = ({
        type,
        object_id
    }) =>
    request({
        method: 'POST',
        url: '/frontend/praise/add/',
        data: (() => {
            const data = new FormData();
            data.set('type', type);
            data.set('object_id', object_id);
            return data;
        })(),
    });

export const deleteCattleById = id =>
    request({
        method: 'POST',
        url: '/frontend/praise/delete/',
        data: (() => {
            const data = new FormData();
            data.set('id', id);
            return data;
        })(),
    });

/**
 * @description 取消点赞(根据类型+对象id)
 */
export const deleteCattleByType = ({
        type,
        object_id
    }) =>
    request({
        method: 'POST',
        url: '/frontend/praise/delete/',
        data: (() => {
            const data = new FormData();
            data.set('type', type);
            data.set('object_id', object_id);
            return 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
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

src\api\comment.js 评论接口

import request from '@/utils/request';

export const getComments = info =>
    request({
        method: 'POST',
        url: '/frontend/comment/add/',
        data: () => {
            const data = new FormData();
            Object.keys(info).forEach(it => {
                data.set(it, info[it]);
            });
            return data;
        },
    });

export const addCommentApi = info =>
    request({
        method: 'POST',
        url: '/frontend/comment/add/',
        data: (() => {
            const data = new FormData();
            Object.keys(info).forEach(key => {
                data.append(key, info[key]);
            });
            return 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
27

# 3,标题

image-20230305221155683

# 4,内容绘制

import {
    ref,
    onMounted,
    computed
} from 'vue';
import {
    getArticleInfo
} from '@/api/recommend';
import {
    addCattle,
    deleteCattleByType
} from '@/api/cattle';
import {
    addCollection,
    deleteCollectionByType
} from '@/api/collection';
import {
    useRoute,
    useRouter
} from 'vue-router';
import {
    addCommentApi
} from '@/api/comment';

const route = useRoute();
const router = useRouter();
// 文章id
const articleId = route.query.articleId;
// 控制收藏
const isCollection = ref(false);
// 控制点赞
const isThumbs = ref(false);

if (!articleId) {
    router.replace('/recommend');
}
// 文章信息
const info = ref({});

// 获取服务器数据
const changeData = async () => {
    if (!articleId) return;
    const res = await getArticleInfo(articleId);
    if (res.code === 1) {
        // 评论反序
        res.data.comments = res.data.comments?.reverse?.();
        info.value = res.data;
    }
};

onMounted(async () => {
    await changeData();
    isThumbs.value = info.value.is_praise;
    isCollection.value = info.value.is_collect;
});
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
  <div class="content">
      <van-image class="img" fit="cover" :src="info.picUrl" />
      <div class="body">
        <p class="title">{{ info.title }}</p>
        <div class="info">
          <p class="desc">{{ info.desc }}</p>
          <p class="created_time">{{ info.createdAt }}</p>
        </div>
        <div class="detail">
          {{ info.detail }}
        </div>
      </div>
    </div>
1
2
3
4
5
6
7
8
9
10
11
12
13

效果如下

image-20230305221639772

# 5,评论绘制

image-20230306095133894

效果如下

image-20230306095149505

# 6,动作栏

image-20230306103850099

 <div class="action_box">
      <div class="add_comment" @click="show = true">
        <van-icon name="edit" size="20px" />&nbsp;评论
      </div>
      <div class="action">
        <div class="Thumbs_action">
          <van-icon
            color="#3498db"
            name="thumb-circle"
            :badge="info.praise || ''"
            :badge-props="{ offset: ['10px', '5px'] }"
          />
        </div>
        <div class="star_action">
          <van-icon
            color="#f39c12"
            :badge="info.collect || ''"
            name="star"
            :badge-props="{ offset: ['10px', '5px'] }"
          />
        </div>
      </div>
    </div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

效果如下

image-20230306103957969

# 7,弹出框

 <van-popup
      class="comment_box"
      safe-area-inset-bottom
      v-model:show="show"
      position="bottom"
    >
      <div class="comment_header">
        <div>评论</div>
        <div class="left">发布</div>
      </div>
      <van-field
        label-align="top"
        v-model.trim="comment"
        rows="6"
        type="textarea"
        maxlength="200"
        placeholder="请输入评论"
        show-word-limit
      />
    </van-popup>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

效果如下

image-20230306104035993

# 8,点赞逻辑

image-20230306104233068

// 点赞逻辑
const giveTheThumbsUp = async () => {
    if (isThumbs.value) {
        // 取消点赞逻辑
        await deleteCattleByType({
            type: 2,
            object_id: info.value.id
        });
    } else {
        // 点赞逻辑
        await addCattle({
            type: 2,
            object_id: info.value.id
        });
    }
    // 重新获取服务器数据验证
    const newDetail = await getArticleInfo(articleId);
    info.value.praise = newDetail.data.praise;
    isThumbs.value = info.value.is_praise = newDetail.data.is_praise;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

效果如下

image-20230306104519564

# 9,收藏逻辑

image-20230306110040290

// 收藏
const collection = async () => {
    if (isCollection.value) {
        // 取消收藏
        await deleteCollectionByType({
            type: 2,
            object_id: info.value.id,
        });
    } else {
        // 收藏
        await addCollection({
            type: 2,
            object_id: info.value.id
        });
    }
    // 同步数据
    const newDetail = await getArticleInfo(articleId);
    info.value.collect = newDetail.data.collect;
    isCollection.value = info.value.is_collect = newDetail.data.is_collect;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

效果如下

image-20230306110235917

# 10,新增评论

image-20230306110345691

// 评论
const addComment = async () => {
    if (!comment.value.length) return;
    // 新增评论
    await addCommentApi({
        type: 2,
        object_id: info.value.id,
        content: comment.value,
    });
    // 隐藏评论框
    show.value = false;
    // 情况输入框
    comment.value = '';
    // 重新获取数据
    changeData();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

效果如下

image-20230306111255027

# 参考代码

<script setup>
import {
    ref,
    onMounted,
    computed
} from 'vue';
import {
    getArticleInfo
} from '@/api/recommend';
import {
    addCattle,
    deleteCattleByType
} from '@/api/cattle';
import {
    addCollection,
    deleteCollectionByType
} from '@/api/collection';
import {
    useRoute,
    useRouter
} from 'vue-router';
import {
    addCommentApi
} from '@/api/comment';

const route = useRoute();
const router = useRouter();
// 文章id
const articleId = route.query.articleId;
// 控制收藏
const isCollection = ref(false);
// 控制点赞
const isThumbs = ref(false);

if (!articleId) {
    router.replace('/recommend');
}
// 文章信息
const info = ref({});

// 获取服务器数据
const changeData = async () => {
    if (!articleId) return;
    const res = await getArticleInfo(articleId);
    if (res.code === 1) {
        // 评论反序
        res.data.comments = res.data.comments?.reverse?.();
        info.value = res.data;
    }
};

onMounted(async () => {
    await changeData();
    isThumbs.value = info.value.is_praise;
    isCollection.value = info.value.is_collect;
});

// 点赞逻辑
const giveTheThumbsUp = async () => {
  if (isThumbs.value) {
    // 取消点赞逻辑
    await deleteCattleByType({ type: 2, object_id: info.value.id });
  } else {
    // 点赞逻辑
    await addCattle({ type: 2, object_id: info.value.id });
  }
  // 重新获取服务器数据验证
  const newDetail = await getArticleInfo(articleId);
  info.value.praise = newDetail.data.praise;
  isThumbs.value = info.value.is_praise = newDetail.data.is_praise;
};
// 收藏
const collection = async () => {
    if (isCollection.value) {
        // 取消收藏
        await deleteCollectionByType({
            type: 2,
            object_id: info.value.id,
        });
    } else {
        // 收藏
        await addCollection({
            type: 2,
            object_id: info.value.id
        });
    }
    // 同步数据
    const newDetail = await getArticleInfo(articleId);
    info.value.collect = newDetail.data.collect;
    isCollection.value = info.value.is_collect = newDetail.data.is_collect;
};
// 控制输入框显示
const show = ref(false);
// 评论收集
const comment = ref('');

const isHasComment = computed(() =>
  comment.value.length > 0 ? '#2ecc71' : '#ccc'
);

// 评论
const addComment = async () => {
    if (!comment.value.length) return;
    // 新增评论
    await addCommentApi({
        type: 2,
        object_id: info.value.id,
        content: comment.value,
    });
    // 隐藏评论框
    show.value = false;
    // 情况输入框
    comment.value = '';
    // 重新获取数据
    changeData();
};
</script>

<template>
  <div class="container">
    <van-nav-bar
      :title="$route.meta.title"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
    <div class="content">
      <van-image class="img" fit="cover" :src="info.picUrl" />
      <div class="body">
        <p class="title">{{ info.title }}</p>
        <div class="info">
          <p class="desc">{{ info.desc }}</p>
          <p class="created_time">{{ info.createdAt }}</p>
        </div>
        <div class="detail">
          {{ info.detail }}
        </div>
      </div>
    </div>
    <div class="comment" v-if="info.comments">
      <p>评论:</p>
      <div class="content_item" v-for="it in info.comments">
        <div class="user_pic">
          <img :src="it.user.avatar" :alt="it.user.name" />
        </div>
        <div class="item_left">
          <div class="user_name">
            {{ it.user.name }}&emsp;<span class="createdTime">{{
              it.created_at
            }}</span>
          </div>
          <div class="user_content">{{ it.content }}</div>
        </div>
      </div>
    </div>
    <div class="action_box">
      <div class="add_comment" @click="show = true">
        <van-icon name="edit" size="20px" />&nbsp;评论
      </div>
      <div class="action">
        <div class="Thumbs_action" @click="giveTheThumbsUp">
          <van-icon
            :color="isThumbs ? '#3498db' : '#767676'"
            :name="`thumb-circle${isThumbs ? '' : '-o'}`"
            :badge="info.praise || ''"
            :badge-props="{ offset: ['10px', '5px'] }"
          />
        </div>
        <div class="star_action" @click="collection">
          <van-icon
            :color="isCollection ? '#f39c12' : '#767676'"
            :badge="info.collect || ''"
            :name="`star${isCollection ? '' : '-o'}`"
            :badge-props="{ offset: ['10px', '5px'] }"
          />
        </div>
      </div>
    </div>
    <van-popup
      class="comment_box"
      safe-area-inset-bottom
      v-model:show="show"
      position="bottom"
    >
      <div class="comment_header">
        <div>评论</div>
        <div class="left" @click="addComment">发布</div>
      </div>
      <van-field
        label-align="top"
        v-model.trim="comment"
        rows="6"
        type="textarea"
        maxlength="200"
        placeholder="请输入评论"
        show-word-limit
      />
    </van-popup>
  </div>
</template>

<style lang="scss" scoped>
.container {
  background-color: #fff;
  .content {
    .body {
      padding: 0 12px;
      .title {
        margin-top: 10px;
        font-size: 18px;
        font-weight: bold;
        text-align: center;
      }
      .info {
        margin-top: 10px;
        display: flex;
        color: gray;
        justify-content: space-around;
        justify-items: center;
        line-height: 100%;
        .desc {
          font-size: 14px;
        }
        .created_time {
          font-size: 14px;
        }
      }
      .detail {
        margin-top: 10px;
        text-indent: 2em;
        font-size: 16px;
        word-wrap: break-word;
        word-break: normal;
        white-space: normal;
      }
    }
  }
  .comment {
    border-top: 1px solid #ccc;
    margin-top: 10px;
    background: #f6f6f6;
    padding: 6px;
    padding-bottom: 50px;
    p {
      font-size: 16px;
    }
    .content_item {
      background: #fff;
      padding: 6px;
      display: flex;
      border-radius: 12px;
      margin-top: 12px;
      .user_pic {
        width: 42px;
        height: 42px;
        position: relative;
        img {
          width: 42px;
          height: 42px;
          object-fit: cover;
          border-radius: 50%;
        }
      }
      .item_left {
        margin-left: 10px;
        font-size: 14px;
        width: 100%;
        .user_name {
          margin-bottom: 6px;
          .createdTime {
            color: #86909c;
          }
        }
        .user_content {
          word-wrap: break-word;
          word-break: break-all;
        }
      }
    }
  }
  .action_box {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: 42px;
    background: #fff;
    border-top: 1px solid rgb(232, 232, 232);
    box-shadow: 0px -2px 10px rgb(232, 232, 232);
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    .add_comment {
      width: 100%;
      padding-left: 16px;
      height: 100%;
      display: flex;
      justify-content: left;
      align-items: center;
      font-size: 14px;
      color: #767676;
      &:active {
        background: #e8e8e8;
      }
    }
    .action {
      height: 100%;
      width: 100%;
      --van-badge-color: black;
      display: flex;
      // justify-content: space-between;
      align-items: center;
      font-size: 20px;
      color: #767676;
      div {
        display: flex;
        align-items: center;
        justify-content: left;
        padding: 6px 6px 6px 26px;
        height: 100%;
        width: 100%;
        :deep(.van-badge) {
          color: #767676;
          background-color: rgba(0, 0, 0, 0);
          border: none;
          font-size: 8px;
          font-weight: normal;
        }
        &:active {
          background: #e8e8e8;
          height: 32px;
        }
      }
    }
  }
  .comment_box {
    width: 100%;
    height: 300px;
    .comment_header {
      padding: 10px 16px 0 16px;
      font-size: 16px;
      display: flex;
      justify-content: space-between;
      .left {
        color: v-bind(isHasComment);
      }
    }
  }
}
</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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352

# 十七,新增文章

# 1,配置路由

  {
      path: '/add-article',
      name: 'addArticle',
      component: () => import('@/views/Recommend/Create.vue'),
      meta: {
          login: true,
          title: '新增文章',
      },
  },
1
2
3
4
5
6
7
8
9

# 2,绘制页面

src\views\Recommend\Create.vue

<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { addArticle } from '@/api/recommend';
import { showSuccessToast } from 'vant';
// 标题
const title = ref('');
// 描述
const desc = ref('');
// 图片url
const pic_url = ref('');
// 详情
const detail = ref('');
// 进度条
const adding = ref(false);
const router = useRouter();
// 提交事件
const onSubmit = async form => {
  if (form) {
    // 开始加载
    adding.value = true;
    // 发起请求
    const res = await addArticle(form);
    // 加载完成
    adding.value = false;
    // 重置输入框
    title.value = '';
    desc.value = '';
    pic_url.value = '';
    detail.value = '';
    if (res.code === 1) {
      router.back();
      showSuccessToast({
        message: '新增成功',
        duration: 1500,
      });
    }
  }
};
</script>

<template>
  <van-nav-bar
    :title="$route.meta.title"
    placeholder
    fixed
    left-arrow
    @click-left="$router.back()"
  />
  <van-form @submit="onSubmit">
    <van-cell-group inset>
      <van-field
        v-model.trim="title"
        name="title"
        label="标题"
        placeholder="文章标题"
        :rules="[{ required: true, message: '请填写文章标题' }]"
      />
      <van-field
        v-model.trim="desc"
        name="desc"
        label="描述"
        rows="2"
        placeholder="文章描述"
        type="textarea"
        :rules="[{ required: true, message: '请填写文章描述' }]"
      />
      <van-field
        v-model="pic_url"
        name="pic_url"
        rows="3"
        label="图片"
        type="textarea"
        placeholder="请输入图片地址"
        :rules="[{ required: true, message: '请填写图片地址' }]"
      />
      <van-field
        v-model="detail"
        name="detail"
        rows="8"
        label="文章内容"
        autosize
        type="textarea"
        placeholder="请输入文章内容"
        maxlength="2000"
        show-word-limit
        :rules="[{ required: true, message: '请填写文章内容' }]"
        label-align="top"
      />
    </van-cell-group>
    <div style="margin: 16px">
      <van-button
        round
        block
        type="primary"
        :loading="adding"
        native-type="submit"
      >
        提交
      </van-button>
    </div>
  </van-form>
</template>

<style lang="scss" scoped>
.van-form {
  margin-top: 16px;
}
</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

效果如下

image-20230306144726515

# 十八,我的页面

src\views\User\index.vue

<script setup>
import { logout, getUserInfo } from '@/api/auth';
import { getToken, removeToken } from '@/utils/auth';
import { useUserStore } from '@/stores/user';
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { resetLogin } from '@/utils/userCache';
import getAvatar from '@/utils/avatar';
import { lastLoginUser } from '@/utils/userCache';

const userStore = useUserStore();
const isLogin = ref(false);
const router = useRouter();
// 获取上次登录的信息
const currentUser = lastLoginUser();

onMounted(async () => {
  if (getToken()) {
// 设置上次登录的信息
    userStore.setInfo(currentUser);
    isLogin.value = true;
    // 重新请求数据验证token是否过期或用户验证修改
    await getUserInfo(currentUser);
  } else {
    // 没有token就重置
    userStore.reset();
  }
});

const toUserInfo = () => {
  // 去往用户详情页面
  router.push('/user-info');
};

// 退出登录
const logOut = async _ => {
  const res = await logout(getToken());
  if (res.code === 1) {
    // 清除token
    removeToken();
    // 重置上次登录
    resetLogin();
    // 刷新页面
    router.go(0);
  }
};
</script>

<template>
  <main class="container">
    <van-nav-bar
      :title="$route.meta.title"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
    <div v-if="isLogin" class="content">
      <van-card
        class="user_card"
        :thumb="userStore.avatar ? userStore.avatar : getAvatar()"
        @click="toUserInfo"
      >
        <template #title>
          <p class="user_name">{{ userStore.name }}</p>
        </template>
        <template #tags>
          <p class="user_sign">{{ userStore.sign }}</p></template
        >
        <template #desc>
          <p class="user_sex">{{ userStore.sex ? '男' : '女' }}</p>
        </template>
      </van-card>
      <van-cell
        icon="logistics"
        title="我的收货地址"
        is-link
        to="/address-list"
      />
      <van-cell icon="orders-o" title="我的订单" is-link to="/order" />
      <van-cell icon="star-o" title="我的收藏" is-link to="/my-collection" />
      <van-cell icon="good-job-o" title="我的点赞" is-link to="/thumbs-up" />
      <div class="logout_box">
        <van-button
          v-if="isLogin"
          class="logout_btn"
          type="warning"
          @click="logOut"
          >退出登录</van-button
        >
      </div>
    </div>
    <div v-else class="no_login_content">
      <van-empty class="thumbnail" description="未登录">
        <van-button
          type="primary"
          class="login_btn"
          @click="router.push('/login')"
          >登录</van-button
        >
      </van-empty>
    </div>
  </main>
</template>

<style lang="scss" scoped>
main.container {
  .user_card {
    background: #fff;
    margin-bottom: 10px;
    .user_name {
      margin-top: 2px;
      font-size: 20px;
    }
    .user_sex {
      margin: 6px 0;
    }
  }
  .content {
    width: 100%;
    .logout_box {
      margin-top: 18px;
      padding: 0 10px;
      .logout_btn {
        width: 100%;
      }
    }
  }
  .no_login_content {
    .login_btn {
      width: 180px;
    }
  }
}
</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

效果如下

image-20230306151339090

# 十九,个人信息页面

# 1,配置路由

 {
     path: '/user-info',
     name: 'userInfo',
     component: () => import('@/views/User/Info.vue'),
     meta: {
         login: true,
         title: '个人信息',
     },
 },
1
2
3
4
5
6
7
8
9

# 2,绘制页面

src\views\User\Info.vue

<script setup>
</script>

<template>
  <main class="container">
    <van-nav-bar
      :title="$route.meta.title"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
    <div class="content">
      <van-cell title="修改密码" is-link to="/edit-pwd" />
    </div>
  </main>
</template>

<style lang="scss" scoped>
.container {
  .content {
    .edit_box {
      padding: 10px;
      .edit_btn {
        width: 100%;
      }
    }
  }
}
</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

# 二十,修改密码

# 1,配置路由

{
    path: '/edit-pwd',
    name: 'editPwd',
    component: () => import('@/views/User/EditPwd.vue'),
    meta: {
        login: true,
        title: '修改密码',
    },
},
1
2
3
4
5
6
7
8
9

# 2,绘制页面

<script setup>
import { ref } from 'vue';
import { resetPwd } from '@/api/auth';
import { showSuccessToast } from 'vant';

// 注册时密保
const secretAnswer = ref('');
// 密码
const pwd = ref('');
// 加载状态
const editing = ref(false);

// 提交事件
const onSubmit = async ({ secret_answer, password }) => {
  // 打开加载状态
  editing.value = true;
  try {
    // 修改密码请求
    const res = await resetPwd(password, secret_answer);
    if (res.code === 1) {
      showSuccessToast({
        message: '修改成功',
        duration: 1500,
      });
    }
  } catch (error) {
    console.error(error);
  }
  // 关闭加载状态
  editing.value = false;
};
</script>

<template>
  <main class="container">
    <van-nav-bar
      :title="$route.meta.title"
      placeholder
      fixed
      left-arrow
      @click-left="$router.back()"
    />
    <div class="content">
      <van-form @submit="onSubmit">
        <van-cell-group inset>
          <van-field
            v-model.trim="secretAnswer"
            name="secret_answer"
            label="密保"
            placeholder="密保"
            :rules="[{ required: true, message: '请填写密保' }]"
          />
          <van-field
            v-model.trim="pwd"
            type="password"
            name="password"
            label="新密码"
            placeholder="新密码"
            :rules="[
              { required: true, message: '请填写密码' },
              {
                pattern: /^[-_a-zA-Z0-9]{6,16}$/,
                message: '只能包含6-16位字母数字下划线减号',
              },
            ]"
          />
        </van-cell-group>
        <div style="margin: 16px">
          <van-button
            :loading="editing"
            round
            block
            type="primary"
            native-type="submit"
          >
            提交
          </van-button>
        </div>
      </van-form>
    </div>
  </main>
</template>

<style lang="scss" scoped>
.container {
  .content {
    margin-top: 10px;
  }
}
</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

效果如下

image-20230306153219305

# 二十一,我的收藏

# 1,配置路由

{
    path: '/my-collection',
    name: 'myCollection',
    component: () => import('@/views/User/Collection.vue'),
    meta: {
        login: true,
        title: '我的收藏',
    },
},
1
2
3
4
5
6
7
8
9

# 2,配置状态

记录标签缓存

src\stores\collection.js

import {
    ref,
    computed
} from 'vue';
import {
    defineStore
} from 'pinia';

export const useCollectionStore = defineStore('collection', () => {
    const active = ref('1');
    const changeActive = status => {
        active.value = status;
    };
    return {
        active,
        changeActive
    };
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3,绘制页面

<script setup>
import { onMounted, ref, watch, onBeforeUnmount } from 'vue';
import { getCollectionList } from '@/api/collection';
import ArticleList from '@/components/ArticleList.vue';
import { useCollectionStore } from '@/stores/collection';

const collectionStore = useCollectionStore();

const active = ref(collectionStore.active);

const stopActiveWatch = watch(active, val => {
  collectionStore.changeActive(val);
});

// 卸载之前停止监听
onBeforeUnmount(() => {
  stopActiveWatch();
});

// 获取数据
const getCollectListByType = async params => {
  const res = await getCollectionList(params);
  if (active.value == 1) {
    const dataList = res.data.list;
    // 格式化数据
    res.data.list = dataList?.map(item => ({
      ...item,
      title: item.goods.name,
      desc: (item.goods.price / 100).toFixed(2),
      pic_url: item.goods.picUrl,
      id: item.object_id,
    }));
    return res;
  }
  const dataList = res.data.list;
  // 格式化数据
  res.data.list = dataList
    ?.map(item => ({
      ...item,
      title: item.article.title,
      desc: item.article.desc,
      pic_url: item.article.picUrl,
      id: item.object_id,
    }))
    .reverse();
  return res;
};

// 改变tab时触发
const changeType = async () => {
  // 重新获取数据
  await getCollectionList({
    limit: 100,
    page: 1,
    type: active.value,
  });
};

// 点击时重新获取数据
onMounted(async () => {
  await changeType();
});

// 点击时重新获取数据
const onClickTab = async type => {
  await changeType();
};
</script>

<template>
  <van-nav-bar
    :title="$route.meta.title"
    placeholder
    fixed
    @click-left="$router.back()"
    left-arrow
  />
  <van-tabs
    v-model:active="active"
    @change="onClickTab"
    offset-top="46"
    sticky
    swipeable
  >
    <van-tab name="1" title="商品"
      ><div class="item-container">
        <ArticleList
          class="list"
          :status="1"
          :api-fn="getCollectListByType"
          :params="{ type: 1, page: 1, limit: 10 }"
        /></div
    ></van-tab>
    <van-tab name="2" title="文章">
      <div class="item-container">
        <ArticleList
          class="list"
          :api-fn="getCollectListByType"
          :params="{ type: 2, page: 1, limit: 10 }"
        />
      </div>
    </van-tab>
  </van-tabs>
  <div></div>
</template>

<style lang="scss" scoped>
.item-container {
  padding: 10px 10px 0px 10px;
}
</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

效果如下

image-20230306160439069

只要不刷新页面,每次进入当前页面就会记忆退出时的页面

# 二十二,我的点赞页面

# 1,配置路由

{
    path: '/thumbs-up',
    name: 'ThumbsUp',
    component: () => import('@/views/User/ThumbsUp.vue'),
    meta: {
        title: '我的点赞',
        login: true,
    },
},
1
2
3
4
5
6
7
8
9

# 2,绘制页面

src\views\User\ThumbsUp.vue

<script setup>
import { getCattleByType } from '@/api/cattle';
import ArticleList from '@/components/ArticleList.vue';
import { onMounted, ref } from 'vue';

// 数据
const dataList = ref([]);

// 获取数据
const getCattleListByType = async params => {
  const res = await getCattleByType(params);
  const dataList = res.data.list;
  // 格式化数据
  res.data.list = dataList
    ?.map(item => ({
      ...item,
      title: item.article.title,
      desc: item.article.desc,
      pic_url: item.article.picUrl,
      id: item.object_id,
    }))
    .reverse();
  return res;
};

onMounted(async () => {
  // 获取数据
  const res = await getCattleByType({ type: 2, page: 1, limit: 20 });
  dataList.value = res.data.list;

});
</script>

<template>
  <slot name="hello">

  </slot>
  <van-nav-bar
    :title="$route.meta.title"
    placeholder
    fixed
    @click-left="$router.back()"
    left-arrow
  />
  <div class="container">
    <ArticleList
      class="list"
      :api-fn="getCattleListByType"
      :params="{ type: 2, page: 1, limit: 20 }"
    />
  </div>
</template>

<style lang="scss" scoped>
.container {
  padding: 10px 10px 0px 10px;
}
</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
Last Updated: 7/24/2023, 8:24:40 AM