feat: 验证码流程

master
xjs 2025-05-21 15:40:20 +08:00
parent 236a33c8ac
commit 1f3d5cfb04
9 changed files with 663 additions and 6 deletions

View File

@ -27,11 +27,34 @@
src="https://api.static.ycymedu.com/images/logo.png" src="https://api.static.ycymedu.com/images/logo.png"
mode="aspectFit" mode="aspectFit"
></image> ></image>
<view <button
class="px-[32rpx] py-[16rpx] bg-[#3370FF] rounded-[40rpx] text-white text-[32rpx] font-medium flex items-center justify-center" class="w-[493rpx]! mb-[40rpx] h-[88rpx]! rounded-[44rpx] text-[32rpx] text-white flex items-center justify-center"
@click="handleLogin" :class="checked.length > 0 ? 'bg-[#1580FF]' : 'bg-[#BFBFBF]'"
@click.stop="handleClick"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
:disabled="checked.length === 0"
> >
立即登录 一键登录
</button>
<view class="flex items-center flex-nowrap">
<CheckboxGroup v-model="checked" class="check-class mr-[10rpx]">
<Checkbox name="1" cell shape="button" class="custom-checkbox"></Checkbox>
</CheckboxGroup>
<view class="flex items-center">
<text class="text-[24rpx] whitespace-nowrap">
已阅读并同意
<text class="text-[#1580FF]" @click.stop="handleClickUserAgreement">
<text>用户协议</text>
</text>
<text class="text-[#1580FF]" @click.stop="handleClickPrivacyPolicy">
<text>隐私条款</text>
</text>
</text>
</view>
</view> </view>
</view> </view>
<LoginMask v-model:show="show" @auth-ready="handleAuthReady" /> <LoginMask v-model:show="show" @auth-ready="handleAuthReady" />
@ -41,11 +64,54 @@
<script setup lang="ts"> <script setup lang="ts">
import LoginMask from './components/LoginMask.vue' import LoginMask from './components/LoginMask.vue'
import Navbar from './components/navbar/Navbar.vue' import Navbar from './components/navbar/Navbar.vue'
import { useUserStore } from '@/store/user'
import {
getSessionKey,
getVolunteerInitialization,
getWxUserInfo,
setWxInfo,
} from '@/service/index/api'
import { City } from '@/types/app-type'
import Checkbox from './components/check-group/Checkbox.vue'
import CheckboxGroup from './components/check-group/CheckboxGroup.vue'
import { useLogin } from '@/login-sub/hooks/useUserInfo'
const show = ref(false) const show = ref(false)
const handleLogin = () => { const checked = ref([]) //
show.value = true const getPhoneInfo = ref(null)
const getPhoneNumber = async (e: any) => {
if (e.detail.errMsg == 'getPhoneNumber:ok') {
const detail = e.detail
let _getPhoneInfo = {
iv: detail.iv,
encryptedData: detail.encryptedData,
code: detail.code,
}
getPhoneInfo.value = _getPhoneInfo
await getUserInfo(detail.code)
} else if (e.detail.errMsg == 'getPhoneNumber:fail not login') {
uni.showToast({
title: '请先登录',
icon: 'none',
})
} else {
uni.showToast({
title: '获取手机号失败',
icon: 'none',
})
}
}
const handleClick = () => {
if (!checked.value) {
uni.showToast({
title: '您需先同意《服务条款》和《隐私条款》',
icon: 'none',
})
return
}
} }
// //
@ -56,4 +122,113 @@ const handleAuthReady = () => {
const handleClickLeft = () => { const handleClickLeft = () => {
uni.navigateBack() uni.navigateBack()
} }
const handleClickUserAgreement = () => {
uni.navigateTo({
url: '/login-sub/userAgreement',
})
}
const handleClickPrivacyPolicy = () => {
uni.navigateTo({
url: '/login-sub/privacyPolicy',
})
}
const userStore = useUserStore()
const getUserInfo = async (_code: string) => {
let userInfo = (await useLogin()) as { code: string; errMsg: string }
if (userInfo.errMsg == 'login:ok') {
const resp = await getSessionKey({ JsCode: userInfo.code })
if (resp.code == 200) {
const result = resp.result as { accessToken: string; openId: string }
userStore.setUserToken(result.accessToken)
userStore.setUserOpenId(result.openId)
setWxInfo({ code: _code, openId: result.openId })
//
getWxUserInfo().then((resp) => {
const infoData = resp.result as unknown as {
userExtend: { provinceCode: string; init: boolean }
zyBatches: any[]
batchDataUrl: string
batchName: string
avatar: string
nickName: string
mobile: string
sex: number
}
userStore.setEstimatedAchievement(infoData.userExtend)
userStore.setZyBatches(infoData.zyBatches)
userStore.setBatchDataUrl(infoData.batchDataUrl)
userStore.setBatchName(infoData.batchName)
userStore.setUserAvatar(infoData.avatar)
userStore.setUserNickName(infoData.nickName)
userStore.setUserBaseInfo({ mobile: infoData.mobile, sex: infoData.sex })
if (resp.code === 200) {
//
getVolunteerInitialization()
.then((res) => {
let list = res.result as any[]
let code = infoData.userExtend ? infoData.userExtend.provinceCode : ''
let addressItem: City
if (code !== '') {
for (let i = 0; i < list.length; i++) {
if (list[i].code == code) {
addressItem = list[i]
}
}
}
userStore.setUserCity(addressItem)
})
.then(() => {
//
if (infoData.userExtend && !infoData.userExtend.init) {
uni.navigateTo({ url: '/login-sub/inviteCode' })
} else {
uni.switchTab({
url: '/pages/home/index/index',
})
}
})
}
})
}
} else {
uni.showToast({
title: '您需先授权',
icon: 'none',
})
}
}
</script> </script>
<style lang="scss" scoped>
:deep(.custom-checkbox) {
display: flex;
align-items: center;
justify-content: center;
.checkbox__icon {
border-radius: 50%;
height: 32rpx;
width: 32rpx;
margin: 0;
}
.custom-box {
width: 32rpx;
height: 32rpx;
border: 1px solid #ddd;
border-radius: 50%;
}
}
:deep(.checkbox-active) {
border-color: #fff !important;
background-color: #fff !important;
}
</style>

View File

@ -0,0 +1,226 @@
<route lang="json5" type="page">
{
style: {
navigationBarTitleText: '基本信息',
},
}
</route>
<template>
<view class="flex flex-col bg-[#f8f8f8] h-screen">
<view class="flex-1 pb-safe">
<view class="mx-[32rpx] mt-[24rpx] bg-[#fff] rounded-[20rpx]">
<form>
<view
class="flex items-center justify-between h-[100rpx] mx-[24rpx] border-b-[2rpx] border-b-solid border-[#F3F3F3]"
>
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">姓名</view>
<view class="text-[#FF5151] font-[500] leading-[1] h-[18rpx] ml-[8rpx]">*</view>
</view>
<view>
<input
v-model="formData.name"
placeholder="请输入姓名"
confirm-type="done"
placeholder-style="color:#BABABA;font-size:28rpx;"
class="text-start w-[140rpx]"
/>
</view>
</view>
<view
class="flex items-center justify-between h-[100rpx] mx-[24rpx] border-b-[2rpx] border-b-solid border-[#F3F3F3]"
>
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">性别</view>
<view class="text-[#FF5151] font-[500] leading-[1] h-[18rpx] ml-[8rpx]">*</view>
</view>
<view>
<RadioGroup v-model="formData.gender" class="custom-radio-group">
<Radio :name="1" class="custom-radio"></Radio>
<Radio :name="2" class="custom-radio"></Radio>
</RadioGroup>
</view>
</view>
<view
class="flex items-center justify-between h-[100rpx] mx-[24rpx] border-b-[2rpx] border-b-solid border-[#F3F3F3]"
>
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">手机号</view>
<view class="text-[#FF5151] font-[500] leading-[1] h-[18rpx] ml-[8rpx]">*</view>
</view>
<view class="text-[#333] text-[28rpx]">{{ userStore.userInfo.mobile }}</view>
</view>
<view class="flex items-center justify-between h-[100rpx] mx-[24rpx]">
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">就读学校</view>
</view>
<view class="">
<input
v-model="formData.school"
placeholder="请输入您的就读学校"
confirm-type="done"
placeholder-style="color:#BABABA;font-size:28rpx;"
class="text-start w-[252rpx]"
/>
</view>
</view>
</form>
</view>
<view class="p-[24rpx] bg-[#fff] mx-[32rpx] mt-[24rpx] rounded-[20rpx]">
<input
v-model="formData.invitedCode"
placeholder="邀请码"
confirm-type="done"
:maxlength="4"
placeholder-style="color:#999;font-size:28rpx;"
class="text-center h-[86rpx] bg-[#F5F5F5] rounded-[16rpx]"
@input="handleInviteCode"
/>
<view class="text-[#666] text-[24rpx] text-center mt-[10rpx]">
输入邀请码获取免费AI报告解读
</view>
</view>
<button
class="w-[560rpx]! h-[88rpx]! rounded-[44rpx] font-500 text-[32rpx] text-white! flex items-center justify-center mt-[80rpx]"
:class="'bg-[#1580FF]!'"
@click="handleSubmit"
>
提交
</button>
</view>
</view>
</template>
<script lang="ts" setup>
import Radio from './components/radio-group/Radio.vue'
import RadioGroup from './components/radio-group/RadioGroup.vue'
import { useUserStore } from '@/store'
import { savePerfectInfo, verifyInviteCode } from '@/service/index/api'
const userStore = useUserStore()
const formData = ref({
name: userStore.userInfo.nickname,
gender: userStore.userInfo.sex || 1,
school: userStore.userInfo.estimatedAchievement.schoolName,
invitedCode: userStore.userInfo.estimatedAchievement.vipCode,
})
const invitedCodeFlag = ref(false)
const handleSubmit = () => {
if (!formData.value.name) {
uni.showToast({
title: '请输入昵称',
icon: 'error',
mask: true,
})
return
}
savePerfectInfo({
nickName: formData.value.name,
schoolName: formData.value.school,
sex: formData.value.gender,
inviteCode: formData.value.invitedCode,
}).then((resp) => {
if (resp.code === 200) {
uni.switchTab({
url: '/pages/home/index/index',
})
} else {
uni.showToast({
title: resp.message,
icon: 'error',
mask: true,
})
}
})
}
const handleInviteCode = () => {
if (formData.value.invitedCode.length === 4) {
verifyInviteCode({ code: formData.value.invitedCode }).then((resp) => {
if (resp.code === 200) {
invitedCodeFlag.value = resp.result as boolean
userStore.setEstimatedAchievement({ init: true })
}
})
} else {
invitedCodeFlag.value = false
}
}
const handleLogout = () => {
userStore.clearUserInfo()
uni.switchTab({
url: '/pages/home/index/index',
})
}
const instance = getCurrentInstance()
onUnload(() => {
console.log(
' userStore.userInfo.estimatedAchievement.init',
userStore.userInfo.estimatedAchievement.init,
)
if (!userStore.userInfo.estimatedAchievement.init) {
handleLogout()
}
}, instance)
</script>
<style lang="scss" scoped>
:deep(.custom-radio-group) {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
background-color: #fff;
justify-content: center;
}
:deep(.custom-radio) {
width: 108rpx;
height: 60rpx;
background-color: #fff;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #ccc;
color: #999;
.radio-wrapper {
padding: 0;
width: 100%;
height: 100%;
}
.radio {
display: none;
}
.radio-label {
margin-left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.radio-label--active {
background-color: rgba(21, 128, 255, 0.1) !important;
border-color: #1580ff !important;
border: 2rpx solid #1580ff;
border-radius: 32rpx;
}
}
</style>

View File

@ -0,0 +1,215 @@
<route lang="json5" type="page">
{
style: {
navigationBarTitleText: '基本信息',
},
}
</route>
<template>
<view class="flex flex-col bg-[#f8f8f8] h-screen">
<view class="flex-1 pb-safe">
<view class="mx-[32rpx] mt-[24rpx] bg-[#fff] rounded-[20rpx]">
<form>
<view
class="flex items-center justify-between h-[100rpx] mx-[24rpx] border-b-[2rpx] border-b-solid border-[#F3F3F3]"
>
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">姓名</view>
<view class="text-[#FF5151] font-[500] leading-[1] h-[18rpx] ml-[8rpx]">*</view>
</view>
<view>
<input
v-model="formData.name"
placeholder="请输入姓名"
confirm-type="done"
placeholder-style="color:#BABABA;font-size:28rpx;"
class="text-start w-[140rpx]"
/>
</view>
</view>
<view
class="flex items-center justify-between h-[100rpx] mx-[24rpx] border-b-[2rpx] border-b-solid border-[#F3F3F3]"
>
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">性别</view>
<view class="text-[#FF5151] font-[500] leading-[1] h-[18rpx] ml-[8rpx]">*</view>
</view>
<view>
<RadioGroup v-model="formData.gender" class="custom-radio-group">
<Radio :name="1" class="custom-radio"></Radio>
<Radio :name="2" class="custom-radio"></Radio>
</RadioGroup>
</view>
</view>
<view
class="flex items-center justify-between h-[100rpx] mx-[24rpx] border-b-[2rpx] border-b-solid border-[#F3F3F3]"
>
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">手机号</view>
<view class="text-[#FF5151] font-[500] leading-[1] h-[18rpx] ml-[8rpx]">*</view>
</view>
<view class="text-[#333] text-[28rpx]">{{ userStore.userInfo.mobile }}</view>
</view>
<view
class="flex items-center justify-between h-[100rpx] mx-[24rpx] border-b-[2rpx] border-b-solid border-[#F3F3F3]"
>
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">就读学校</view>
</view>
<view class="">
<input
v-model="formData.school"
placeholder="请输入您的就读学校"
confirm-type="done"
placeholder-style="color:#BABABA;font-size:28rpx;"
class="text-start w-[252rpx]"
/>
</view>
</view>
<view class="flex items-center justify-between h-[100rpx] mx-[24rpx]">
<view class="flex items-center">
<view class="text-[28rpx] text-[#000] font-[500]">邀请码</view>
</view>
<view class="">
<input
v-model="formData.invitedCode"
placeholder="请输入您的邀请码"
confirm-type="done"
:maxlength="4"
placeholder-style="color:#BABABA;font-size:28rpx;"
class="text-start w-[252rpx]"
@input="handleInviteCode"
/>
</view>
</view>
</form>
</view>
<button
class="w-[560rpx]! h-[88rpx]! rounded-[44rpx] font-500 text-[32rpx] text-white! flex items-center justify-center mt-[80rpx]"
:class="'bg-[#1580FF]!'"
@click="handleSubmit"
>
提交
</button>
</view>
</view>
</template>
<script lang="ts" setup>
import Radio from '@/pages-sub/components/radio-group/Radio.vue'
import RadioGroup from '@/pages-sub/components/radio-group/RadioGroup.vue'
import { useUserStore } from '@/store'
import { savePerfectInfo, verifyInviteCode } from '@/service/index/api'
const userStore = useUserStore()
const formData = ref({
name: userStore.userInfo.nickname,
gender: userStore.userInfo.sex || 1,
school: userStore.userInfo.estimatedAchievement.schoolName,
invitedCode: userStore.userInfo.estimatedAchievement.vipCode,
})
const invitedCodeFlag = ref(false)
const handleSubmit = () => {
if (!formData.value.name) {
uni.showToast({
title: '请输入昵称',
icon: 'error',
mask: true,
})
return
}
savePerfectInfo({
nickName: formData.value.name,
schoolName: formData.value.school,
sex: formData.value.gender,
inviteCode: formData.value.invitedCode,
}).then((resp) => {
if (resp.code === 200) {
userStore.setUserNickName(formData.value.name)
userStore.setUserBaseInfo({ sex: formData.value.gender })
userStore.setEstimatedAchievement({
schoolName: formData.value.school,
vipCode: formData.value.invitedCode,
})
uni.navigateBack()
} else {
uni.showToast({
title: resp.message,
icon: 'error',
mask: true,
})
}
})
}
const handleInviteCode = () => {
if (formData.value.invitedCode.length === 4) {
verifyInviteCode({ code: formData.value.invitedCode }).then((resp) => {
if (resp.code === 200) {
invitedCodeFlag.value = resp.result as boolean
userStore.setEstimatedAchievement({ init: true })
}
})
} else {
invitedCodeFlag.value = false
}
}
</script>
<style lang="scss" scoped>
:deep(.custom-radio-group) {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
background-color: #fff;
justify-content: center;
}
:deep(.custom-radio) {
width: 108rpx;
height: 60rpx;
background-color: #fff;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #ccc;
color: #999;
.radio-wrapper {
padding: 0;
width: 100%;
height: 100%;
}
.radio {
border-radius: 32rpx;
}
.radio-label {
margin-left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.radio-label--active {
background-color: rgba(21, 128, 255, 0.1) !important;
border-color: #1580ff !important;
border: 2rpx solid #1580ff;
border-radius: 32rpx;
}
}
</style>

View File

@ -287,6 +287,13 @@
"navigationBarTitleText": "设置" "navigationBarTitleText": "设置"
} }
}, },
{
"path": "ucenter/setting/userInfo",
"type": "page",
"style": {
"navigationBarTitleText": "基本信息"
}
},
{ {
"path": "ucenter/star/myStar", "path": "ucenter/star/myStar",
"type": "page", "type": "page",
@ -320,6 +327,13 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "inviteCode",
"type": "page",
"style": {
"navigationBarTitleText": "基本信息"
}
},
{ {
"path": "privacyPolicy", "path": "privacyPolicy",
"type": "page" "type": "page"

View File

@ -182,10 +182,16 @@ const toSetting = () => {
} }
const goLogin = () => { const goLogin = () => {
console.log('hhh')
if (!userStore.userInfo.openid) { if (!userStore.userInfo.openid) {
uni.navigateTo({ uni.navigateTo({
url: '/login-sub/index', url: '/login-sub/index',
}) })
} else {
uni.navigateTo({
url: '/pages-sub/ucenter/setting/userInfo',
})
} }
} }
</script> </script>

View File

@ -219,6 +219,9 @@ export const savePerfectInfo = (params: {
score?: number score?: number
sp?: number sp?: number
year?: number year?: number
sex?: number
nickName?: string
inviteCode?: string
}) => { }) => {
return http.post('/api/weChatUserEx/perfectInfo', params) return http.post('/api/weChatUserEx/perfectInfo', params)
} }
@ -470,3 +473,7 @@ export const sendMessage = (params: { conversation_id: string; user: string; que
export const getAssistant = () => { export const getAssistant = () => {
return http.get('/api/weChatUserEx/areaExtend', {}) return http.get('/api/weChatUserEx/areaExtend', {})
} }
export const verifyInviteCode = ({ code }: { code: string }) => {
return http.post('/api/weChatUserEx/verifyInviteCode', { code })
}

View File

@ -5,6 +5,8 @@ import { ref } from 'vue'
const initState = { const initState = {
nickname: '', nickname: '',
avatar: '', avatar: '',
mobile: '',
sex: 0,
city: { city: {
allscore: 0, allscore: 0,
code: '0', code: '0',
@ -97,6 +99,13 @@ export const useUserStore = defineStore(
userInfo.value.avatar = val userInfo.value.avatar = val
} }
const setUserBaseInfo = ({ mobile, sex }: { mobile?: string; sex: number }) => {
userInfo.value.sex = sex
if (mobile) {
userInfo.value.mobile = mobile
}
}
// 清除预估成绩 // 清除预估成绩
const clearUserEstimatedAchievement = () => { const clearUserEstimatedAchievement = () => {
userInfo.value.estimatedAchievement = Object.assign(userInfo.value.estimatedAchievement, { userInfo.value.estimatedAchievement = Object.assign(userInfo.value.estimatedAchievement, {
@ -221,6 +230,7 @@ export const useUserStore = defineStore(
setIsVIP, setIsVIP,
setVipCode, setVipCode,
setIsShowAi, setIsShowAi,
setUserBaseInfo,
} }
}, },
{ {

View File

@ -64,6 +64,8 @@ export type ExtraUserInfo = {
batchName: string batchName: string
wishList: any[] wishList: any[]
isShowAi: boolean isShowAi: boolean
mobile: string
sex: number
} & IUserInfo } & IUserInfo
export type News = { export type News = {

View File

@ -33,10 +33,12 @@ interface NavigateToOptions {
"/pages-sub/ucenter/evaluate/evaluateList" | "/pages-sub/ucenter/evaluate/evaluateList" |
"/pages-sub/ucenter/setting/about" | "/pages-sub/ucenter/setting/about" |
"/pages-sub/ucenter/setting/index" | "/pages-sub/ucenter/setting/index" |
"/pages-sub/ucenter/setting/userInfo" |
"/pages-sub/ucenter/star/myStar" | "/pages-sub/ucenter/star/myStar" |
"/pages-sub/ucenter/vip/openVip" | "/pages-sub/ucenter/vip/openVip" |
"/pages-sub/ucenter/wishList/wishList" | "/pages-sub/ucenter/wishList/wishList" |
"/login-sub/index" | "/login-sub/index" |
"/login-sub/inviteCode" |
"/login-sub/privacyPolicy" | "/login-sub/privacyPolicy" |
"/login-sub/userAgreement" | "/login-sub/userAgreement" |
"/pages-evaluation-sub/aiAutoFill/index" | "/pages-evaluation-sub/aiAutoFill/index" |