11-Vue3.x新版码路严选
# 一,项目准备工作
前台接口:https://www.apifox.cn/apidoc/shared-a11f6c11-eaa8-456a-85ae-0e2a97cb561c
中后台接口:https://www.apifox.cn/apidoc/shared-38c2b1bf-75e7-4146-a849-507798f38fdb
# 1,创建项目
创建项目命令如下:
进入项目,安装依赖,运行项目,如下:
在浏览器中测试之,如下:
# 2,配置vant
**vant的地址:**https://vant-contrib.gitee.io/vant/#/zh-CN
- 第一步:安装最新版的vant,安装自动引入插件,如下:
在vite.config.js中配置,如下:
Vant 中有个别组件是以函数的形式提供的,包括 Toast
, Dialog
, Notify
和 ImagePreview
组件。在使用函数组件时, unplugin-vue-components
无法自动引入对应的样式,因此需要手动引入样式。引入如下:
测试vant中的组件是否可以使用,如下:
浏览器中测试如下:
# 3,配置scss
安装:
测试如下:
浏览器中测试效果如下:
# 4,配置rem移动端适配
安装对应的依赖,如下:
创建postcss.config.js,并配置如下:
参考代码:
module.exports = {
plugins: {
/**
* 自动px转rem
*/
'postcss-pxtorem': {
// 一个元素是75px ===> 2rem
rootValue: 37.5,
propList: ['*'],
},
/**
* 自动配置浏览器样式
*/
'postcss-preset-env': {},
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
适配的js,如下:
参考代码如下:
// 首先是一个立即执行函数,执行时传入的参数是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 {};
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
重置样式,如下:
参考重置样式,如下:
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;
}
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和重置样式,如下:
测试rem适配是否起作用,如下:
浏览器中测试如下:
# 5,配置全局样式
创建全局样式,如下:
参考代码如下:
html,
body,
#app {
height: 100%;
width: 100%;
background: #F6F6F6;
// vant自带变量
--van-card-background: #fff;
}
2
3
4
5
6
7
8
9
在main.js中引入全局样式,如下:
效果如下:
# 6,配置jsconfig
配置jsconfig.json,开启路径别名提示,如下:
参考代码如下:
{
"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/*"
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
测试如下:
# 二,创建基本组件并配置路由
# 1,创建基本组件并配置路由
删除views下面的代码,并创建对应的组件,如下:
配置对应的路由,如下:
参考代码如下:
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;
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组件,如下:
在App.vue中引入,使用之,如下:
注释掉创建项目时的默认样式,如下:
浏览器测试如下:
# 三,首页面绘制
# 1,配置axios
安装axios,如下:
创建utils文件夹,在utils文件夹下创建request.js,如下:
baseURL:"http://8.218.112.99",
# 2,配置api接口
创建api文件夹,封装首页面的api接口,如下:
参考代码如下:
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,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3,绘制轮播图数据
先解决跨域问题:
参考代码如下:
server: {
proxy: {
"/api": {
target: "http://8.218.112.99",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
2
3
4
5
6
7
8
9
分析了接口地址如下:
改变baseURL,如下:
测试数据数据如下:
在首页面中获取数据,渲染数据,如下:
打开调试工具,如下:
参考代码如下:
<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>
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
浏览器中测试如下:
# 4,渲染首页面中的商品列表
在components目录下创建Goods组件,并书写代码,如下:
参考代码如下:
<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>
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组件,如下:
效果如下:
开启请求显示进度条,安装对应的依赖,如下:
在路由前置守卫中开启,在后置守卫中关闭,如下:
浏览器中测试如下:
# 四,登录注册
# 1,配置路由
创建一个登录页面,如下:
配置对应的路由,如下:
参考代码如下:
{
path: '/login',
name: 'login',
component: () => import('@/views/User/Login.vue'),
meta: {
// 标题
title: '登录',
},
},
2
3
4
5
6
7
8
9
访问之,如下:
# 2,api接口封装
封装api接口,如下:
参考代码如下:
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
})()
});
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一样。如下:
参考代码如下:
// 默认头像地址
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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在登录组件中就需要获取一个头像了,如下:
在浏览器中测试之,如下:
把用户注册的相关的信息定义出来,如下:
把样式处理之,如下:
参考代码如下:
.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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
每次刷新页面,头像都不一样,效果如下:
# 5,登录表单绘制
在头像下面,开始绘制表单,如下:
参考代码如下:
<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>
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
效果如下
# 6,表单提交事件
实现onSubmit方法,这个方法的形参就是收集到的数据,如下:
浏览器中效果如下:
实现登录,看服务器返回的结果,如下:
实现提示,实现跳转,如下 :
效果如下
# 7,注册
注册的表单也是写在登录组件中的,分析如下:
定义一个状态,这个状态来控制上面多的表单和按钮是否显示,如下:
然后去定义一些表单,如下:
参考代码如下:
<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>
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
效果如下
# 8,处理注册
昨天有一个地方写错了,如下:
在Login.vue中导入接口,去调用接口,如下:
效果如下:
# 五,完善登录流程
# 1,分析需求
打开上线的项目,注册几个账号,观察cookie,和loacalstorege,如下:
点击注册,去观察,如下:
去登录,再测试之,如下:
然后,退出登录,使用之前注册的账号demo01,去登录,再分析,如下:
点击后,如下:
然后,使用demo01去登录,如下:
登录后,分析,如下:
现在去网页关闭掉,如下:
# 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
创建的项目默认就是使用的第二种方式,如下:
在这个项目中,我们使用第二种方式。如下:
参考代码如下:
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
};
});
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,如下:
把操作cookie进行封装,封装工具函数,如下:
参考代码如下:
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);
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):
参考代码如下:
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)
})
}
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中)
之前已经实现登录成功了,如下:
登录成功后,要做什么呢?如下:
浏览器测试之,如下 :
# 5,已登录的头像回显
和QQ登录一样,输入QQ号之后,显示已经登录的QQ号头像。实现监听用户名的变化,如下:
浏览器中测试之,如下:
用户名变了,根据用户名,调用getData,得到用户信息,有了用户信息,就可以得到头像,有了头像就可以回显了。代码如下:
在浏览器中输入一个a,如下:
如果没有头像就给默认头像,如下:
浏览器测试之,如下:
参考代码如下:
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;
}
});
2
3
4
5
6
7
8
9
10
11
# 6,导航标题动态变换
效果如下
登录或注册页面不需要tabbar,隐藏tabbar,如下:
效果如下:
# 六,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
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,表示登录了,你登录了,就不能去登录了,从哪来还去哪。代码如下:
浏览器测试之,如下:
登录之,如下:
参考代码如下:
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
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,创建商品详情组件并配置路由
创建商品组件,如下:
配置路由,如下:
参考代码:
{
path: '/goods/:goodsId',
name: 'goodsDetail',
component: () => import('@/views/Home/GoodsDetail.vue'),
meta: {
title: '商品详情',
},
},
2
3
4
5
6
7
8
浏览器测试之,如下:
点击商品,代码如下:
# 2,api接口配置
封装根据id获取商品详情的接口,如下:
参考代码如下:
import request from '@/utils/request';
/**
* @description 获取商品详情
* @param {number} id 商品id
*/
export const getGoodsDetail = id =>
request({
method: 'POST',
url: '/frontend/goods/detail',
params: {
id,
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3,绘制页面
# 1,标题实现
# 2,获取数据
根据ID,调用接口,获取商品详情的数据,如下:
参考代码如下:
<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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3,渲染页面
数据如下:
绘制页面,显示数据,如下:
参考代码如下:
<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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
效果如下:
书写对应的样式,如下:
浏览器效果如下:
参考代码如下:
<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>
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,动作栏实现
书写代码如下:
参考代码如下:
<!-- 动作栏 -->
<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>
2
3
4
5
6
7
8
9
10
11
效果如下
# 3,购物车逻辑实现
# 1,购物车api配置
封装添加商品到购物车的api,如下:
参考代码如下:
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
});
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中,如下:
参考代码如下:
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
};
});
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接口,获取购物车数据,如下:
当点击加入购物车时,就需要调用方法,给加入购物车绑定点击事件,如下:
实现此方法,如下:
浏览器测试之,如下:
到此,pinia中就有数据了,如下:
前面我们点击了加入购物车,我们调用了仓库中的changeCart方法,这个方法中,调用了api接口,获取购物车数据了。但是并没有实现添加购物车的功用,也就是说当我们点击了加入购物车,分两种情况下:1)这个商品压根就没有在购物车中,需要调用addCart() 2)购物车中有此商品,就仅仅是让当前商品的数量+1,调用editCart。所以我们再封装一个编辑购物车,如下:
参考代码如下:
/**
* @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;
})(),
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如何知道,商品是否在购物车呢?我们需要获取购物车中的所有数据,前面我们已经把购物车中的数据获取了放到了pinia中了,现在只需要从pinia中获取数据遍历对比,就可以知道购物车有没有此商品了,如下:
浏览器测试之,如下:
到此,加入购物车的逻辑就实现了。
# 3 ,加入购物车逻辑完善(难)
需求:
直接在pinia中取数量就OK了,如下:
浏览器测试之,如下:
pinia中的数据一刷新没有了,因为pinia中的数据是存储在内存中的。测试如下:
现在的问题是一刷新pinia中的数据就没有了,除了这个问题,还有一个问题,如果换一个用户重新登录,另一个用户和当前用户的购物车数据是不一样的,也就是说换用户了,需要重新获取购物车数据,如果换用户了,会走setInfo,如下:
浏览器测试之,如下:
进入到商品详情,解决一下之前的一个不bug,如下:
一进入详情,就需要调用,只有一进来就调用,仓库中才有数据,如下:
测试如下:
然后,我们换一个用户登录,换一个用户登录,就会走setInfo,测试如下:
重新使用malu001用户登录之,如下:
进入详情,如下:
再使用demo01测试之,再进入到详情,如下:
当我们把浏览器关了,再打开这个项目,肯定需要找到最后登录的用户,之前封装过一个方法,可以找到最后登录的用户,需要调用setInfo,在setInfo中调用了获取购物车数据,先去封装一个api接口,如下:
关闭项目,如下:
重新打开项目,会走App中的onMounted,在onMounted中判断是否登录(根据是否有token),如果
登录了,到底是哪个用户登录了,调用lastloginUser,得到最后登录者的用户,再调用setInfo,在setInfo中重新获取购物车数据,如下:
# 4,tabbar购物车图标数量显示
需求:
实现父传子,如下:
子接收之,如下:
到此,详情页面中,添加购物车的逻辑就写完了。
# 4,收藏实现
# 1,分析
本项目中,可以商品,也可以收藏文章,点击了收藏,如下 :
在我的模块中,就可以查看收藏的商品了,如下:
点击我的收藏,如下:
再点击收藏按钮,表示取消收藏,如下:
打开接口文档,如下:
# 1,api接口配置
上面分析了api接口文档,封装四个api接口,如下:
参考代码:
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,
});
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,实现添加收藏的逻辑
找到添加收藏的按钮,给添加收藏按钮上绑定点击事件,如下:
然后,实现toggleStar方法,如下:
引用对应的接口,如下:
调用接口,如下:
在浏览器中测试之,如下:
解决bug,如下:
浏览器测试之,如下:
# 5,购买实现
分析:
找到立即购买,绑定点击事件,如下:
点击立即购买,去地址选择页面,导入router,如下:
实现对应的方法,如下:
浏览器测试之,如下:
# 九,地址管理页
# 1,配置路由
创建地址列表组件,如下:
配置路由,如下:
参考代码如下:
{
path: '/address-list',
name: 'addressList',
component: () => import('@/views/Address/index.vue'),
meta: {
login: true,
title: '收货地址',
},
},
2
3
4
5
6
7
8
9
# 2,api接口配置
查看api接口文档,如下:
封装地址管理的api接口,如下:
参考代码:
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;
})(),
});
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,如下:
浏览器中测试如下:
然后就需要绘制地址列表,地址列表在vant中有现成的组件,如下:
分析:
看一下地址列表的数据格式如下:
去给demo01这个用户添加一些测试地址数据,如下:
然后,我们就需要调用接口,获取地址列表相关的数据,如果数据非常多,要实现上拉加载下一页,下拉刷新(重新调接口,获取最新的数据)。此时就需要使用到下面的组件,如下:
分析list组件,如下:
书写对应的代码,如下:
参考代码如下:
<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>
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
效果如下:
开始绘制地址组件,定义相关的状态,如下 :
调接口,获取数据,如下:
浏览器中测试如下:
使用数据之前,需要进行数据格式转化,如下:
开始转化,使用之,如下:
浏览器中测试之,如下:
还有一个数据,处理之,如下:
浏览器测试之,如下:
到此,地址组件相关的两个状态就处理好了,使用组件,如下:
浏览器测试之,如下:
处理之如下:
浏览器测试之,如下:
到此,显示地址列表就OK了。
# 十,地址修改页
# 1,创建组件,配置路由
创建一个Edit组件,Edit组件即表示新增,也表示编辑,如下:
配置路由,如下:
参考代码如下:
{
path: '/edit-address',
name: 'edit',
component: () => import('@/views/Address/Edit.vue'),
meta: {
login: true,
title: '编辑',
},
},
2
3
4
5
6
7
8
9
现在我们登录了,浏览器访问之,如下:
# 2,页面绘制
开始绘制编辑 或 新增的页面,这个页面中,有一个选项地区的popup,如下:
安装vant地区包,如下:
开始绘制页面,如下:
参考代码如下:
<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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
浏览器效果如下
# 3,实现新增逻辑
绑定save事件,如下:
实现对应的onSave方法,打印收集的表单数据,如下:
浏览器测试如下:
还需要分析,调用接口,给接口传递的数据如下:
直接可以发请求了,如下 :
浏览器测试之,如下:
我们去访问地址列表页面,如下:
到此,添加地址就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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
还需要在地址列表页面中,给新增地址绑定点击事件,如下:
# 4,地址编辑功能
然后,实现编辑地址的功能,与地址新增功能可以复用同一个组件,在地址列表页面中,点击编辑时,触发的事件如下:
实现onEdit方法,如下:
浏览器测试之,如下:
在编辑组件中,就可以获取地址信息了,测试之,如下:
浏览器中测试之,是否可以获取用户信息,如下:
然后,实现数据的回显,先定义状态,给状态赋值,如下:
浏览器测试是否实现了数据回显,如下:
然后,我可以修改表单中的数据,修改完后,再点击保存,作用就是实现编辑,如下:
直接上参考代码:
<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>
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
浏览器测试之,如下:
修改一下van-nav-bar的title,如下:
浏览器测试之,如下:
# 5,地址的删除功能
要显示删除按钮,如下:
浏览器效果如下:
实现删除的逻辑,如下:
浏览器测试之,如下:
到此,删除地址的逻辑就实现了。
# 十一,订单数据的传递
# 1,分析流程
# 1,订单数据传递
点击购买按钮时把商品信息通过路由传参到了地址管理页,在地址列表页面,接收数据,如下:
点击新增或编辑,传递数据,如下:
在新增或编辑组件中,编辑或新增完成时,回到地址列表时,还需要把数据传递回去,如下:
点击删除时,也需要传递回去,如下:
点击返回箭头时,也需要传递回去,如下:
浏览器中测试如下:
# 2,地址选中
现在我们有很多地址,选中某个地址,要去生成订单页面,首先需要打印出我们选中哪个地址,如下:
实现代码如下:
实现对应的方法,如下:
浏览器测试之,如下:
参考代码如下:
// 地址选中事件
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,
})
)
),
},
});
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 十二,订单生成页
# 1,创建组件并配置路由
创建生成订单组件,如下:
配置路由,如下:
浏览器访问之,如下:
点击地址,测试是否可以到达生成订单页面,如下:
参考代码:
{
path: '/create-order',
name: 'createOrder',
component: () => import('@/views/Order/Create.vue'),
meta: {
title: '生成订单',
login: true,
},
},
2
3
4
5
6
7
8
9
# 2,配置api
查看接口文章,看一下关于订单的相关的接口,如下:
然后,封装api,如下:
参考代码如下:
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,
});
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,标题绘制
先把标题绘制出来,如下:
浏览器测试之,如下:
点击确认,我们是没有/order这个路由,按我们之前讲的,会报错,报错的原因是通过name来跳转的。但是现在我们是通过/order这个路径来跳转的,没有报错,原因是我们在router.j,配置了如下代码:
所以说,此时我们点击确认,就去Home页面了。
# 4,接收路由参数
到达生成订单页面,人家是传递参数过来的,参数中包含商品信息和收货人信息,如下:
此时,我们就需要接收参数,解析参数,如下:
浏览器测试如下:
把上面的数据定义成响应式,如下:
参考代码如下:
< 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 >
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,地址展示和商品信息展示
开始绘制页面,显示数据,如下:
效果如下:
书写一些样式,如下:
参考代码:
<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>
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
效果如下:
除了显示收货人信息,还需要显示商品信息,如下:
效果如下:
到此,生成订单页面中的收货人信息和商品信息展示就绘制OK了。
# 6,绘制 提交订单 和 备注
开始绘制,如下:
准备onSubmit方法和remark备注,如下:
在浏览器中看一下效果如下:
# 7,绘制支付弹出框
定义一个状态,控制是否显示弹窗,当我们点击生成订单时,就需要把弹窗显示出来,如下:
绘制弹窗,如下:
实现上面的两个方法,如下:
效果如下:
# 8,支付逻辑实现
点击了某一种支付方式,要实现支付,分析接口,如下:
商品相关的信息在哪里?如下:
准备数据,发送ajax请求,如下:
浏览器测试之,如下:
参考代码如下:
// 选择的支付方式
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");
};
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,支付框关闭事件
点击取消,表示也要生成订单,只不过没有支付,逻辑如下:
参考代码如下:
// 关闭事件
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');
};
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,创建组件并配置路由
创建组件如下:
配置路由,如下:
参考代码:
{
path: '/order',
name: 'order',
component: () => import('@/views/Order/index.vue'),
meta: {
title: '我的订单',
login: true,
},
},
2
3
4
5
6
7
8
9
浏览器访问之,如下:
# 2,标题绘制
代码如下:
浏览器效果如下 :
# 3,tab导航
找到vant中的tab导航,如下 :
定义状态,使用tab组件,如下:
浏览器效果如下:
# 4,封装OrderList组件
在components下面封装一个组件,叫OrderList,如下 :
在订单查看页面中使用之,如下:
参考代码如下:
<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>
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
在浏览器中测试之,如下:
接收到数据,只需要写html+css去渲染数据,如下 :
浏览器效果如下:
参考代码如下:
<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>
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时,会触发一个事件,如下:
实现changeTab方法,如下:
浏览器中测试之,如下:
完成逻辑,如下:
浏览器测试之,如下:
# 十四,购物车页面
# 1,标题显示
购物车页面的路由,之前已经配置好了,对应的组件也创建好了,现在绘制一下头部,如下:
浏览器效果如下:
# 2,未登陆时显示
如果没有登录,访问购物车,是不能访问的,处理之,如下:
参考代码如下:
< 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>
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,效果如下:
# 3,商品列表
如果已经登录了,需要获取商品的列表,我们要知道,购物车的数据存储在pinia中,之前已经调用过获取购物车数据的接口了,得到数据后,把数据存储到了pinia中了,如下:
现在切换到购物车商品,重新调用接口,获取数据,如下:
浏览器中测试之,如下:
现在购物车中是有数据的,数据在cartStore中,但是有可能购物车中没有数据,没有数据要显示购物车空空如也。如果有数据,需要把数据渲染出来,如下:
此时此时是有数据的,渲染数据如下,渲染数据,用到了如下的组件:
使用之,如下:
参考代码如下:
<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>
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
效果如下:
# 4,动作栏
绘制动作栏,如下:
参考代码如下:
<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" />
2
3
4
5
6
7
8
9
效果如下:
# 5,全选状态
上面已经把页面绘制差不多了,开始实现业务功能,先实现全选,给全选按钮绑定v-model,如下:
实现对应的计算属性,如下:
参考代码:
// 全选状态
const selectedAll = computed({
get: () => cartStore.data?.every?.(it => it.checked),
set: val => {
cartStore.data.forEach?.(it => {
it.checked = val;
});
},
});
2
3
4
5
6
7
8
9
浏览器测试之,如下:
# 6,总价
开始计算总价,绑定计算属性,如下:
实现totalPrice,如下:
参考代码如下:
// 总价
const totalPrice = computed(() =>
cartStore.data
?.filter(it => it.checked)
.reduce((pre, cur) => pre + cur.goods_info.price * cur.count, 0)
);
2
3
4
5
6
浏览器效果如下:
# 7,更改数量
实现商品的累加,给van-stepper绑定change事件,如下:
实现对应的方法,如下:
浏览器打印测试之,如下:
引入接口,调用接口,如下:
浏览器测试之,如下:
# 8,删除商品
实现删除商品,先封装api接口,如下:
在组件中,导入之,如下:
给删除按钮绑定点击事件,如下:
实现删除逻辑,如下:
参考代码如下:
// 删除商品时事件
const deleteGoods = async id => {
const res = await deleteCart([id]);
if (res.code == 1) {
cartStore.changeCart();
}
};
2
3
4
5
6
7
效果如下:
# 9,提交订单事件
分析如下:
绑定提交事件,如下:
实现对应的方法,如下:
参考代码如下:
// 提交事件
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
}))
),
},
});
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
浏览器测试之,如下:
# 十五,种草页面
src\views\Recommend\index.vue
# 1,标题
效果
# 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,
},
});
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
};
};
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>
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>
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: '文章详情',
},
},
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;
})(),
});
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;
})(),
});
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,标题
# 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;
});
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>
2
3
4
5
6
7
8
9
10
11
12
13
效果如下
# 5,评论绘制
效果如下
# 6,动作栏
<div class="action_box">
<div class="add_comment" @click="show = true">
<van-icon name="edit" size="20px" /> 评论
</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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
效果如下
# 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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
效果如下
# 8,点赞逻辑
// 点赞逻辑
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;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
效果如下
# 9,收藏逻辑
// 收藏
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;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
效果如下
# 10,新增评论
// 评论
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();
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
效果如下
# 参考代码
<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 }} <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" /> 评论
</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>
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: '新增文章',
},
},
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>
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
效果如下
# 十八,我的页面
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>
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
效果如下
# 十九,个人信息页面
# 1,配置路由
{
path: '/user-info',
name: 'userInfo',
component: () => import('@/views/User/Info.vue'),
meta: {
login: true,
title: '个人信息',
},
},
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>
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: '修改密码',
},
},
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>
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
效果如下
# 二十一,我的收藏
# 1,配置路由
{
path: '/my-collection',
name: 'myCollection',
component: () => import('@/views/User/Collection.vue'),
meta: {
login: true,
title: '我的收藏',
},
},
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
};
});
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>
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
效果如下
只要不刷新页面,每次进入当前页面就会记忆退出时的页面
# 二十二,我的点赞页面
# 1,配置路由
{
path: '/thumbs-up',
name: 'ThumbsUp',
component: () => import('@/views/User/ThumbsUp.vue'),
meta: {
title: '我的点赞',
login: true,
},
},
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>
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