客户只有登录了才能继续购物操作,才能进行某些业务。在view/Login文件夹下新建index.vue
文件,代码如下

用到的接口
在apis文件夹下新建文件user.js
,需要用到的接口如下
1 2 3 4 5 6 7 8 9 10 11 12
| import request from "@/utils/http"
export const loginAPI = ({ account, password }) => { return request({ url: '/login', method: 'POST', data: { account, password } }) }
|
表单校验实现
自定义校验
校验的核心部件为
1 2 3 4 5 6 7 8 9 10 11 12
| <el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="60px" status-icon> <el-form-item prop="account" label="账户"> <el-input v-model="form.account" /> </el-form-item> <el-form-item prop="password" label="密码"> <el-input v-model="form.password" /> </el-form-item> <el-form-item prop="agree" label-width="22px"> <el-checkbox size="large" v-model="form.agree">我已同意隐私条款和服务条款</el-checkbox> </el-form-item> <el-button size="large" class="subBtn" @click="doLogin">点击登录</el-button> </el-form>
|
其中,<el-form />
用于绑定表单对象和规则对象,<el-form-item />
用于绑定使用的规则字段,<el-input />
用于绑定表单数据。
<el-form />
绑定了表单”formRef”,其在js上定义的表现形式为
1 2 3 4 5
| const form = ref({ account: '', password: '', agree: true })
|
定义的规则对象为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const rules = { account: [ { required: true, message: '账户名不能为空', trigger: 'blur' } ], password: [ { required: true, message: '密码不能为空', trigger: 'blur' }, { min: 6, max: 14, message: '密码在6至14字符之间', trigger: 'blur' } ], agree: [ { validator: (rule, value, callback) => { if (value) { callback(); } else { callback(new Error('请勾选协议')) } } } ] }
|
blur
表示在输入框失焦时触发。
统一校验
此时假如什么都不填,直接点击登录,还是能进去的。因此需要统一校验规则,在收到成功消息后才允许登录
1 2 3 4 5 6 7 8 9 10 11
| const doLogin = () => { const { account, password } = form.value formRef.value.validate(async (valid) => { if (valid) { userStore.getUserInfo({ account, password }) ElMessage({ type: 'success', message: '登录成功' }) router.replace({ path: '/' }) } }) }
|
userStore在下面的章节
Pinia管理用户数据
用户数据在后面都需要用到,因此一般是用Pinia将其存储起来。
在store文件夹下面新建文件user.js
,源码如下
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
| import { defineStore } from "pinia"; import { ref } from "vue"; import { loginAPI } from "../apis/user"; import { useCartStore } from "./cartStore"; import { mergeCartAPI } from "../apis/cart";
export const useUserStore = defineStore('user', () => { const userInfo = ref({}) const cartStore = useCartStore() const getUserInfo = async ({ account, password }) => { const res = await loginAPI({ account, password }) userInfo.value = res.result await mergeCartAPI(cartStore.cartList.map((item) => { return { skuId: item.skuId, selected: item.selected, count: item.count } })) cartStore.updateNewList() }
const clearUserInfo = () => { userInfo.value = {} cartStore.clearCart()
} return { userInfo, getUserInfo, clearUserInfo, } }, { persist: true, })
|
用户登录后同步本地的购物车数据到自己的账户中,然后通过调取loginAPI
获取用户信息,信息都存到userInfo
中。退出操作则让用户数据清空。
而后面的persist
属性需要配合pinia的持久化。用户数据中有一个关键的数据Token(标识用户是否已经登录),而Token持续一段时间才会过期。因此需要让保存在内存中的数据在LocalStorage里也存一份。该功能需要在main.js
中全局注册实现。
购物车和用户这两个store互有联系,需要相互看。
1 2
| const pinia = createPinia() pinia.use(piniaPluginPersistedstate)
|
源码
index.vue
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
| <script setup> import { ref } from 'vue'; import { ElMessage } from 'element-plus'; import 'element-plus/theme-chalk/el-message.css' import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const form = ref({ account: '', password: '', agree: true })
const rules = { account: [ { required: true, message: '账户名不能为空', trigger: 'blur' } ], password: [ { required: true, message: '密码不能为空', trigger: 'blur' }, { min: 6, max: 14, message: '密码在6至14字符之间', trigger: 'blur' } ], agree: [ { validator: (rule, value, callback) => { if (value) { callback(); } else { callback(new Error('请勾选协议')) } } } ] }
const formRef = ref(null) const router = useRouter() const doLogin = () => { const { account, password } = form.value formRef.value.validate(async (valid) => { if (valid) { userStore.getUserInfo({ account, password }) ElMessage({ type: 'success', message: '登录成功' }) router.replace({ path: '/' })
} }) } </script>
<template> <div> <header class="login-header"> <div class="container m-top-20"> <h1 class="logo"> <router-link to="/">小兔鲜</router-link> </h1> <router-link class="entry" to="/"> 进入网站首页 <i class="iconfont icon-angle-right"></i> <i class="iconfont icon-angle-right"></i> </router-link> </div> </header> <section class="login-section"> <div class="wrapper"> <nav><a href="javascript:;">用户登录</a></nav> <div class="account-box"> <div class="form"> <el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="60px" status-icon> <el-form-item prop="account" label="账户"> <el-input v-model="form.account" /> </el-form-item> <el-form-item prop="password" label="密码"> <el-input v-model="form.password" /> </el-form-item> <el-form-item prop="agree" label-width="22px"> <el-checkbox size="large" v-model="form.agree">我已同意隐私条款和服务条款</el-checkbox> </el-form-item> <el-button size="large" class="subBtn" @click="doLogin">点击登录</el-button> </el-form> </div> </div> </div> </section> <footer class="login-footer"> <div class="container"> <p> <a href="javascript:;">关于我们</a> <a href="javascript:;">帮助中心</a> <a href="javascript:;">售后服务</a> <a href="javascript:;">配送与验收</a> <a href="javascript:;">商务合作</a> <a href="javascript:;">搜索推荐</a> <a href="javascript:;">友情链接</a> </p> <p>CopyRight © 小兔鲜儿</p> </div> </footer> </div> </template>
<style scoped lang='scss'> .login-header { background: #fff; border-bottom: 1px solid #e4e4e4;
.container { display: flex; align-items: flex-end; justify-content: space-between; }
.logo { width: 200px;
a { display: block; height: 132px; width: 100%; text-indent: -9999px; background: url("@/assets/images/logo.png") no-repeat center 18px / contain; } }
.sub { flex: 1; font-size: 24px; font-weight: normal; margin-bottom: 38px; margin-left: 20px; color: #666; }
.entry { width: 120px; margin-bottom: 38px; font-size: 16px;
i { font-size: 14px; color: $xtxColor; letter-spacing: -5px; } } }
.login-section { background: url('@/assets/images/login-bg.png') no-repeat center / cover; height: 488px; position: relative;
.wrapper { width: 380px; background: #fff; position: absolute; left: 50%; top: 54px; transform: translate3d(100px, 0, 0); box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
nav { font-size: 14px; height: 55px; margin-bottom: 20px; border-bottom: 1px solid #f5f5f5; display: flex; padding: 0 40px; text-align: right; align-items: center;
a { flex: 1; line-height: 1; display: inline-block; font-size: 18px; position: relative; text-align: center; } } } }
.login-footer { padding: 30px 0 50px; background: #fff;
p { text-align: center; color: #999; padding-top: 20px;
a { line-height: 1; padding: 0 10px; color: #999; display: inline-block;
~a { border-left: 1px solid #ccc; } } } }
.account-box { .toggle { padding: 15px 40px; text-align: right;
a { color: $xtxColor;
i { font-size: 14px; } } }
.form { padding: 0 20px 20px 20px;
&-item { margin-bottom: 28px;
.input { position: relative; height: 36px;
>i { width: 34px; height: 34px; background: #cfcdcd; color: #fff; position: absolute; left: 1px; top: 1px; text-align: center; line-height: 34px; font-size: 18px; }
input { padding-left: 44px; border: 1px solid #cfcdcd; height: 36px; line-height: 36px; width: 100%;
&.error { border-color: $priceColor; }
&.active, &:focus { border-color: $xtxColor; } }
.code { position: absolute; right: 1px; top: 1px; text-align: center; line-height: 34px; font-size: 14px; background: #f5f5f5; color: #666; width: 90px; height: 34px; cursor: pointer; } }
>.error { position: absolute; font-size: 12px; line-height: 28px; color: $priceColor;
i { font-size: 14px; margin-right: 2px; } } }
.agree { a { color: #069; } }
.btn { display: block; width: 100%; height: 40px; color: #fff; text-align: center; line-height: 40px; background: $xtxColor;
&.disabled { background: #cfcdcd; } } }
.action { padding: 20px 40px; display: flex; justify-content: space-between; align-items: center;
.url { a { color: #999; margin-left: 10px; } } } }
.subBtn { background: $xtxColor; width: 100%; color: #fff; } </style>
|