今日目标
✔ 了解第三方登录实现的过程。
登录流程梳理

在登录页面,QQ 登录按钮处,赋予其打开 QQ 登录页面功能。
回跳的页面得到 QQ 给的唯一标识 openId,根据 openId 去后台查询是否已经绑定过账户。
登录成功后,跳转首页,或者来源页面。
前置工作准备
基本准备
常见疑问
这个申请工作一般由谁去做?公司的运维(负责管理公司账号的人)。
申请下来的 id,应用 key,回调地址 uri 能改吗?
都不能修改,否则无效。
🐛 回调地址 uri 的包含四部分:域名、端口号、哈希路由模式、路由地址都必须完全一致,否则不能展示。
输入地址 http://www.corho.com:8080/#/login/callback
看不到内容?
修改 vite.config.ts
配置。
修改电脑的 host
文件,访问本地服务器。
配置 VueRouter
路由 和 vue
组件。
DNS 解析
DNS 解析:网络中的服务器不认域名,只认 IP,DNS 解析的目的就是将域名地址解析成 IP 地址,解析时先以本地的 hosts 文件为主,然后才走线上的 DNS 服务器。
由于本地我们访问的是 http://localhost:8080
或 http://127.0.0.1:8080
。
而回调地址的域名是 http://www.corho.com:8080
,俩个地址不一致是无法进行跳转的。
需要我们修改本地的 hosts 文件,让域名访问时解析到我们本地的 IP 上。
- 先问本地 hosts 文件,如果本地配置了域名和地址的映射关系,优先使用 hosts 中的映射。
- 如果本地 hosts 文件里面没配,比如 www.baidu.com。
1 2 3 4 5
|
112.80.248.75 www.baidu.com xxx.xx.xxx.xx www.xxx.com
|
电脑环境设置
目标:浏览器访问 http://www.corho.com:8080/#/login/callback 地址,能打开正在开发的 Vue 项目。
- 修改 vite 配置,
vite.config.ts
。
1 2 3 4 5 6 7 8 9 10 11 12
| export default defineConfig({ server: { host: 'www.corho.com', port: 8080, }, })
|
- 修改
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 项目表示第二步配置成功。
- 配置回调页面的路由和组件,
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
,可以看到回调页面组件。
打开弹框

QQ 互联 JS_SDK(opens new window)
- 在 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>
|
- 打开弹框,
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>
|
- 新增 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
| type loginFn = ({ btnId: string }) => void type loginObj = { check: () => boolean getMe: (callback: (openId: string) => void) => void }
declare namespace QC { const Login: loginObj | loginFn const api: (api: 'get_user_info') => { success: (res: any) => void } }
|
- 在当前页面打开,
views/login/components/login-form.vue
。
1 2 3
| <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" ><img src="https://qzonestyle.gtimg.cn/qzone/vas/opensns/res/img/Connect_logo_7.png" alt="QQ登录" border="0" /></a>
|
基本结构
- 回调页面的结构和样式,
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>
|
- 切换效果,
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' 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> <div class="tab-content" v-if="hasAccount">绑定手机</div> <div class="tab-content" v-else>完善资料</div> </section> <LoginFooter></LoginFooter> </template>
|
组件创建
- 绑定手机界面,
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>
|
- 完善信息界面,
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>
|
- 渲染组件,
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> <div class="tab-content" v-if="hasAccount"> <CallbackBind /> </div> <div class="tab-content" v-else> <CallbackPatch /> </div> </section> <LoginFooter></LoginFooter> </template>
|
有账号
已绑定
有账号已绑定,直接跳转到首页。
初始化回跳组件的时候根据 QQ 的接口获取 openId,根据 openId 去自己后台尝试进行登录,如果成功,就代表已注册已绑定,记录返回的用户信息并跳转到首页或者来源页面。
- JS SDK 公开方法。
QC.Login.check()
:检查 QQ 是否已经登录 true。
QC.Login.getMe()
:获取用户标识 openId 将来传给 userQQLogin 作为第一个参数。
QC.api("get_user_info")
:获取用户资料。
- 确认当前 QQ 是否登录成功,
views/login/callback.vue
。
1 2 3
| const isLogin = QC.Login.check() console.log(isLogin)
|
- 获取 openId。
1 2 3 4 5 6 7 8
| const isLogin = QC.Login.check() if (isLogin) { QC.Login.getMe((openId) => { console.log(openId) }) }
|
- 封装方法,使用 QQ 登录,
store/modules/user.ts
。
1 2 3 4 5 6 7 8 9 10
| async qqLogin(openId: string) { const res = await request.post<IApiRes<Profile>>('/login/social', { unionId: openId, source: 6 }) this.profile = res.data.result setProfile(res.data.result) },
|
- 登录成功。
1 2 3 4 5 6 7 8 9 10 11
| const isLogin = QC.Login.check() if (isLogin) { QC.Login.getMe(async (openId) => { 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 没绑定过账号,需要绑定已存在的账号或者注册新的账号。
- 提供数据类型,
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
| 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 }
export interface QQUserInfoRes { status: string fmt: string ret: number code: number data: QQUserInfo seq: string dataText: string }
|
- 获取 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) if (QC.Login.check()) { QC.api('get_user_info').success((res: QQUserInfoRes) => { qqInfo.value = res.data }) } </script>
|
- 渲染 QQ 信息。
1 2 3 4
| <div class="user-info"> <img :src="qqInfo.figureurl_2" alt="" /> <p>Hi,{{ qqInfo.nickname }} 欢迎来小兔鲜,完成绑定后可以QQ账号一键登录哦~</p> </div>
|
表单校验
- 提取校验规则,
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) { 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 }
|
- 修改登录功能,
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, }, })
|
- 添加表单校验,
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' 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 }) } const { validate } = useForm({ validationSchema: { mobile: mobileRule, code: codeRule, }, }) 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> <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> <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 2 3 4 5
| const bind = async () => { const res = await validate() if (!res.valid) return }
|
1
| <a href="javascript:;" class="submit" @click="bind">立即绑定</a>
|
发送验证码
- 提供 action,用于获取短信绑定 QQ,
store/modules/user.ts
。
1 2 3 4 5 6 7 8
| async sendQQBindMsg(mobile: string) { await request.get('/login/social/code', { params: { mobile } }) }
|
- 点击获取短信验证码时,发送请求获取验证码,
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 const res = await validateMobile() if (!res.valid) return await user.sendQQBindMsg(mobile.value) Message.success('获取验证码成功') start() }
|
- 渲染倒计时。
1
| <span class="code" @click="send"> {{ time === 0 ? '发送验证码' : `${time}s后发送` }} </span>
|
QQ 绑定完成
- 提供绑定的 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| let openId = ''
if (QC.Login.check()) { QC.api('get_user_info').success((res: QQUserInfoRes) => { console.log(res) qqInfo.value = res.data }) 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 号。
- 增加校验类型,
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个字符' if (value !== form.password) return '两次输入的密码不一致' return true }
|
- 提供两个 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 ) this.profile = res.data.result setProfile(res.data.result) }
|
- 完整代码,
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() let openId = '' if (QC.Login.check()) { 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 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>
|