vue3小兔鲜商城项目学习笔记+资料分享05
vue3小兔鲜商城项目学习笔记+资料分享
建议大家先去看我第一篇小兔鲜的文章,强烈建议,非常建议,十分建议,从头开始看更完整。
最近正在学习vue3小兔鲜,
下面是学习笔记
登录模块
路由与组件
目标:登录组件在书写一级路由的时候已经准备,添加路由链接跳转到登录页即可。
- 添加跳转链接:
src/components/app-topnav.vue
<li><RouterLink to="/login">请先登录</RouterLink></li>
结构布局-CV
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ismlX4gn-1668072991723)(media/01-16459813154986.png)]
静态结构参考代码 - CV
<script setup lang="ts">
//
</script>
<template>
<div class="page-login">
<!-- 1. 头部 -->
<header class="login-header">
<div class="container">
<h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1>
<h3 class="sub"><slot>欢迎登录</slot></h3>
<RouterLink class="entry" to="/">
进入网站首页
<i class="iconfont icon-angle-right"></i>
<i class="iconfont icon-angle-right"></i>
</RouterLink>
</div>
</header>
<!-- 2. 主体登录区域 -->
<section class="login-section">
<div class="wrapper">
<nav>
<a href="javascript:;">账户登录</a>
</nav>
<LoginForm />
</div>
</section>
<!-- 3. 底部 -->
<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="less">
// 头部样式
.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;
}
}
}
}
</style>
表单布局-CV
目标: 实现登录页面的整体大结构布局
- 新建表单组件
src/views/Login/components/login-form.vue
<script setup lang="ts">
//
</script>
<template>
<div class="account-box">
<div class="form">
<div class="form-item">
<div class="input">
<i class="iconfont icon-user"></i>
<input type="text" placeholder="请输入用户名或手机号" />
</div>
<!-- 表单验证错误信息提示 -->
<!-- <div class="error"><i class="iconfont icon-warning" />请输入手机号</div> -->
</div>
<div class="form-item">
<div class="input">
<i class="iconfont icon-lock"></i>
<input type="password" placeholder="请输入密码" />
</div>
</div>
<div class="form-item">
<div class="agree">
<XtxCheckBox />
<span>我已同意</span>
<a href="javascript:;">《隐私条款》</a>
<span>和</span>
<a href="javascript:;">《服务条款》</a>
</div>
</div>
<a href="javascript:;" class="btn">登录</a>
</div>
<div class="action">
<img
src="https://qzonestyle.gtimg.cn/qzone/vas/opensns/res/img/Connect_logo_7.png"
alt=""
/>
<div class="url">
<a href="javascript:;">忘记密码</a>
<a href="javascript:;">免费注册</a>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
// 账号容器
.account-box {
.toggle {
padding: 15px 40px;
text-align: right;
a {
color: @xtxColor;
i {
font-size: 14px;
}
}
}
.form {
padding: 0 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;
}
}
}
}
</style>
登录模块入口组件 src/views/Login/index.vue
在主体登录区域引入并使用表单组件。
<script setup lang="ts">
import LoginForm from "./components/login-form.vue";
</script>
<template>
<div class="page-login">
<!-- 1. 头部 -->
<header class="login-header">
....
</header>
<!-- 2. 主体登录区域 -->
<section class="login-section">
<div class="wrapper">
<nav>
<a href="javascript:;">账户登录</a>
</nav>
<LoginForm />
</div>
</section>
<!-- 3. 底部 -->
<footer class="login-footer">
....
</footer>
</div>
</template>
消息提示组件 和 复选框组件
组件外观
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mqc5JFik-1668072991725)(media/05-1627943191361.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjHBxdKU-1668072991727)(assets/image-20210803065321123.png)]
XtxCheckBox 组件使用
<script setup lang="ts">
import { ref } from "vue";
const isAgree = ref(false);
</script>
<XtxCheckBox v-model="isAgree">我已同意</XtxCheckBox>
Message 组件使用
Message 组件支持 函数 和 组件 两种调用方式:
函数调用 - 推荐👍
- 调用函数时由函数内部动态创建组件,使用完成自动卸载
<script setup lang="ts">
import { message } from "@/components/XtxUI/Message/index";
message({ type: "success", text: "登录成功", time: 2000});
</script>
- 组件库代码升级:
src\components\XtxUI\index.ts
组件库做统一出口
// 做统一出口
export { message } from "./Message/index";
- 调用 message 函数变得更方便
<script setup lang="ts">
import { message } from "@/components/XtxUI";
message({ type: "success", text: "登录成功", time: 2000});
</script>
登录前表单校验
目标:校验之前我们已经实现了很多次,这里弱化验证写法,简单做一个非空检验即可。
参考代码
<script setup lang="ts">
import { reactive } from "vue";
import { message } from "@/components/XtxUI";
const form = reactive({
account: "",
password: "",
});
const loginBtn = () => {
if (!form.account) {
message({ type: "error", text: "用户名或手机号不能为空" });
return;
}
if (!form.password) {
message({ type: "error", text: "密码不能为空" });
return;
}
if (!isAgree.value) {
message({ type: "error", text: "请同意许可" });
return;
}
console.log("通过校验,可以发送请求");
};
</script>
// 📌 绑定 form 响应式数据到表单元素中
<input v-model.trim="form.account" type="text" placeholder="请输入用户名或手机号" />
<input v-model.trim="form.password" type="password" placeholder="请输入密码" />
<XtxCheckBox v-model="isAgree">我已同意</XtxCheckBox>
// 📌 登录按钮
<a href="javascript:;" class="btn" @click="loginBtn">登录</a>
会员 Pinia 状态管理
新建 Strore
新建文件: src\store\modules\member.ts
import { defineStore } from "pinia";
const useMemberStore = defineStore("member",{
// 状态
state: () => ({}),
// 计算
getters: {},
// 方法
actions: {},
});
export default useMemberStore;
合并 Store
修改文件:src\store\index.ts
import { defineStore } from 'pinia';
import useHomeStore from './modules/home';
+ import useMemberStore from './modules/member';
// 定义总Store,管理所有模块Store
const useStore = defineStore('main', {
// state 存放模块 Store
state: () => ({
home: useHomeStore(),
+ member: useMemberStore(),
}),
});
// 默认导出
export default useStore;
- 调试工具检查 Pinia。
账户登录实现
本节目标:
实现账户名密码登录功能
用户名和密码登录接口
基本信息
Path: /login
Method: POST
接口描述:
登录成功后,后台返回的 token,请在本地保存,并在每次请求接口时在 Header 中携带上。
请求参数
Body
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
account | string | 必须 | 用户名或手机号 | undefined: ceshi | |
password | string | 必须 | 密码 | undefined: 123456 |
发送登录请求
修改文件:src/store/modules/member.ts
import { http } from "@/utils/request";
import { defineStore } from "pinia";
const useMemberStore = defineStore("member",{
// 状态
state: () => ({
profile: {},
}),
// 计算
getters: {},
// 方法
actions: {
// 用户名和密码登录
async login(data: { account: string; password: string }) {
const res = await http("POST", "/login", data);
console.log("/login", res.data.result);
},
},
});
export default useMemberStore;
3)登录表单调用
login-form.vue
<script setup lang="ts">
import { reactive, ref } from "vue";
import useStore from "@/store";
const isAgree = ref(false);
const form = reactive({
account: "sujiehao",
password: "123456",
});
const { member } = useStore();
const login = async () => {
if (!form.account) {
return message({ type: "error", text: "用户名或手机号不能为空" });
}
if (!form.password) {
return message({ type: "error", text: "密码不能为空" });
}
if (!isAgree.value) {
return message({ type: "error", text: "请同意许可" });
}
// 发送登录请求
await member.login(form);
};
</script>
定义TS类型
新增类型文件:src\types\api\member.d.ts
export interface Profile {
id: string;
account: string;
mobile: string;
token: string;
avatar: string;
nickname: string;
gender: string;
birthday: string;
cityCode: string;
provinceCode: string;
profession: string;
}
应用类型:
import type { Profile } from "@/types";
import { http } from "@/utils/request";
import { defineStore } from "pinia";
import { message } from "@/components/XtxUI";
// 🎯非 vue 组件,导入路由实例
import router from "@/router";
const useMemberStore = defineStore("member", {
// 状态
state: () => ({
// 用户信息
profile: {} as Profile,
}),
// 计算
getters: {
// 是否登录
isLogin(): boolean {
return Boolean(this.profile.token);
},
},
// 方法
actions: {
// 用户名和密码登录
async login(data: { account: string; password: string }) {
const res = await http<Profile>("POST", "/login", data);
// 1. 保存用户信息到 state 中
this.profile = res.data.result;
// 2. 请求成功给用户提示
message({ type: "success", text: "登录成功" });
// 3. 跳转页面
router.push('/')
},
},
});
export default useMemberStore;
持久化存储
登录成功后,把用户信息保存到本地。(本地和pinia都保存)
用户信息渲染
用户信息渲染: Layout/components/app-topnav.vue
<script setup lang="ts">
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { member } = useStore();
// storeToRefs 解构出来的数据还能保持响应式
const { profile } = storeToRefs(member);
</script>
<template>
<nav class="app-topnav">
<div class="container">
<ul>
<template v-if="member.isLogin">
<li>
<a href="javascript:;">
<i class="iconfont icon-user"></i>
{{ profile.nickname || profile.account || profile.mobile }}
</a>
</li>
<li><a href="javascript:;">退出登录</a></li>
</template>
<template v-else>
<li><RouterLink to="/login">请先登录</RouterLink></li>
<li><a href="javascript:;">免费注册</a></li>
</template>
...
</ul>
</div>
</nav>
</template>
退出登录实现
本节目标:
实现账户名密码登录功能
1 提供 actions, 清空用户数据 (本地和pinia都清空)
actions: {
// 退出登录
async logout() {
// 1. 清理 Pinia 用户信息
this.profile = {} as Profile;
// 2. 提示用户
message({ type: 'success', text: '退出成功' });
// 3. 跳转页面
router.push("/login");
},
},
- 页面中调用
<template v-if="profile.isLogin">
<li>
<a href="javascript:;"><i class="iconfont icon-user"></i>
{{ profile.nickname || profile.account || profile.mobile }}
</a>
</li>
<li><a @click="member.logout()" href="javascript:;">退出登录</a></li>
</template>
优化:登录成功页面回跳
src\views\Layout\components\app-topnav.vue
<template>
<RouterLink :to="`/login?target=${$route.fullPath}`">
请先登录
</RouterLink>
</template>
登录成功后跳转处理
const useMemberStore = defineStore("member",{
...
// 方法
actions: {
// 用户名密码登录
async login(data: { account: string; password: string }) {
...
// 🐛 在非 .vue 组件中 useRoute() 返回 undefined,没法获取当前路由信息
// 📌 解决方案,通过 router 路由实例 currentRoute 获取
const { target = "/" } = router.currentRoute.value.query;
// 跳转到指定地址
router.push(target as string);
},
},
});
请求拦截器和响应拦截器
请求拦截器 - 请求成功携带 token
完善请求拦截器 utils/request.ts
// 官方说明:https://pinia.vuejs.org/core-concepts/outside-component-usage.html
// ❌ 非组件中,Pinia 常见错误写法
// const { member } = useStore();
// 添加请求拦截器
instance.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
// ✅ 在组件外,哪里使用,写哪里(消费前获取)
const { member } = useStore();
// 1. 获取token
const { token } = member.profile;
// 2. token 和 headers 的非空判断
if (token && config.headers) {
// 3. 请求头中携带 token 信息
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
响应拦截器 - 请求失败提示用户
响应拦截器,添加错误提示
import { message } from '@/components/Message'
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 如果请求成功成功 2xx 就直接返回 data 中的数据
return response
},
function (error) {
// 对响应错误做点什么
if (error.code === 'ERR_NETWORK') {
// 无网络,错误提示
message({ type: 'error', text: '亲,换个网络试试~' });
} else {
// 有网络,但后端认为有错误,提示后端响应的错误
message({ type: 'error', text: error.response.data.message });
}
// 控制台显示错误
return Promise.reject(error);
})
QQ三方登录 - 前置环境和交互
登录简要流程梳理
本节目标:
掌握第三方登录的实现流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NxRV3K20-1668072991731)(media/06-1628025535476.png)]
- 在登录页面,QQ登录按钮处,添加个超链接,赋予其打开QQ登录页面功能
- 回跳的页面得到QQ给的唯一标识 openId,根据openId去后台查询是否已经绑定过账户
- 如果绑定过,完成登录
- 没有绑定过
- 有账号的,绑定手机号,即为登录
- 没账号的,完善账户信息,注册后登录
- 登录成功后,跳转首页或者来源页面
申请流程
1)参考文档
2)大概步骤
- 准备一个已经备案的网站需要有
QQ
登录的逻辑(登录页面,回跳页面) - 然后在
QQ
互联上进行身份认证
,并且审核通过
- 在
QQ
互联上创建应用,应用需要域名,备案号,回调地址等 - 等待人工审核,审核通过会得到
应用ID
回调地址
,应用key
后端使用。 - 帮大家申请的结果如下:
# 测试用 appid
# 100556005
# 测试用 redirect_uri
# http://www.corho.com:8080/#/login/callback
常见疑问❓
- 这个申请工作一般由谁去做?
- 公司的运维 (负责管理公司账号的人)
- 申请下来的 id,应用 key,回调地址 uri 能改吗?
- 都不能修改,否则无效。
- 🐛 回调地址 uri 的包含四部分: 1. 域名,2. 端口号 3. 哈希路由模式 4. 路由地址 都必须完全一致,否则不能展示。
- 🚨 访问 http://www.corho.com:8080/ 看不到内容?
- 修改脚手架
vite.config.ts
配置。 - 修改电脑的
host
文件,访问本地服务器。
- 修改脚手架
QQ三方登录-电脑环境设置🚨
目标:浏览器访问 http://www.corho.com:8080/#/login/callback 地址,能打开正在开发的本地
Vue
项目。
核心步骤
- 修改脚手架
vite.config.ts
配置。 - 修改电脑的
host
文件。
第一步:修改 vite
配置
修改 vite.config.ts
文件:
export default defineConfig({
// 配置开发服务器
server: {
// QQ三方登录的回调uri为:http://www.corho.com:8080/#/login/callback
// vite 中配置: www.corho.com:8080
host: "www.corho.com",
port: 8080,
// 其他有价值的配置项
open: true, // 帮我们打开浏览器
cors: true, // 允许开发时 ajax 跨域
},
...
});
第二步:修改 host
文件
windows 系统
🔔提醒:修改电脑配置,需要先退出 360 或 各种管家 各种 杀毒软件
🔔提醒:如果修改 hosts 文件有弹窗警告,点击信任(因为这是我们自己进行的安全操作)
1. 找到 C:\Windows\System32\drivers\etc 下 hosts 文件
2. 在文件中加入 127.0.0.1 www.corho.com
3. 保存即可
# 如果提示没有权限
1. 将hosts文件移到桌面,然后进行修改,确认保存。
2. 将桌面hosts文件替换c盘文件
mac OS 系统
1. 打开命令行窗口
2. 输入:sudo vim /etc/hosts
3. 按下:i 键
4. 输入:127.0.0.1 www.corho.com
5. 按下:esc
6. 按下:shift + :
7. 输入:wq 回车即可
关键步骤验证
📌步骤验证:浏览器访问 http://www.corho.com:8080/#/ 能看到自己开发的 Vue3 项目表示成功。
DNS 解析概念 - 了解
由于本地我们的网站是访问
http://localhost:3000
而回调地址的域名是
http://www.corho.com:8080
,俩个地址不一致是无法进行跳转的,需要我们修改本地的 hosts 文件,让域名访问时解析到我们本地的ip上
DNS 解析:(网络中,服务器不认域名的,认的是 ip) www.jd.com
- 作用是将域名地址解析成ip地址
- 优先级 先以本地的 hosts 文件为主 然后才走线上的dns服务器
DSN解析说明
DNS解析: 将域名解析成ip地址的过程。
想看一个网站 www.jd.com => 电脑不知道什么是 www.jd.com,需要询问的
-
先问本地 hosts 文件(一般不改) 如果本地配置了 域名 和 地址的映射关系,优先使用 hosts 中的映射
127.0.0.1 www.jd.com
-
如果本地hosts文件里面没配(默认一般都没配)比如:找www.baidu.com
会找线上的 dns 服务器, dns 服务器就像一个字典, 字典中记录大量的 网站域名 和 网站ip 的对应关系 dns 服务器 112.80.248.75 www.baidu.com xxx.xx.xxx.xx www.xxx.com
回跳页面路由准备
前面的配置完成了:http://www.corho.com:8080/#/
接下来在完善:http://www.corho.com:8080/#/login/callback
配置路由和组件
新建组件:views/Login/callback.vue
<script setup lang="ts">
//
</script>
<template>
<h1>三方登录的回跳页面</h1>
</template>
配置路由:
2)绑定路由 (一级路由)
{
path: '/login/callback',
component: () => import('@/views/Login/callback.vue')
},
📌步骤验证:http://www.corho.com:8080/#/login/callback 看到回调页面组件。
🚨常见错误:没有使用 hash 哈希路由模式,无法识别 # 哈希部分路径,请检查并修正。
QQ授权登录实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RcTw1uat-1668072991732)(media/image-20220222213438246.png)]
按钮跳转实现
1)在index.html
开发需要的添加 sdk.js
文件导入。
<script src="http://connect.qq.com/qc_jssdk.js" data-appid="100556005" data-redirecturi="http://www.corho.com:8080/#/login/callback"></script>
2)在 src/views/login/components/login-form.vue
给图片套上跳转链接。
<a
:href="`https://graph.qq.com/oauth2.0/authorize?response_type=token&scope=all&client_id=${'100556005'}&redirect_uri=http%3A%2F%2Fwww.corho.com%3A8080%2F%23%2Flogin%2Fcallback`"
>
<img
src="https://qzonestyle.gtimg.cn/qzone/vas/opensns/res/img/Connect_logo_7.png"
alt=""
/>
</a>
3)点击QQ登录按钮,点击后新窗口打开登录页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3WlMOMEq-1668072991734)(media/08-1628025535476.png)]
小知识补充:
// 这是什么东西,怎么看起来像乱码?
http%3A%2F%2Fwww.corho.com%3A8080%2F%23%2Flogin%2Fcallback
// 解码
decodeURIComponent('http%3A%2F%2Fwww.corho.com%3A8080%2F%23%2Flogin%2Fcallback')
// 解码结果:'http://www.corho.com:8080/#/login/callback'
// 转码
encodeURIComponent('http://www.corho.com:8080/#/login/callback')
// 转码结果:'http%3A%2F%2Fwww.corho.com%3A8080%2F%23%2Flogin%2Fcallback'
QQ互联核心 API
项目中需要用到的 3 个 API
- QC.Login.check
- 检查用户是否登录
- QC.api(“get_user_info”).success
- 获取信息
- QC.Login.getMe
- 获取 QQ 用户唯一标识 openId
ESlint 添全局变量
- 注意:由于项目开启了
eslint
检查,需要在.eslintrc.cjs
添加QC
全局变量。
// eslintrc.cjs
module.exports = {
...
// 全局变量
globals: {
QC: true,
},
}
TS 类型声明文件
- 注意:由于是 TS 开发,需要在
env.d.ts
添加QC
类型声明。
// env.d.ts
// QC 类型声明 - QQ 登录模块
declare namespace QC {
const Login: {
// QC.Login.check()
check: () => boolean;
// QC.Login.getMe((openId) => {
// console.log("获取QQ用户openId", openId);
// });
getMe: (callback: (openId: string) => void) => void;
};
// QC.api("get_user_info").success((res: unknown) => {
// console.log("获取QQ用户资料", res);
// });
function api(s: string): {
success: (res: unknown) => void;
};
}
测试
<script setup lang="ts">
// 1. 检查用户是否已登录
if (QC.Login.check()) {
// 2. 获取 QQ 用户唯一标识 openId
QC.Login.getMe((openId) => {
console.log("🗝️openId", openId);
});
// 3. 获取用户资料
QC.api("get_user_info").success((res: unknown) => {
console.log("😀获取用户资料", res);
});
}
</script>
<template>
<h1>callback-QQ登录回跳页面测试</h1>
</template>
回跳组件静态结构
目标:准备静态结果,渲染切换效果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q7OKGrln-1668072991736)(media/image-20210804062529636.png)]
静态结构
- 复制组件静态结构:
views/Login/callback.vue
- 温馨提示:头部和底部和登录页相同,自己完成
LoginHeader
头部组件和LoginFooter
底部组件的抽离。
...
<template>
<LoginHeader>联合登录</LoginHeader>
<section class="container">
<nav class="tab">
<a
href="javascript:;"
class="active"
>
<i class="iconfont icon-bind" />
<span>已有小兔鲜账号,请绑定手机</span>
</a>
<a
href="javascript:;"
>
<i class="iconfont icon-edit" />
<span>没有小兔鲜账号,请完善资料</span>
</a>
</nav>
<div class="tab-content">
<!-- 内容 -->
</div>
</section>
<LoginFooter />
</template>
<style scoped lang='less'>
.container {
padding: 25px 0;
}
.tab {
background: #fff;
height: 80px;
padding-top: 40px;
font-size: 18px;
text-align: center;
a {
color: #666;
display: inline-block;
width: 350px;
line-height: 40px;
border-bottom: 2px solid #e4e4e4;
i {
font-size: 22px;
vertical-align: middle;
}
span {
vertical-align: middle;
margin-left: 4px;
}
&.active {
color: @xtxColor;
border-color: @xtxColor;
}
}
}
.tab-content {
min-height: 600px;
background: #fff;
}
</style>
3)准备绑定手机组件 (有老账号) 和 完善信息组件(新账号)
src/views/Login/components/callback-bind.vue
绑定手机
<script setup lang="ts">
//
</script>
<template>
<div class="xtx-form">
<div class="user-info">
<img
src="http://qzapp.qlogo.cn/qzapp/100556005/86EB7ABDD27C651376A94AC9DF95B7BD/100"
alt=""
/>
<p>Hi,Vue3 欢迎来小兔鲜,完成绑定后可以QQ账号一键登录哦~</p>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-phone"></i>
<input class="input" type="text" placeholder="绑定的手机号" />
</div>
<div class="error"></div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-code"></i>
<input class="input" type="text" placeholder="短信验证码" />
<span class="code">发送验证码</span>
</div>
<div class="error"></div>
</div>
<a href="javascript:;" class="submit">立即绑定</a>
</div>
</template>
<style scoped lang="less">
.user-info {
width: 320px;
height: 70px;
margin: 0 auto;
display: flex;
background: #f2f2f2;
align-items: center;
padding: 0 10px;
margin-bottom: 25px;
img {
background: #f2f2f2;
width: 50px;
height: 50px;
}
p {
padding-left: 10px;
}
}
.code {
position: absolute;
right: 0;
top: 0;
line-height: 50px;
width: 80px;
color: #999;
&:hover {
cursor: pointer;
}
}
</style>
src/views/Login/components/callback-register.vue
注册信息
<script setup lang="ts">
//
</script>
<template>
<div class="xtx-form">
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-user"></i>
<input class="input" type="text" placeholder="请输入用户名" />
</div>
<div class="error"></div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-phone"></i>
<input class="input" type="text" placeholder="请输入手机号" />
</div>
<div class="error"></div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-code"></i>
<input class="input" type="text" placeholder="请输入验证码" />
<span class="code">发送验证码</span>
</div>
<div class="error"></div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-lock"></i>
<input class="input" type="password" placeholder="请输入密码" />
</div>
<div class="error"></div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-lock"></i>
<input class="input" type="password" placeholder="请确认密码" />
</div>
<div class="error"></div>
</div>
<a href="javascript:;" class="submit">立即提交</a>
</div>
</template>
<style scoped lang="less">
.code {
position: absolute;
right: 0;
top: 0;
line-height: 50px;
width: 80px;
color: #999;
&:hover {
cursor: pointer;
}
}
</style>
4)使用组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IG1Z2AkN-1668072991737)(media/image-20210818144919351.png)]
完成切换交互
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v0vb0R0C-1668072991738)(media/image-20210804062606238.png)]
实现思路 : 典型的tab类效果实现,升级成
<KeepAlive>
动态组件实现。
代码落地
<script setup lang="ts">
...
const isBind = ref(true);
</script>
<template>
<!-- 1. 头部 -->
<LoginHeader>联合登录</LoginHeader>
<!-- 2. 主体 -->
<section class="container">
<!-- 2.1 tab 头部 -->
<nav class="tab">
<a
href="javascript:;"
@click="isBind = true"
:class="{ active: isBind === true }"
>
<i class="iconfont icon-bind"></i>
<span>已有小兔鲜账号,请绑定手机</span>
</a>
<a
href="javascript:;"
@click="isBind = false"
:class="{ active: isBind === false }"
>
<i class="iconfont icon-edit"></i>
<span>没有小兔鲜账号,请完善资料</span>
</a>
</nav>
<!-- 2.2 tab 内容 -->
<div class="tab-content">
<KeepAlive>
<component
:is="isBind ? CallbackBind : CallbackRegister"
/>
</KeepAlive>
</div>
</section>
<!-- 3. 底部 -->
<LoginFooter />
</template>
QQ头像和昵称渲染
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlcJxvhl-1668072991739)(media/image-20220222213959584.png)]
TS类型声明文件
新建文件:src\types\api\qq.d.ts
export interface Data {
ret: number;
msg: string;
is_lost: number;
nickname: string;
gender: string;
gender_type: number;
province: string;
city: string;
year: string;
constellation: string;
figureurl: string;
figureurl_1: string;
figureurl_2: string;
figureurl_qq_1: string;
figureurl_qq_2: string;
figureurl_qq: string;
figureurl_type: string;
is_yellow_vip: string;
vip: string;
yellow_vip_level: string;
level: string;
is_yellow_year_vip: string;
}
export interface QQUserInfo {
status: string;
fmt: string;
ret: number;
code: number;
data: Data;
seq: string;
dataText: string;
}
统一出口导出 src\types\index.d.ts
// 统一导出所有自定义的类型文件
export * from "./api/home";
export * from "./api/category";
export * from "./api/goods";
export * from "./api/member";
+export * from "./api/qq";
渲染用户信息
- 父组件传递属性
<script setup lang="ts">
// ...
+import { ref } from "vue";
+import type { QQUserInfo } from "@/types";
+const userInfo = ref<QQUserInfo>();
// 1. 检查用户是否已登录
if (QC.Login.check()) {
// 2. 获取 QQ 用户唯一标识 openId
QC.Login.getMe((openId) => {
console.log("openId", openId);
});
// 3. 获取用户资料
- QC.api("get_user_info").success((res: unknown) => {
+ QC.api("get_user_info").success((res: QQUserInfo) => {
// console.log("获取用户资料", res);
+ userInfo.value = res;
});
}
const isBind = ref(true);
</script>
<template>
<!-- 2.2 tab 内容 -->
<div class="tab-content">
<KeepAlive>
<component
+ :user-info="userInfo"
:is="isBind ? CallbackBind : CallbackRegister"
/>
</KeepAlive>
</div>
</section>
</template>
- 子组件渲染
<script setup lang="ts">
import type { QQUserInfo } from "@/types";
defineProps<{
userInfo?: QQUserInfo;
}>();
</script>
<template>
<!-- 🚨防止控制台报错,QQ用户信息没回来不渲染 -->
<div class="xtx-form" v-if="userInfo">
<div class="user-info">
<img :src="userInfo?.data.figureurl_2" alt="" />
<p>
Hi,{{ userInfo?.data.nickname }}
欢迎来小兔鲜,完成绑定后可以QQ账号一键登录哦~
</p>
</div>
...
</template>
QQ三方登录 - 多状态介绍
回跳地址这样一个组件内做判断态,分以下 3 种状态。
状态1:已经有账号并且已经绑定 qq
直接获取用户信息,并直接跳转。
状态2:QQ
绑定某个已注册账号,绑定成功后,获取用户信息,再跳转。
状态3:注册全新账号并绑定 QQ
,注册成功后,获取用户信息,再跳转。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xleEomrB-1668072991739)(media/image-20220222213459020.png)]
有账号未绑定 (绑定测试账号)🚨🚨
目标:把自己的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pwRAtjF6-1668072991749)(media/image-20220222215020282.png)]
🔔温馨提示:如果测试账号已被绑定的,手动调用一下解绑被占用的测试手机号。
http://pcapi-xiaotuxian-front-devtest.itheima.net/login/social/unbind?mobile=测试手机号
实现思路
- 进行手机号码校验
- 进行短信验证码发送 (🚨必须调用接口,验证码接口比较多,要看清楚后再调用)
- 进行绑定,完成后把当前用户数据存入
Pinia
,跳转到首页
手机号码校验
<script setup lang="ts">
import { message } from "@/components/XtxUI";
import type { QQUserInfo } from "@/types";
import { reactive } from "vue";
defineProps<{
userInfo?: QQUserInfo;
}>();
// 准备表单数据
const form = reactive({
mobile: "13535337057",
code: "",
});
// 发送验证码
const sendCode = () => {
if (!/^1[3-9]\d{9}$/.test(form.mobile)) {
return message({ type: "warn", text: "手机号码格式错误~" });
}
message({ type: "success", text: "可以发送验证码~" });
};
</script>
模板绑定
<template>
...
<input
+ v-model="form.mobile"
+ maxlength="11"
class="input"
type="text"
placeholder="绑定的手机号"
/>
...
<input
+ v-model="form.code"
+ maxlength="6"
class="input"
type="text"
placeholder="短信验证码"
/>
+ <span class="code" @click="sendCode">发送验证码</span>
...
</template>
验证码按钮倒计时效果
自己补充
发送验证码业务
🚨注意:这个发送的动作必须要有!也就是接口必须要调用才可以绑定。
接口文档:三方登录_发送已有账号短信
Path: /login/social/code
Method: GET
请求参数
Query
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
mobile | 否 | 13211112222 | 手机号 |
参考代码
<script setup lang="ts">
import { message } from "@/components/XtxUI";
import useStore from "@/store";
import type { QQUserInfo } from "@/types";
import { reactive } from "vue";
defineProps<{
userInfo?: QQUserInfo;
}>();
// 准备表单响应式数据
const form = reactive({
mobile: "13012345764",
code: "123456",
});
const { member } = useStore();
// 发送验证码按钮
const sendCode = () => {
// 1. 进行手机号格式校验
if (!/^1[3-9]\d{9}$/.test(form.mobile)) {
// 校验不通过,提醒用户
return message({ type: "warn", text: "手机号码格式错误~" });
}
// 2. 调用获取验证码接口
await member.getCodeBind(form.mobile);
};
</script>
unionId 参数父传子
目标:把
unionId
参数传递给子组件,作为
父组件:src\views\Login\callback.vue
<script setup lang="ts">
...
const userInfo = ref<QQUserInfo>();
+const unionId = ref("");
// 1. 检查用户是否已登录
if (QC.Login.check()) {
// 2. 获取 QQ 用户唯一标识 openId
QC.Login.getMe((openId) => {
+ unionId.value = openId;
});
// 3. 获取用户资料
QC.api("get_user_info").success((res: QQUserInfo) => {
userInfo.value = res;
});
}
</script>
<template>
...
<section class="container">
<!-- 2.2 tab 内容 -->
<div class="tab-content">
<KeepAlive>
<component
:userInfo="userInfo"
+ :unionId="unionId"
:is="isBind ? CallbackBind : CallbackRegister"
/>
</KeepAlive>
</div>
</section>
...
</template>
子组件:src\views\Login\components\callback-bind.vue
<script setup lang="ts">
defineProps<{
userInfo?: QQUserInfo;
+ unionId: string;
}>();
</script>
QQ登录并绑定手机号
接口文档:三方登录_账号绑定
Path: /login/social/bind
Method: POST
请求参数
Body
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
unionId | string | 必须 | 三方标识 | QQ登录后的 openId | |
mobile | string | 必须 | 手机号 | ||
code | string | 必须 | 验证码 |
Pinia 定义
import { http } from "@/utils/request";
import { defineStore } from "pinia";
import type { Profile } from "@/types";
import { message } from "@/components/XtxUI";
// 📌在非 .vue 组件文件中,可通过导入 router 直接获取路由实例
import router from "@/router";
import {
clearStorageProfile,
getStorageProfile,
saveStorageProfile,
} from "@/utils/storage";
const useMemberStore = defineStore("member", {
// 🚨使用插件遇到小问题,自己手写本地存储
// persist: true,
// 状态
state: () => ({
// 用户资料
profile: {} as Profile,
}),
// 方法
actions: {
// 登录成功后的复用逻辑封装
loginSuccess(profile: Profile) {
// 1. 提示
message({ type: 'success', text: '登录成功' });
// 2. 存储数据
this.profile = profile;
// 🔔获取目标页面 target,如果没有目标页,则设置默认值 '/' 去首页
const { target = '/' } = router.currentRoute.value.query;
// 3. 跳转页面
router.push(target as string);
},
// 用户名密码登录
async login(data: { account: string; password: string }) {
// 发送请求
const res = await http<Profile>("POST", "/login", data);
// 调用登录成功后的逻辑
this.loginSuccess(res.data.result);
},
// 退出登录
async logout() {
// 1. 提示
message({ type: 'success', text: '退出成功' });
// 2. 清空用户信息
this.profile = {} as Profile;
// 3. 跳转页面
router.push('/login');
},
// 三方登录_账号绑定 /login/social/bind
async loginBind(data: { unionId: string; mobile: string; code: string }) {
// 发送请求
const res = await http<Profile>("POST", "/login/social/bind", data);
// 调用登录成功后的逻辑
this.loginSuccess(res.data.result);
},
// 获取三方登录验证码 /login/social/code
async loginBindGetCode(mobile: string) {
// 🚨注意接口:三方登录_发送已有账号短信
await http('GET', '/login/social/code', { mobile: mobile });
// 🔔温馨提醒:验证码是发送到用户手机上的
message({ type: 'success', text: '验证码发送成功' });
},
},
});
export default useMemberStore;
QQ 三方登录回跳处理
🔔遇到问题:QQ
三方登录无法实现回跳,如何解决?
目标:把路由的
target
目标页传递给
<a
:href="`https://graph.qq.com/oauth2.0/authorize?client_id=100556005&response_type=token&scope=all&redirect_uri=http%3A%2F%2Fwww.corho.com%3A8080%2F%23%2Flogin%2Fcallback?target=${$route.query.target || '/'}`"
>
<img
src="https://qzonestyle.gtimg.cn/qzone/vas/opensns/res/img/Connect_logo_7.png"
alt=""
/>
</a>
有账号已绑定 (QQ登录直接跳转)🚨
目标:
实现思路
- 回跳组件初始化的时候,获取openId (openId => 对应用户身份 - QQ返回的唯一id身份标识)
- 根据
openId
去自己后台尝试进行 直接登录 - 如果成功,就代表已注册已绑定,记录返回的用户信息,跳转到首页或者来源页面
接口描述:三方直接登录
Path: /login/social
Method: POST
请求参数
Body
名称 | 类型 | 是否必须 | 备注 |
---|---|---|---|
unionId | string | 必须 | 三方标识 |
source | integer | 必须 | 注册来源 注册来源,1为pc,2为webapp,3为微信小程序,4为Android,5为ios,6为qq,7为微信 |
代码落地
1)准备使用QQ uionId进行应用登录的接口
const useMemberStore = defineStore("member",{
// 方法
actions: {
...
// QQ直接登录
async loginQQUnionId(data: { unionId: string; source: number }) {
const res = await http<Profile>("POST", "/login/social", data);
// console.log("POST", "/login/social", res.data.result);
this.profile = res.data.result;
this.loginSuccess();
},
},
});
export default useMemberStore;
调用
<script setup lang="ts">
...
// 1. 检查用户是否已登录
if (QC.Login.check()) {
// 2. 获取 QQ 用户唯一标识 openId
QC.Login.getMe((openId) => {
console.log("openId", openId);
+ // 🚨 获取 openId 后,尝试直接登录
+ member.loginQQUnionId({ unionId: openId, source: 6 });
unionId.value = openId;
});
// 3. 获取用户资料
QC.api("get_user_info").success((res: QQUserInfo) => {
// console.log("获取用户资料", res.data);
userInfo.value = res;
});
}
</script>
可以考虑升级成枚举
// 1为pc,2为webapp,3为微信小程序,4为Android,5为ios,6为qq,7为微信
enum LoginSource {
PC = 1,
WebApp,
MiniProgram,
Android,
IOS,
QQ,
WeChat,
}
无账号未绑定 (注册登录-课后作业)
说明:业务流程和绑定测试账号流程几乎一致,表单校验,发送验证码,绑定
openId
实现注册登录。🚨温馨提示:一个手机号和一个
- 如果
http://pcapi-xiaotuxian-front-devtest.itheima.net/login/social/unbind?mobile=手机号
- 如果手机号已被使用,更换新的手机号,建议记到小本本上,否则无法找回。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QWQhsf0i-1668072991751)(media/11-1628025535476.png)]
接口三方登录-注册登录
Path: /login/social/:unionId/complement
Method: POST
路径参数
参数名称 | 示例 | 备注 |
---|---|---|
unionId | megasuiscool | unionId |
Body
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
account | string | 必须 | |||
mobile | string | 必须 | |||
code | string | 必须 | |||
password | string | 必须 |
倒计时逻辑函数封装-课后练习
封装一个通用的倒计时逻辑函数
useCountDown
需求描述
-
支持自定义参数传入定制倒计时初始值
eg:
useCountDown(60)
-
返回当前计数/暂停计时方法/开始计时方法供业务使用
eg:
const {countTime, start, pause} = useCountDown(60)
逻辑实现
核心逻辑:每隔一秒钟,对 countTime 进行减 1 操作
- resume:可执行函数,开启定时器,每隔一个时间执行一次
- pause: 可执行函数,只要执行这个函数,定时器就会暂停
组件中写法
<script setup lang="ts">
import { useIntervalFn } from "@vueuse/core";
import { ref } from "vue";
const countTime = ref(0);
const { pause, resume } = useIntervalFn(
() => {
countTime.value--;
if (countTime.value <= 0) {
pause();
}
},
1000,
{ immediate: false }
);
const start = () => {
countTime.value = 60;
resume();
};
</script>
<template>
<div>
{{ countTime }}
<button @click="start">开始定时器</button>
<button @click="pause">暂停定时器</button>
<button @click="resume">继续定时器</button>
</div>
</template>
封装成 hooks 钩子函数
在 src\hooks\index.ts
文件中新增以下代码:
/**
* 倒计时效果
* @param startTime 初始化时间
* @returns count 倒计时秒数
* @returns start 初始化启动
* @returns resume 继续启动
* @returns pause 暂停(清理定时器)
*/
export const useCountDown = (startTime = 60) => {
// 倒计时秒数
const count = ref(0);
// resume 继续启动
// pause 暂停(清理定时器)
const { resume, pause } = useIntervalFn(
() => {
// 倒计时减少
count.value--;
// 倒计时结束
if (count.value === 0) {
pause();
}
},
1000,
// 不需要立即执行
{ immediate: false }
);
// 初始化启动函数
const start = () => {
// 倒计时
count.value = startTime;
resume();
};
// count 倒计时秒数
// start 初始化启动
// resume 继续启动
// pause 暂停(清理定时器)
return { count, resume, pause, start };
};
test 组件测试
<script setup lang="ts">
import { useCountDown } from '@/hooks';
// 封装后调用很方便
const { start, pause, resume, count } = useCountDown();
</script>
<template>
<h1>Hello vue3</h1>
<button @click="start">初始化启动</button>
<button @click="pause">暂停</button>
<button @click="resume">继续启动</button>
<h3>{{ count }}</h3>
</template>
课后应用
- 自己把封装的倒计时 hooks 钩子函数,整合到发送验证码业务中。
更多推荐
所有评论(0)