登录模块
路由与组件
目标:登录组件在书写一级路由的时候已经准备,添加路由链接跳转到登录页即可。
- 添加跳转链接:
src/components/app-topnav.vue
<li><RouterLink to="/login">请先登录</RouterLink></li>
结构布局-CV
静态结构参考代码 - CV
src\views\Login\index.vue
<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>
<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>
消息提示组件 和 复选框组件
XtxCheckBox 组件使用
<script setup lang="ts">
import { ref } from "vue";
const isAgree = ref(false);
</script>
<XtxCheckBox v-model="isAgree">我已同意</XtxCheckBox>
Message 组件使用
<script setup lang="ts">
import { message } from "@/components/XtxUI";
message({ type: "success", text: "登录成功", time: 2000});
</script>
登录前表单校验
目标:校验之前我们已经实现了很多次,这里弱化验证写法,简单做一个非空检验即可。
参考代码
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { message } from '@/components/XtxUI';
const isAgree = ref(false);
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>
会员 Store 和 类型声明
定义 Store
定义新 Store: src\store\modules\member.ts
import { defineStore } from 'pinia';
export const useMemberStore = defineStore('member', () => {
// 记得 return
return {};
});
合并新 Store: src\store\index.ts
export * from './modules/member';
定义类型声明
新建类型声明文件:src\types\modules\member.d.ts
// 类型声明文件
合并类型声明:src\types\index.d.ts
// 统一导出所有类型文件
export * from "./modules/member";
账户登录实现
本节目标:实现账户名密码登录功能
用户名和密码登录接口
基本信息
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';
export const useMemberStore = defineStore('member', () => {
// 用户名和密码登录
const loginAccount = async (data: object) => {
const res = await http('POST', '/login', data);
console.log('/login', res.data.result);
};
// 记得 return
return {
loginAccount,
};
});
3)登录表单调用
login-form.vue
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { message } from '@/components/XtxUI';
+ import { useMemberStore } from '@/store';
const isAgree = ref(false);
const form = reactive({
account: '',
password: '',
});
const loginBtn = () => {
if (!form.account) {
return message({ type: 'error', text: '用户名或手机号不能为空' });
}
if (!form.password) {
return message({ type: 'error', text: '密码不能为空' });
}
if (!isAgree.value) {
return message({ type: 'error', text: '请同意许可' });
}
+ // 发送登录请求
+ const member = useMemberStore();
+ member.loginAccount(form);
};
</script>
定义TS类型
类型文件:src\types\modules\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 { message } from '@/components/XtxUI';
import type { Profile } from '@/types';
import { http } from '@/utils/request';
import { defineStore } from 'pinia';
import { ref } from 'vue';
// 🎯非 vue 组件,需导入路由实例,不能使用 useRouter()
import router from '@/router';
export const useMemberStore = defineStore('member', () => {
// 用户信息
const profile = ref<Profile>();
// 用户名和密码登录
const loginAccount = async (data: object) => {
// 0. 发送登录请求
const res = await http<Profile>('POST', '/login', data);
// 1. 保存用户信息
profile.value = res.data.result;
// 2. 请求成功给用户提示
message({ type: 'success', text: '登录成功' });
// 3. 跳转页面
router.push('/');
};
// 记得 return
return {
loginAccount,
profile,
};
});
持久化存储
登录成功后,把用户信息保存到本地。
{
persist: {
// 持久化存储 profile
paths: ['profile'],
},
}
用户信息渲染
用户信息渲染: Layout/components/app-topnav.vue
<script setup lang="ts">
import { useMemberStore } from '@/store';
import { storeToRefs } from 'pinia';
const member = useMemberStore();
// storeToRefs 解构出来的数据还能保持响应式
const { profile } = storeToRefs(member);
</script>
<template>
<nav class="app-topnav">
<div class="container">
<ul>
<template v-if="profile">
<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>
退出登录实现
本节目标: 实现账户名密码登录功能
- 提供 actions, 清空用户数据
// 退出登录
const logout = async () => {
// 1. 清理 Pinia 用户信息
profile.value = undefined;
// 2. 提示用户
message({ type: 'success', text: '退出成功' });
// 3. 跳转页面
router.push('/login');
};
// 记得 return
- 页面中调用
src\views\Layout\components\app-topnav.vue
<a @click="member.logout()" href="javascript:;">退出登录</a>
优化:登录成功页面回跳
携带路由完整路径:src\views\Layout\components\app-topnav.vue
- <RouterLink to="/login">请先登录</RouterLink>
+ <RouterLink :to="`/login?target=${$route.fullPath}`">请先登录</RouterLink>
登录成功后回跳:src\store\modules\member.ts
// 用户名和密码登录
const loginAccount = async (data: object) => {
// 0. 发送登录请求
const res = await http<Profile>('POST', '/login', data);
// 1. 保存用户信息
profile.value = res.data.result;
// 2. 请求成功给用户提示
message({ type: 'success', text: '登录成功' });
+ // 🐛 在非 .vue 组件中 useRoute() 返回 undefined,没法获取当前路由信息
+ // 📌 解决方案,通过 router 路由实例 currentRoute 获取
+ const { target = '/' } = router.currentRoute.value.query;
// 3. 跳转页面
- router.push('/');
+ router.push(target as string);
};
请求拦截器和响应拦截器
请求拦截器 - 请求成功携带 token
完善请求拦截器 utils/request.ts
// 官方说明:https://pinia.vuejs.org/core-concepts/outside-component-usage.html
// 中文翻译:https://fedocs.gitee.io/docs-pinia-zh/zh/core-concepts/outside-component-usage.html
// ❌ 非组件中,Pinia 常见错误写法
// const { member } = useStore();
// 添加请求拦截器
instance.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
// ✅ 在组件外,哪里使用,写哪里前面
const member = useMemberStore();
// 1. 获取 token
const token = member.profile?.token;
// 2. 如果有 token
if (token) {
// 3. 请求头携带 token 信息
config.headers = {
...config.headers, // 保留原来的 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三方登录 - 前置环境和交互
登录简要流程梳理
本节目标: 了解第三方登录的实现流程

- 在登录页面,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. 路由地址 都必须完全一致,否则不能展示。
- 🚨 访问 www.corho.com:8080/ 看不到内容?
- 修改脚手架
vite.config.ts配置。 - 修改电脑的
host文件,访问本地服务器。
- 修改脚手架
QQ三方登录-电脑环境设置🚨
目标:浏览器访问 www.corho.com:8080/#/ 地址,看到开发的本地
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, // 帮我们打开浏览器
},
...
});
第二步:修改 host 文件
windows 系统
🔔其他情况:
- 如果没有 hosts 文件,请新建 hosts 文件(无后缀名)
- 修改电脑配置,需要先退出 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 回车即可
关键步骤验证
📌步骤验证:浏览器访问 www.corho.com:8080/#/ 能看到自己开发的 Vue3 项目表示成功。
QQ授权登录实现
按钮跳转实现
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登录按钮,点击后新窗口打开登录页面
小知识补充:
// 这是什么东西,怎么看起来像乱码?
http%3A%2F%2Fwww.corho.com%3A8080%2F%23%2Flogin%2Fcallback
http://www.corho.com:8080/#/login/callback
// 解码
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'
为了更方便维护
<script>
const client_id = '100556005';
const redirect_uri = encodeURIComponent(
'http://www.corho.com:8080/#/login/callback'
);
const qqLoginUrl = `https://graph.qq.com/oauth2.0/authorize?response_type=token&scope=all&client_id=${client_id}&redirect_uri=${redirect_uri}`;
</script>
<a :href="qqLoginUrl">
<img
src="https://qzonestyle.gtimg.cn/qzone/vas/opensns/res/img/Connect_logo_7.png"
alt=""
/>
</a>
回跳页面路由准备
前面的配置完成了:www.corho.com:8080/#/
接下来在完善:www.corho.com:8080/#/login/cal…
配置路由和组件
新建组件:views/Login/callback.vue
<script setup lang="ts">
//
</script>
<template>
<h1>三方登录的回跳页面</h1>
</template>
配置路由:
2)绑定路由 (一级路由)
{
path: '/login/callback',
component: () => import('@/views/Login/callback.vue')
},
📌步骤验证:www.corho.com:8080/#/login/cal… 看到回调页面组件。
🚨常见错误:没有使用 hash 哈希路由模式,无法识别 # 哈希部分路径,请检查并修正。
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;
};
}
测试 3 个 API
<script setup lang="ts">
// 1. 检查用户是否已登录
if (QC.Login.check()) {
// 2. 获取用户资料
QC.api("get_user_info").success((res: any) => {
console.log("😀获取用户资料", res);
});
// 3. 获取 QQ 用户唯一标识 openId
QC.Login.getMe((openId) => {
console.log("🗝️openId", openId);
});
}
</script>
<template>
<h1>callback-QQ登录回跳页面测试</h1>
</template>
回跳组件静态结构
目标:准备静态结果,渲染切换效果。
静态结构
-
复制组件静态结构:
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="@/assets/images/200.png" 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)使用组件
完成切换交互
实现思路 : 典型的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">
<div class="tab-content">
<!-- 👶普通写法,能互斥切换,但是组件反复挂载和卸载状态会丢失 -->
<!--
<CallbackBind v-if="isBind" />
<CallbackRegister v-else />
-->
<!-- 💪升级写法,KeepAlive + component 缓存组件,保存组件的状态 -->
<KeepAlive>
<component :is="isBind ? CallbackBind : CallbackRegister" />
</KeepAlive>
</div>
</div>
</section>
<!-- 3. 底部 -->
<LoginFooter />
</template>
QQ头像和昵称渲染
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: QQUserInfo) => {
// console.log("获取用户资料", res);
+ userInfo.value = res;
});
}
const isBind = ref(true);
</script>
<template>
<!-- 2.2 tab 内容 -->
<div class="tab-content">
<KeepAlive>
<component
+ :userInfo="userInfo"
:is="isBind ? CallbackBind : CallbackRegister"
/>
</KeepAlive>
</div>
</section>
</template>
- 子组件渲染
<script setup lang="ts">
import type { QQUserInfo } from "@/types";
defineProps<{
userInfo: QQUserInfo | undefined;
}>();
</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,注册成功后,获取用户信息,再跳转。
有账号未绑定 (绑定测试账号)🚨🚨
目标:把自己的
🔔温馨提示:如果测试账号已被绑定的,手动调用一下解绑被占用的测试手机号。
实现思路
- 收集接口所需的 三个 参数
- 进行短信验证码发送 (🚨必须调用接口,验证码接口比较多,看清楚后再调用)
- 进行绑定,完成后把当前用户数据存入
Pinia,跳转到首页
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 | undefined;
+ unionId: string;
}>();
</script>
表单数据收集
- 准备响应式数据
// 表单信息收集
const form = reactive({
mobile: '13012345729',
code: '123456',
});
- 模板绑定
<template>
...
<input
+ v-model="form.mobile"
class="input"
type="text"
placeholder="绑定的手机号"
/>
...
<input
+ v-model="form.code"
class="input"
type="text"
placeholder="短信验证码"
/>
...
</template>
- 点击绑定按钮,检查数据
<script>
// ...省略其他代码
// 获取会员 Store
const member = useMemberStore();
// 点击绑定按钮(绑定登录)
const loginBtn = () => {
// ...省略表单校验
// 🔔检查三个参数是否收集成功
console.log(unionId, form);
};
</script>
<a @click="loginBtn()" href="javascript:;" class="submit">立即绑定</a>
接口1:三方登录_账号绑定
Path: /login/social/bind
Method: POST
请求参数
Body
| 名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
|---|---|---|---|---|---|
| unionId | string | 必须 | 三方标识 | QQ登录后的 openId | |
| mobile | string | 必须 | 手机号 | ||
| code | string | 必须 | 验证码 |
接口2:三方登录-发送验证码
🚨注意:这个发送的动作必须要有!也就是接口必须要调用才可以绑定。
接口文档:三方登录_发送已有账号短信
Path:/login/social/code
Method: GET
请求参数
Query
| 参数名称 | 是否必须 | 示例 | 备注 |
|---|---|---|---|
| mobile | 否 | 13211112222 | 手机号 |
QQ 绑定登录实现
- store 中添加:
src\store\modules\member.ts
// 第三方绑定登录
const loginBind = async (data: object) => {
// 发送登录请求
const res = await http('POST', '/login/social/bind', data);
console.log('POST', '/login/social/bind', res);
// 登录成功提示
message({ type: 'success', text: '绑定登录成功' });
};
// 第三方绑定登录-发送验证码
const sendCodeBind = async (mobile: string) => {
// 无需接收返回值,验证码是发送给用户手机的
await http('GET', '/login/social/code', { mobile: mobile });
// 成功提示即可
message({ type: 'success', text: '发送验证码成功' });
};
- 页面中调用:
src\views\Login\components\callback-bind.vue
<script>
// 点击绑定按钮(绑定登录)
const loginBtn = () => {
// 调用绑定登录接口
member.loginBind({
...form, // 表单数据
unionId, // 三方标识
});
};
// 发送验证码
const sendCode = () => {
member.sendCodeBind(form.mobile)
}
</script>
<a @click="loginBtn()" href="javascript:;" class="submit">立即绑定</a>
<span @click="sendCode()" class="code">发送验证码</span>
QQ 三方登录回跳处理
🔔遇到问题:QQ 三方登录无法实现回跳,如何解决?
目标:把路由的
target目标页传递给
<script>
// 获取路由信息
const route = useRoute();
// QQ授权登录 id
const client_id = '100556005';
// QQ授权登录回跳地址,绑定成功后跳转到 target 目标页面
const redirect_uri = encodeURIComponent(
`http://www.corho.com:8080/#/login/callback?target=${
route.query.target || '/'
}`
);
// QQ授权登录完整 url 拼接
const qqLoginUrl = `https://graph.qq.com/oauth2.0/authorize?response_type=token&scope=all&client_id=${client_id}&redirect_uri=${redirect_uri}`;
</script>
<a :href="qqLoginUrl">
<img src="https://qzonestyle.gtimg.cn/qzone/vas/opensns/res/img/Connect_logo_7.png" />
</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)准备接口:src\store\modules\member.ts
// 三方登录-直接unionId登录
const loginSocial = async (data: object) => {
const res = await http<Profile>('POST', '/login/social', data);
// console.log('POST', '/login/social', res);
loginSuccess(res.data.result);
};
// 记得 return
2)页面调用:
<script setup lang="ts">
QC.Login.getMe((openId) => {
unionId.value = openId;
// 🚨 获取 openId 后,尝试直接登录
// 如果登录成功,页面会跳转
// 如果不成功,停留在当前绑定账号页面
+ const member = useMemberStore();
+ member.loginSocial({ unionId: openId, source: 6 });
});
</script>
可选操作:升级成 TS 常量枚举类型,提高代码可读性。
// 1为pc,2为webapp,3为微信小程序,4为Android,5为ios,6为qq,7为微信
const enum LoginSource {
PC = 1,
WebApp,
MiniProgram,
Android,
IOS,
QQ,
WeChat,
}
无账号未绑定 (注册登录-课后练习)
说明:业务流程和绑定测试账号流程几乎一致,表单校验,发送验证码,绑定
openId实现注册登录。🚨温馨提示:一个手机号和一个
- 如果
pcapi-xiaotuxian-front-devtest.itheima.net/login/socia…
- 如果手机号已被使用,更换新的手机号,建议记到小本本上,否则无法找回。
接口三方登录-注册登录
Path: /login/social/:unionId/complement
Method: POST
路径参数
| 参数名称 | 示例 | 备注 |
|---|---|---|
| unionId | megasuiscool | unionId |
Body
| 名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
|---|---|---|---|---|---|
| account | string | 必须 | |||
| mobile | string | 必须 | |||
| code | string | 必须 | |||
| password | string | 必须 |