危险

为之则易,不为则难

0%

10_第三方登录

今日目标

✔ 了解第三方登录实现的过程。

登录流程梳理

image-20220226162639253

  1. 在登录页面,QQ 登录按钮处,赋予其打开 QQ 登录页面功能。

  2. 回跳的页面得到 QQ 给的唯一标识 openId,根据 openId 去后台查询是否已经绑定过账户。

    • 如果绑定过,完成登录。

    • 没有绑定过,曾经有账号的,可以直接绑定手机号,即为登录;没账号的,完善账户信息(注册账号并和 QQ 号绑定后登录)。

  3. 登录成功后,跳转首页,或者来源页面。

前置工作准备

基本准备

  • 参考文档:准备工作(opens new window)QQ 互联 JS_SDK(opens new window)

  • 大概步骤。

    1. 准备一个已经备案的网站需要有 QQ 登录的逻辑(登录页面,回跳页面)。

    2. 然后在 QQ 互联上进行身份认证,并且审核通过

    3. 在 QQ 互联上创建应用,应用需要域名,备案号,回调地址等。

    4. 等待人工审核,审核通过会得到应用ID 应用key 回调地址

  • 帮大家申请的结果如下。

    1
    2
    # 测试用 appid => 100556005
    # 测试用 redirect_uri => http://www.corho.com:8080/#/login/callback

常见疑问

  • 这个申请工作一般由谁去做?公司的运维(负责管理公司账号的人)。

  • 申请下来的 id,应用 key,回调地址 uri 能改吗?

    1. 都不能修改,否则无效。

    2. 🐛 回调地址 uri 的包含四部分:域名、端口号、哈希路由模式、路由地址都必须完全一致,否则不能展示。

  • 输入地址 http://www.corho.com:8080/#/login/callback 看不到内容?

    1. 修改 vite.config.ts 配置。

    2. 修改电脑的 host 文件,访问本地服务器。

    3. 配置 VueRouter 路由 和 vue 组件。

DNS 解析

  • DNS 解析:网络中的服务器不认域名,只认 IP,DNS 解析的目的就是将域名地址解析成 IP 地址,解析时先以本地的 hosts 文件为主,然后才走线上的 DNS 服务器。

  • 由于本地我们访问的是 http://localhost:8080http://127.0.0.1:8080

  • 而回调地址的域名是 http://www.corho.com:8080,俩个地址不一致是无法进行跳转的。

  • 需要我们修改本地的 hosts 文件,让域名访问时解析到我们本地的 IP 上。

  1. 先问本地 hosts 文件,如果本地配置了域名和地址的映射关系,优先使用 hosts 中的映射。
1
127.0.0.1           www.jd.com
  1. 如果本地 hosts 文件里面没配,比如 www.baidu.com。
1
2
3
4
5
# 会找线上的 DNS 服务器,DNS 服务器就像一个字典,字典中记录大量的 网站域名 和 IP 的对应关系

# DNS 服务器
112.80.248.75 www.baidu.com
xxx.xx.xxx.xx www.xxx.com

电脑环境设置

目标:浏览器访问 http://www.corho.com:8080/#/login/callback 地址,能打开正在开发的 Vue 项目。

  1. 修改 vite 配置,vite.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
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 跨域
},
// ...
})
  1. 修改 host 文件。

Windows 系统

提醒:修改电脑配置,需要先退出 360、各种管家、杀毒软件;如果修改 hosts 文件有弹窗警告,点击信任(因为这是我们自己进行的安全操作);

1
2
3
4
5
6
1. 找到 C:\Windows\System32\drivers\etc 下hosts文件
2. 在文件中加入 127.0.0.1 www.corho.com
3. 保存即可
# 如果提示没有权限
1. 将hosts文件移到桌面,然后进行修改,确认保存。
2. 将桌面hosts文件替换c盘文件

Mac 系统

1
2
3
4
5
6
7
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 项目表示第二步配置成功。

  1. 配置回调页面的路由和组件,views/login/callback.vue
1
2
3
4
5
6
7
<script setup lang="ts" name="LoginCallback">
//
</script>

<template>
<h1>callback-QQ登录回跳页面测试</h1>
</template>

router/index.ts

1
2
3
4
5
// 绑定路由 (一级路由)
{
path: '/login/callback',
component: () => import('@/views/login/callback.vue')
}

步骤验证:http://www.corho.com:8080/#/login/callback,可以看到回调页面组件。

打开弹框

image-20220226165952149

QQ 互联 JS_SDK(opens new window)

  1. 在 index.html 中添加。
1
<script type="text/javascript" charset="utf-8" src="http://connect.qq.com/qc_jssdk.js" data-appid="100556005" data-redirecturi="http://www.corho.com:8080/#/login/callback"></script>
  1. 打开弹框,views/Login/components/login-form.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="action">
<span id="qqLoginBtn"></span>
</div>
</template>

<script name="LoginForm" lang="ts" setup>
onMounted(() => {
;(QC.Login as loginFn)({
btnId: 'qqLoginBtn',
})
})
</script>
  1. 新增 QC 的类型声明,src/env.d.ts

直接使用 QC 在 TS 中会报错,因为 TS 无法识别 QC,且 QQ 第三方登录默认是不支持 TS 的,需要自定义命名空间来实现该效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 如果一个 ts 文件中没有出现 export,表示这些类型定义是全局的
type loginFn = ({ btnId: string }) => void
type loginObj = {
// 检查 QQ 是否登录成功
check: () => boolean
// 获取 openId
getMe: (callback: (openId: string) => void) => void
}

// 声明全局对象
declare namespace QC {
const Login: loginObj | loginFn
const api: (api: 'get_user_info') => { success: (res: any) => void }
}
  1. 在当前页面打开,views/login/components/login-form.vue
1
2
3
<a href="https://graph.qq.com/oauth2.0/authorize?client_id=100556005&amp;response_type=token&amp;scope=all&amp;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="QQ登录" border="0"
/></a>

基本结构

  1. 回调页面的结构和样式,views/login/callback.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
<script lang="ts" setup name="LoginCallback">
import LoginHeader from './components/login-header.vue'
import LoginFooter from './components/login-footer.vue'
</script>

<template>
<LoginHeader></LoginHeader>
<section class="container">
<nav class="tab">
<a href="javascript:;">
<i class="iconfont icon-bind" />
<span>已有小兔鲜账号,请绑定手机</span>
</a>
<a href="javascript:;">
<i class="iconfont icon-edit" />
<span>没有小兔鲜账号,请完善资料</span>
</a>
</nav>
<div class="tab-content" v-if="true">绑定手机</div>
<div class="tab-content" v-else>完善资料</div>
</section>
<LoginFooter></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>
  1. 切换效果,views/login/callback.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
<script lang="ts" setup name="LoginCallback">
import { ref } from 'vue'
import LoginHeader from './components/login-header.vue'
import LoginFooter from './components/login-footer.vue'
// !#1
const hasAccount = ref(true)
</script>

<template>
<LoginHeader></LoginHeader>
<section class="container">
<nav class="tab">
<!-- //!#2 -->
<a href="javascript:;" :class="{ active: hasAccount }" @click="hasAccount = true">
<i class="iconfont icon-bind" />
<span>已有小兔鲜账号,请绑定手机</span>
</a>
<!-- //!#3 -->
<a href="javascript:;" :class="{ active: !hasAccount }" @click="hasAccount = false">
<i class="iconfont icon-edit" />
<span>没有小兔鲜账号,请完善资料</span>
</a>
</nav>
<!-- //!#4 -->
<div class="tab-content" v-if="hasAccount">绑定手机</div>
<div class="tab-content" v-else>完善资料</div>
</section>
<LoginFooter></LoginFooter>
</template>

组件创建

  1. 绑定手机界面,views/login/components/callback-bind.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
<script name="CallbackBind" lang="ts" setup></script>
<template>
<div class="xtx-form">
<div class="user-info">
<img src="http://qzapp.qlogo.cn/qzapp/101941968/57C7969540F9D3532451374AA127EE5B/50" alt="" />
<p>Hi,Tom 欢迎来小兔鲜,完成绑定后可以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>
  1. 完善信息界面,views/login/components/callback-patch.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
<script lang="ts" setup name="CallbackPatch"></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>
  1. 渲染组件,views/login/callback.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
<script lang="ts" setup name="LoginCallback">
import { ref } from 'vue'
import LoginHeader from './components/login-header.vue'
import LoginFooter from './components/login-footer.vue'
import CallbackBind from './components/callback-bind.vue'
import CallbackPatch from './components/callback-patch.vue'
const hasAccount = ref(true)
</script>

<template>
<LoginHeader></LoginHeader>
<section class="container">
<nav class="tab">
<a href="javascript:;" :class="{ active: hasAccount }" @click="hasAccount = true">
<i class="iconfont icon-bind" />
<span>已有小兔鲜账号,请绑定手机</span>
</a>
<a href="javascript:;" :class="{ active: !hasAccount }" @click="hasAccount = false">
<i class="iconfont icon-edit" />
<span>没有小兔鲜账号,请完善资料</span>
</a>
</nav>
<!-- !#mark -->
<div class="tab-content" v-if="hasAccount">
<CallbackBind />
</div>
<div class="tab-content" v-else>
<CallbackPatch />
</div>
</section>
<LoginFooter></LoginFooter>
</template>

有账号

已绑定

有账号已绑定,直接跳转到首页。

初始化回跳组件的时候根据 QQ 的接口获取 openId,根据 openId 去自己后台尝试进行登录,如果成功,就代表已注册已绑定,记录返回的用户信息并跳转到首页或者来源页面。

  1. JS SDK 公开方法。
  • QC.Login.check():检查 QQ 是否已经登录 true。

  • QC.Login.getMe():获取用户标识 openId 将来传给 userQQLogin 作为第一个参数。

  • QC.api("get_user_info"):获取用户资料。

  1. 确认当前 QQ 是否登录成功,views/login/callback.vue
1
2
3
// 获取用户信息
const isLogin = QC.Login.check()
console.log(isLogin)
  1. 获取 openId。
1
2
3
4
5
6
7
8
// 1. 判断用户是否是 QQ 扫码过来的
const isLogin = QC.Login.check()
if (isLogin) {
// 2. 获取 openId
QC.Login.getMe((openId) => {
console.log(openId)
})
}
  1. 封装方法,使用 QQ 登录,store/modules/user.ts
1
2
3
4
5
6
7
8
9
10
//  source: 1 为 pc,2 为 webapp,3 为微信小程序, 4 为 Android, 5 为 ios, 6 为 qq, 7 为微信
async qqLogin(openId: string) {
const res = await request.post<IApiRes<Profile>>('/login/social', {
unionId: openId,
source: 6
})
// 1. 保存用户信息到 state 中
this.profile = res.data.result
setProfile(res.data.result)
},
  1. 登录成功。
1
2
3
4
5
6
7
8
9
10
11
// 1. 判断用户是否是 QQ 扫码过来的
const isLogin = QC.Login.check()
if (isLogin) {
// 2. 获取 openId
QC.Login.getMe(async (openId) => {
// 3. 发送请求进行 QQ 登录
await user.qqLogin(openId)
Message.success('登录成功')
router.push('/')
})
}

未绑定

有账号未绑定,直接绑定 QQ 号。

如果账号已经是绑定的状态,为了方便测试未绑定的情况,可以手动调用一下下面解绑的接口。

http://pcapi-xiaotuxian-front.itheima.net/login/social/unbind?mobile=手机号

https://apipc-xiaotuxian-front.itheima.net/login/social/unbind?mobile=手机号

获取 QQ 头像和昵称

如果 QQ 没绑定过账号,需要绑定已存在的账号或者注册新的账号。

  1. 提供数据类型,types/user.d.ts
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
// QQ 信息-用户详情
export interface QQUserInfo {
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
}
// QQ 返回信息
export interface QQUserInfoRes {
status: string
fmt: string
ret: number
code: number
data: QQUserInfo
seq: string
dataText: string
}
  1. 获取 QQ 信息,views/login/components/callback-bind.vue
1
2
3
4
5
6
7
8
9
10
11
12
<script name="CallbackBind" lang="ts" setup>
import { QQUserInfo, QQUserInfoRes } from '@/types/login'
import { ref } from 'vue'
const qqInfo = ref<QQUserInfo>({} as QQUserInfo)
// 1. 判断QQ是否登录
if (QC.Login.check()) {
// 2. 获取QQ用户信息
QC.api('get_user_info').success((res: QQUserInfoRes) => {
qqInfo.value = res.data
})
}
</script>
  1. 渲染 QQ 信息。
1
2
3
4
<div class="user-info">
<img :src="qqInfo.figureurl_2" alt="" />
<p>Hi,{{ qqInfo.nickname }} 欢迎来小兔鲜,完成绑定后可以QQ账号一键登录哦~</p>
</div>

表单校验

  1. 提取校验规则,utils/validate.ts
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
export function accountRule(value: string) {
// value 是将来使用该规则的表单元素的值
// 1. 必填
// 2. 6-20 个字符,需要以字母开头
// 3. 如何反馈校验成功还是失败,返回 true 才是成功,其他情况失败,返回失败原因
if (!value) return '请输入用户名'
if (!/^[a-zA-Z]\w{5,19}$/.test(value)) return '字母开头且6-20个字符'
return true
}
export function passwordRule(value: string) {
if (!value) return '请输入密码'
if (!/^\w{6,24}$/.test(value)) return '密码是6-24个字符'
return true
}
export function mobileRule(value: string) {
if (!value) return '请输入手机号'
if (!/^1[3-9]\d{9}$/.test(value)) return '手机号格式错误'
return true
}
export function codeRule(value: string) {
if (!value) return '请输入验证码'
if (!/^\d{6}$/.test(value)) return '验证码是6个数字'
return true
}
export function isAgreeRule(value: string) {
if (!value) return '请勾选同意用户协议'
return true
}
  1. 修改登录功能,views/login/components/login-form.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { accountRule, mobileRule, codeRule, passwordRule, isAgreeRule } from '@/utils/validate'

useForm({
validationSchema: {
account: accountRule,
mobile: mobileRule,
code: codeRule,
password: passwordRule,
isAgree: isAgreeRule,
},
initialValues: {
mobile: '13666666666',
code: '123456',
account: 'xiaotuxian001',
password: '123456',
isAgree: true,
},
})
  1. 添加表单校验,views/login/components/callback-bind.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
<script name="CallbackBind" lang="ts" setup>
import { QQUserInfo, QQUserInfoRes } from '@/types/data'
import { useField, useForm } from 'vee-validate'
import { ref } from 'vue'
// #1
import { mobileRule, codeRule } from '@/utils/validate'
const qqInfo = ref<QQUserInfo>({} as QQUserInfo)
if (QC.Login.check()) {
QC.api('get_user_info').success((res: QQUserInfoRes) => {
qqInfo.value = res.data
})
}
// #2
const { validate } = useForm({
validationSchema: {
mobile: mobileRule,
code: codeRule,
},
})
// #3
const { value: mobile, errorMessage: mobileError } = useField<string>('mobile')
const { value: code, errorMessage: codeError } = useField<string>('code')
</script>
<template>
<div class="xtx-form">
<div class="user-info">
<img :src="qqInfo.figureurl_2" alt="" />
<p>Hi,{{ qqInfo.nickname }} 欢迎来小兔鲜,完成绑定后可以QQ账号一键登录哦~</p>
</div>
<!-- //!#4 -->
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-phone"></i>
<input class="input" type="text" placeholder="绑定的手机号" v-model="mobile" />
</div>
<div class="error">{{ mobileError }}</div>
</div>
<!-- //!#5 -->
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-code"></i>
<input class="input" type="text" placeholder="短信验证码" v-model="code" />
<span class="code">发送验证码</span>
</div>
<div class="error">{{ codeError }}</div>
</div>
<a href="javascript:;" class="submit">立即绑定</a>
</div>
</template>
  1. 绑定时完成表单校验。
1
2
3
4
5
const bind = async () => {
const res = await validate()
if (!res.valid) return
// 如果校验,发送请求进行绑定
}
1
<a href="javascript:;" class="submit" @click="bind">立即绑定</a>

发送验证码

  1. 提供 action,用于获取短信绑定 QQ,store/modules/user.ts
1
2
3
4
5
6
7
8
// 绑定 QQ 的短信验证码
async sendQQBindMsg(mobile: string) {
await request.get('/login/social/code', {
params: {
mobile
}
})
}
  1. 点击获取短信验证码时,发送请求获取验证码,views/login/components/callback-bind.vue
1
2
3
4
5
6
7
8
9
10
11
12
const { time, start } = useCountDown(10)
const send = async () => {
if (time.value > 0) return
// 注意这儿只需要检验 mobile 就行啦
const res = await validateMobile()
if (!res.valid) return
// 发送请求绑定qq
await user.sendQQBindMsg(mobile.value)
Message.success('获取验证码成功')
// 开启倒计时
start()
}
  1. 渲染倒计时。
1
<span class="code" @click="send"> {{ time === 0 ? '发送验证码' : `${time}s后发送` }} </span>

QQ 绑定完成

  1. 提供绑定的 action,store/modules/user.ts
1
2
3
4
5
6
7
8
9
async qqBindLogin(openId: string, mobile: string, code: string) {
const res = await request.post<IApiRes<Profile>>('/login/social/bind', {
mobile,
code,
unionId: openId
})
this.profile = res.data.result
setProfile(res.data.result)
},
  1. 校验后,发送请求绑定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let openId = ''
// 1. 判断 QQ 是否登录
if (QC.Login.check()) {
// 2. 获取QQ用户信息
QC.api('get_user_info').success((res: QQUserInfoRes) => {
console.log(res)
qqInfo.value = res.data
})
// 3. 获取openId
QC.Login.getMe((id) => {
openId = id
})
}

const bind = async () => {
const res = await validForm()
if (!res.valid) return
// 如果校验,发送请求进行绑定
await user.qqBindLogin(openId, mobile.value, code.value)
Message.success('QQ绑定成功')
router.push('/')
}

无账号

没有账号,完善注册信息并绑定 QQ 号。

  1. 增加校验类型,utils/validate.ts
1
2
3
4
5
6
7
export function rePasswordRule(value: string, { form }: any) {
if (!value) return '请输入确认密码'
if (!/^\w{6,24}$/.test(value)) return '密码是6-24个字符'
// 校验密码是否一致 form表单数据对象
if (value !== form.password) return '两次输入的密码不一致'
return true
}
  1. 提供两个 action,store/modules/user.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async sendQQPathMsg(mobile: string) {
await request.get('/register/code', {
params: {
mobile
}
})
},

async qqPatchLogin(data: any) {
const res = await request.post<ApiRes<Profile>>(
`/login/social/${data.openId}/complement`,
data
)
// 1. 保存用户信息到 state 中
this.profile = res.data.result
setProfile(res.data.result)
}
  1. 完整代码,views/login/components/callback-patch.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
<script lang="ts" setup name="CallbackPatch">
import { useField, useForm, useValidateForm, useValidateField } from 'vee-validate'
import { accountRule, mobileRule, codeRule, passwordRule, rePasswordRule } from '@/utils/validate'
import { useCountDown } from '@/hooks'
import Message from '@/components/message'
import useStore from '@/store'
import { useRouter } from 'vue-router'
const { user } = useStore()
const router = useRouter()
// 1. 获取openId
let openId = ''
// 判断QQ是否登录
if (QC.Login.check()) {
// 获取openId
QC.Login.getMe((id) => {
openId = id
})
}

// 表单校验

useForm({
validationSchema: {
account: accountRule,
mobile: mobileRule,
code: codeRule,
password: passwordRule,
repassword: rePasswordRule,
},
})

const { errorMessage: accountError, value: account } = useField<string>('account')

const { errorMessage: passwordError, value: password } = useField<string>('password')
const { errorMessage: mobileError, value: mobile } = useField<string>('mobile')
const { errorMessage: codeError, value: code } = useField<string>('code')
const { errorMessage: repasswordError, value: repassword } = useField<string>('repassword')

const validForm = useValidateForm()
const bind = async () => {
const res = await validForm()
if (!res.valid) return
await user.qqPatchLogin({
openId,
mobile: mobile.value,
code: code.value,
account: account.value,
password: password.value,
})
Message.success('注册成功')
router.push('/')
}

// 获取验证码
const validMobile = useValidateField('mobile')
const { time, start } = useCountDown(60)
const send = async () => {
if (time.value > 0) return
// console.log('获取验证码')
// 单独校验手机号
const res = await validMobile()
if (!res.valid) {
return
}
await user.sendQQPathMsg(mobile.value)
Message.success('获取验证码成功')

start()
}
</script>
<template>
<div class="xtx-form">
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-user"></i>
<input class="input" v-model="account" type="text" placeholder="请输入用户名" />
</div>
<div class="error">{{ accountError }}</div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-phone"></i>
<input class="input" v-model="mobile" type="text" placeholder="请输入手机号" />
</div>
<div class="error">{{ mobileError }}</div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-code"></i>
<input class="input" v-model="code" type="text" placeholder="请输入验证码" />
<span class="code" @click="send">{{ time === 0 ? '发送验证码' : `${time}s后发送` }}</span>
</div>
<div class="error">{{ codeError }}</div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-lock"></i>
<input class="input" v-model="password" type="password" placeholder="请输入密码" />
</div>
<div class="error">{{ passwordError }}</div>
</div>
<div class="xtx-form-item">
<div class="field">
<i class="icon iconfont icon-lock"></i>
<input class="input" v-model="repassword" type="password" placeholder="请确认密码" />
</div>
<div class="error">{{ repasswordError }}</div>
</div>
<a href="javascript:;" class="submit" @click="bind">立即提交</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>