feat(auth): refactor mini program login flows
This commit is contained in:
3
env/.env.production
vendored
3
env/.env.production
vendored
@@ -8,3 +8,6 @@ VITE_SHOW_SOURCEMAP = false
|
||||
# 后台请求地址
|
||||
VITE_SERVER_BASEURL = 'https://viewshanghai.com/admin-api'
|
||||
VITE_UPLOAD_BASEURL = 'https://viewshanghai.com/admin-api'
|
||||
|
||||
# 验证码的开关
|
||||
VITE_APP_CAPTCHA_ENABLE=true
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface AuthRegisterReqVO {
|
||||
export interface AuthSmsLoginReqVO {
|
||||
mobile: string
|
||||
code: string
|
||||
captchaVerification?: string
|
||||
}
|
||||
|
||||
/** 发送短信验证码 Request VO */
|
||||
@@ -122,27 +123,43 @@ export function logout() {
|
||||
return http.post<void>('/system/auth/logout')
|
||||
}
|
||||
|
||||
// TODO @芋艿:三方登录
|
||||
/**
|
||||
* 获取微信登录凭证
|
||||
* @returns Promise 包含微信登录凭证(code)
|
||||
*/
|
||||
export function getWxCode() {
|
||||
return new Promise<UniApp.LoginRes>((resolve, reject) => {
|
||||
/** 微信小程序一键登录 Request VO */
|
||||
export interface AuthWeixinMiniAppLoginReqVO {
|
||||
phoneCode: string
|
||||
loginCode: string
|
||||
state: string
|
||||
}
|
||||
|
||||
/** 社交快捷登录 Request VO */
|
||||
export interface AuthSocialLoginReqVO {
|
||||
type: number
|
||||
code: string
|
||||
state: string
|
||||
}
|
||||
|
||||
/** 获取微信小程序 login code */
|
||||
export function getWxLoginCode(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: res => resolve(res),
|
||||
fail: err => reject(new Error(err)),
|
||||
success: (res) => {
|
||||
if (!res.code) {
|
||||
reject(new Error('未获取到有效的微信登录凭证,请重试'))
|
||||
return
|
||||
}
|
||||
resolve(res.code)
|
||||
},
|
||||
fail: err => reject(new Error(err.errMsg)),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TODO @芋艿:三方登录
|
||||
/**
|
||||
* 微信登录
|
||||
* @param params 微信登录参数,包含code
|
||||
* @returns Promise 包含登录结果
|
||||
*/
|
||||
export function wxLogin(data: { code: string }) {
|
||||
return http.post<IAuthLoginRes>('/auth/wxLogin', data)
|
||||
/** 社交快捷登录(已绑定用户) */
|
||||
export function socialLogin(data: AuthSocialLoginReqVO) {
|
||||
return http.post<IAuthLoginRes>('/system/auth/social-login', data)
|
||||
}
|
||||
|
||||
/** 微信小程序一键登录(手机号匹配) */
|
||||
export function weixinMiniAppLogin(data: AuthWeixinMiniAppLoginReqVO) {
|
||||
return http.post<IAuthLoginRes>('/system/auth/weixin-mini-app-login', data)
|
||||
}
|
||||
|
||||
124
src/pages-core/auth/components/sms-login-panel.vue
Normal file
124
src/pages-core/auth/components/sms-login-panel.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<view class="sms-login-panel">
|
||||
<AuthInput
|
||||
v-model="form.mobile"
|
||||
placeholder="请输入手机号"
|
||||
icon="phone"
|
||||
type="number"
|
||||
:maxlength="11"
|
||||
/>
|
||||
<CodeInput
|
||||
v-model="form.code"
|
||||
:mobile="form.mobile"
|
||||
:scene="21"
|
||||
:before-send="validateTenant"
|
||||
/>
|
||||
|
||||
<view v-if="captchaEnabled">
|
||||
<Verify
|
||||
ref="verifyRef"
|
||||
:captcha-type="captchaType"
|
||||
explain="向右滑动完成验证"
|
||||
:img-size="{ width: '300px', height: '150px' }"
|
||||
mode="pop"
|
||||
@success="handleVerifySuccess"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="auth-btn-wrapper" style="margin-top: 40rpx">
|
||||
<wd-button block :loading="loading" custom-class="auth-btn" @click="handleSubmit">
|
||||
登 录
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { redirectAfterLogin } from '@/utils'
|
||||
import { isMobile } from '@/utils/validator'
|
||||
import AuthInput from './auth-input.vue'
|
||||
import CodeInput from './code-input.vue'
|
||||
import Verify from './verifition/verify.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'SmsLoginPanel',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
agreed: boolean
|
||||
redirectUrl?: string
|
||||
validateTenant: () => boolean
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const verifyRef = ref()
|
||||
const captchaEnabled = import.meta.env.VITE_APP_CAPTCHA_ENABLE === 'true'
|
||||
const captchaType = ref('blockPuzzle')
|
||||
const form = reactive({
|
||||
mobile: '',
|
||||
code: '',
|
||||
captchaVerification: '',
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!props.agreed) {
|
||||
toast.warning('请先阅读并同意用户协议和隐私政策')
|
||||
return
|
||||
}
|
||||
if (!props.validateTenant()) {
|
||||
return
|
||||
}
|
||||
if (!form.mobile) {
|
||||
toast.warning('请输入手机号')
|
||||
return
|
||||
}
|
||||
if (!isMobile(form.mobile)) {
|
||||
toast.warning('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
if (!form.code) {
|
||||
toast.warning('请输入验证码')
|
||||
return
|
||||
}
|
||||
|
||||
if (!captchaEnabled) {
|
||||
await handleVerifySuccess({})
|
||||
return
|
||||
}
|
||||
|
||||
verifyRef.value?.show()
|
||||
}
|
||||
|
||||
async function handleVerifySuccess(params: { captchaVerification?: string }) {
|
||||
loading.value = true
|
||||
try {
|
||||
form.captchaVerification = params?.captchaVerification || ''
|
||||
const tokenStore = useTokenStore()
|
||||
await tokenStore.login({ type: 'sms', ...form })
|
||||
redirectAfterLogin(props.redirectUrl)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/auth.scss';
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.sms-login-panel .auth-btn {
|
||||
background: linear-gradient(to right, #fbbf24, #f97316) !important;
|
||||
border: none !important;
|
||||
border-radius: 24rpx !important;
|
||||
height: 100rpx !important;
|
||||
font-size: 34rpx !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 8rpx !important;
|
||||
box-shadow: 0 12rpx 40rpx rgba(249, 115, 22, 0.25) !important;
|
||||
}
|
||||
</style>
|
||||
139
src/pages-core/auth/components/username-login-panel.vue
Normal file
139
src/pages-core/auth/components/username-login-panel.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<view class="username-login-panel">
|
||||
<AuthInput
|
||||
v-model="form.username"
|
||||
placeholder="请输入用户名"
|
||||
icon="user"
|
||||
/>
|
||||
<AuthInput
|
||||
v-model="form.password"
|
||||
placeholder="请输入密码"
|
||||
icon="lock-on"
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<view v-if="captchaEnabled">
|
||||
<Verify
|
||||
ref="verifyRef"
|
||||
:captcha-type="captchaType"
|
||||
explain="向右滑动完成验证"
|
||||
:img-size="{ width: '300px', height: '150px' }"
|
||||
mode="pop"
|
||||
@success="handleVerifySuccess"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="auth-links">
|
||||
<text class="auth-link" @click="$emit('forget-password')">
|
||||
忘记密码?
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="auth-btn-wrapper">
|
||||
<wd-button block :loading="loading" custom-class="auth-btn" @click="handleSubmit">
|
||||
登 录
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { redirectAfterLogin } from '@/utils'
|
||||
import AuthInput from './auth-input.vue'
|
||||
import Verify from './verifition/verify.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'UsernameLoginPanel',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
agreed: boolean
|
||||
redirectUrl?: string
|
||||
validateTenant: () => boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'forget-password': []
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const verifyRef = ref()
|
||||
const captchaEnabled = import.meta.env.VITE_APP_CAPTCHA_ENABLE === 'true'
|
||||
const captchaType = ref('blockPuzzle')
|
||||
const form = reactive({
|
||||
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
||||
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
||||
captchaVerification: '',
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!props.agreed) {
|
||||
toast.warning('请先阅读并同意用户协议和隐私政策')
|
||||
return
|
||||
}
|
||||
if (!props.validateTenant()) {
|
||||
return
|
||||
}
|
||||
if (!form.username) {
|
||||
toast.warning('请输入用户名')
|
||||
return
|
||||
}
|
||||
if (!form.password) {
|
||||
toast.warning('请输入密码')
|
||||
return
|
||||
}
|
||||
|
||||
if (!captchaEnabled) {
|
||||
await handleVerifySuccess({})
|
||||
return
|
||||
}
|
||||
|
||||
verifyRef.value?.show()
|
||||
}
|
||||
|
||||
async function handleVerifySuccess(params: { captchaVerification?: string }) {
|
||||
loading.value = true
|
||||
try {
|
||||
form.captchaVerification = params?.captchaVerification || ''
|
||||
const tokenStore = useTokenStore()
|
||||
await tokenStore.login({ type: 'username', ...form })
|
||||
redirectAfterLogin(props.redirectUrl)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/auth.scss';
|
||||
|
||||
.auth-links {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
font-size: 26rpx;
|
||||
color: #f97316;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.username-login-panel .auth-btn {
|
||||
background: linear-gradient(to right, #fbbf24, #f97316) !important;
|
||||
border: none !important;
|
||||
border-radius: 24rpx !important;
|
||||
height: 100rpx !important;
|
||||
font-size: 34rpx !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 8rpx !important;
|
||||
box-shadow: 0 12rpx 40rpx rgba(249, 115, 22, 0.25) !important;
|
||||
}
|
||||
</style>
|
||||
140
src/pages-core/auth/components/wechat-login-panel.vue
Normal file
140
src/pages-core/auth/components/wechat-login-panel.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<view>
|
||||
<view v-if="!needPhoneAuth" class="auth-btn-wrapper" style="margin-top: 40rpx">
|
||||
<view class="wechat-btn" @click="handleWechatLogin">
|
||||
<wd-icon name="chat" size="22px" color="#07c160" />
|
||||
<text class="wechat-btn__text">微信一键登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="wx-phone-auth">
|
||||
<view class="wx-phone-auth__tip">
|
||||
<text>检测到当前账号未绑定手机号,请先完成微信手机号授权</text>
|
||||
</view>
|
||||
<view class="auth-btn-wrapper" style="margin-top: 20rpx">
|
||||
<button
|
||||
class="wx-phone-btn"
|
||||
open-type="getPhoneNumber"
|
||||
:loading="loading"
|
||||
@getphonenumber="handleGetPhoneNumber"
|
||||
>
|
||||
微信手机号一键登录
|
||||
</button>
|
||||
</view>
|
||||
<view class="auth-btn-wrapper" style="margin-top: 16rpx">
|
||||
<wd-button block type="text" @click="needPhoneAuth = false">
|
||||
返回重试
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getWxLoginCode } from '@/api/login'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { redirectAfterLogin } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
name: 'WechatLoginPanel',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
agreed: boolean
|
||||
redirectUrl?: string
|
||||
validateTenant: () => boolean
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const needPhoneAuth = ref(false)
|
||||
|
||||
async function handleWechatLogin() {
|
||||
if (!props.agreed) {
|
||||
toast.warning('请先阅读并同意用户协议和隐私政策')
|
||||
return
|
||||
}
|
||||
if (!props.validateTenant()) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const code = await getWxLoginCode()
|
||||
const state = Date.now().toString()
|
||||
const tokenStore = useTokenStore()
|
||||
await tokenStore.socialLogin({ type: 34, code, state })
|
||||
redirectAfterLogin(props.redirectUrl)
|
||||
} catch (error: any) {
|
||||
if (error?.code === 1002000005) {
|
||||
needPhoneAuth.value = true
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetPhoneNumber(e: any) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
toast.warning('未获取到手机号授权,无法继续登录')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const phoneCode = e.detail.code
|
||||
const loginCode = await getWxLoginCode()
|
||||
const state = Date.now().toString()
|
||||
const tokenStore = useTokenStore()
|
||||
await tokenStore.weixinMiniAppLogin({ phoneCode, loginCode, state })
|
||||
redirectAfterLogin(props.redirectUrl)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/auth.scss';
|
||||
|
||||
.wechat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
background: #d6f5e0;
|
||||
border: 1rpx solid #b8edca;
|
||||
border-radius: 24rpx;
|
||||
height: 100rpx;
|
||||
width: 100%;
|
||||
|
||||
&__text {
|
||||
font-size: 30rpx;
|
||||
color: #07c160;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.wx-phone-auth__tip {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #9c9b99;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.wx-phone-btn {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
background: linear-gradient(to right, #07c160, #06ad56);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 24rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(7, 193, 96, 0.25);
|
||||
}
|
||||
</style>
|
||||
@@ -3,116 +3,50 @@
|
||||
<Header />
|
||||
|
||||
<view class="form-container">
|
||||
<!-- Tab 切换器 -->
|
||||
<view class="tab-switcher">
|
||||
<view
|
||||
class="tab-item" :class="[activeTab === 'sms' ? 'tab-item--active' : '']"
|
||||
@click="activeTab = 'sms'"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
class="tab-item"
|
||||
:class="[activeTab === tab.value ? 'tab-item--active' : '']"
|
||||
@click="activeTab = tab.value"
|
||||
>
|
||||
快捷登录
|
||||
</view>
|
||||
<view
|
||||
class="tab-item" :class="[activeTab === 'username' ? 'tab-item--active' : '']"
|
||||
@click="activeTab = 'username'"
|
||||
>
|
||||
账号登录
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 共享:租户选择器 -->
|
||||
<TenantPicker ref="tenantPickerRef" />
|
||||
|
||||
<!-- 快捷登录面板 -->
|
||||
<view v-show="activeTab === 'sms'">
|
||||
<AuthInput
|
||||
v-model="smsForm.mobile"
|
||||
placeholder="请输入手机号"
|
||||
icon="phone"
|
||||
type="number"
|
||||
:maxlength="11"
|
||||
<view v-if="isMpWeixin && activeTab === 'wechat'" class="login-panel login-panel--wechat">
|
||||
<WechatLoginPanel
|
||||
:agreed="agreed"
|
||||
:redirect-url="redirectUrl"
|
||||
:validate-tenant="validateTenant"
|
||||
/>
|
||||
<CodeInput
|
||||
v-model="smsForm.code"
|
||||
:mobile="smsForm.mobile"
|
||||
:scene="21"
|
||||
:before-send="validateBeforeSend"
|
||||
/>
|
||||
|
||||
<view v-if="captchaEnabled">
|
||||
<Verify
|
||||
ref="smsVerifyRef"
|
||||
:captcha-type="captchaType"
|
||||
explain="向右滑动完成验证"
|
||||
:img-size="{ width: '300px', height: '150px' }"
|
||||
mode="pop"
|
||||
@success="smsVerifySuccess"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="auth-btn-wrapper" style="margin-top: 40rpx">
|
||||
<wd-button block :loading="smsLoading" custom-class="auth-btn" @click="handleSmsLogin">
|
||||
登 录
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- "或"分割线 -->
|
||||
<view class="social-divider">
|
||||
<view class="social-divider__line" />
|
||||
<text class="social-divider__text">或</text>
|
||||
<view class="social-divider__line" />
|
||||
</view>
|
||||
|
||||
<!-- 微信授权登录按钮 -->
|
||||
<view class="wechat-btn" @click="handleWechatLogin">
|
||||
<wd-icon name="chat" size="22px" color="#07c160" />
|
||||
<text class="wechat-btn__text">微信授权登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 账号登录面板 -->
|
||||
<view v-show="activeTab === 'username'">
|
||||
<AuthInput
|
||||
v-model="usernameForm.username"
|
||||
placeholder="请输入用户名"
|
||||
icon="user"
|
||||
<view v-if="!isMpWeixin && activeTab === 'sms'" class="login-panel login-panel--sms">
|
||||
<SmsLoginPanel
|
||||
:agreed="agreed"
|
||||
:redirect-url="redirectUrl"
|
||||
:validate-tenant="validateTenant"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'username'" class="login-panel login-panel--username">
|
||||
<UsernameLoginPanel
|
||||
:agreed="agreed"
|
||||
:redirect-url="redirectUrl"
|
||||
:validate-tenant="validateTenant"
|
||||
@forget-password="goToForgetPassword"
|
||||
/>
|
||||
<AuthInput
|
||||
v-model="usernameForm.password"
|
||||
placeholder="请输入密码"
|
||||
icon="lock-on"
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<view v-if="captchaEnabled">
|
||||
<Verify
|
||||
ref="usernameVerifyRef"
|
||||
:captcha-type="captchaType"
|
||||
explain="向右滑动完成验证"
|
||||
:img-size="{ width: '300px', height: '150px' }"
|
||||
mode="pop"
|
||||
@success="usernameVerifySuccess"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="auth-links">
|
||||
<text class="auth-link" @click="goToForgetPassword">
|
||||
忘记密码?
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="auth-btn-wrapper">
|
||||
<wd-button block :loading="usernameLoading" custom-class="auth-btn" @click="handleUsernameLogin">
|
||||
登 录
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部协议区 -->
|
||||
<view class="agreement">
|
||||
<view class="agreement__checkbox" :class="[agreed ? 'agreement__checkbox--checked' : '']" @click="agreed = !agreed">
|
||||
<wd-icon v-if="agreed" name="check-bold" size="18rpx" color="#fff" />
|
||||
</view>
|
||||
<text class="agreement__text">登录即表示同意</text>
|
||||
<text class="agreement__text">登录即代表同意</text>
|
||||
<text class="agreement__link" @click="goToAgreement">用户协议</text>
|
||||
<text class="agreement__text">和</text>
|
||||
<text class="agreement__link" @click="goToPrivacy">隐私政策</text>
|
||||
@@ -122,17 +56,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { isMpWeixin } from '@uni-helper/uni-env'
|
||||
import { computed, ref } from 'vue'
|
||||
import { FORGET_PASSWORD_PAGE } from '@/router/config'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { ensureDecodeURIComponent, redirectAfterLogin } from '@/utils'
|
||||
import { isMobile } from '@/utils/validator'
|
||||
import AuthInput from './components/auth-input.vue'
|
||||
import CodeInput from './components/code-input.vue'
|
||||
import { ensureDecodeURIComponent } from '@/utils'
|
||||
import Header from './components/header.vue'
|
||||
import SmsLoginPanel from './components/sms-login-panel.vue'
|
||||
import TenantPicker from './components/tenant-picker.vue'
|
||||
import Verify from './components/verifition/verify.vue'
|
||||
import UsernameLoginPanel from './components/username-login-panel.vue'
|
||||
import WechatLoginPanel from './components/wechat-login-panel.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'LoginPage',
|
||||
@@ -144,32 +76,24 @@ definePage({
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
type LoginTab = 'wechat' | 'sms' | 'username'
|
||||
|
||||
const redirectUrl = ref<string>()
|
||||
const tenantPickerRef = ref<InstanceType<typeof TenantPicker>>()
|
||||
const captchaEnabled = import.meta.env.VITE_APP_CAPTCHA_ENABLE === 'true'
|
||||
const captchaType = ref('blockPuzzle')
|
||||
const agreed = ref(false)
|
||||
const activeTab = ref<LoginTab>(isMpWeixin ? 'wechat' : 'sms')
|
||||
|
||||
// Tab 状态
|
||||
const activeTab = ref<'sms' | 'username'>('sms')
|
||||
|
||||
// === 快捷登录(短信验证码) ===
|
||||
const smsLoading = ref(false)
|
||||
const smsVerifyRef = ref()
|
||||
const smsForm = reactive({
|
||||
mobile: '',
|
||||
code: '',
|
||||
captchaVerification: '',
|
||||
})
|
||||
|
||||
// === 账号登录 ===
|
||||
const usernameLoading = ref(false)
|
||||
const usernameVerifyRef = ref()
|
||||
const usernameForm = reactive({
|
||||
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
||||
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
||||
captchaVerification: '',
|
||||
const tabs = computed(() => {
|
||||
if (isMpWeixin) {
|
||||
return [
|
||||
{ label: '微信登录', value: 'wechat' as const },
|
||||
{ label: '账号登录', value: 'username' as const },
|
||||
]
|
||||
}
|
||||
return [
|
||||
{ label: '快捷登录', value: 'sms' as const },
|
||||
{ label: '账号登录', value: 'username' as const },
|
||||
]
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
@@ -178,85 +102,14 @@ onLoad((options) => {
|
||||
}
|
||||
})
|
||||
|
||||
function validateBeforeSend(): boolean {
|
||||
function validateTenant(): boolean {
|
||||
return tenantPickerRef.value?.validate() ?? false
|
||||
}
|
||||
|
||||
// --- 快捷登录逻辑 ---
|
||||
async function handleSmsLogin() {
|
||||
if (!tenantPickerRef.value?.validate())
|
||||
return
|
||||
if (!smsForm.mobile) {
|
||||
toast.warning('请输入手机号')
|
||||
return
|
||||
}
|
||||
if (!isMobile(smsForm.mobile)) {
|
||||
toast.warning('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
if (!smsForm.code) {
|
||||
toast.warning('请输入验证码')
|
||||
return
|
||||
}
|
||||
if (!captchaEnabled) {
|
||||
await smsVerifySuccess({})
|
||||
} else {
|
||||
smsVerifyRef.value.show()
|
||||
}
|
||||
}
|
||||
|
||||
async function smsVerifySuccess(params: any) {
|
||||
smsLoading.value = true
|
||||
try {
|
||||
const tokenStore = useTokenStore()
|
||||
smsForm.captchaVerification = params.captchaVerification
|
||||
await tokenStore.login({ type: 'sms', ...smsForm })
|
||||
redirectAfterLogin(redirectUrl.value)
|
||||
} finally {
|
||||
smsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- 账号登录逻辑 ---
|
||||
async function handleUsernameLogin() {
|
||||
if (!tenantPickerRef.value?.validate())
|
||||
return
|
||||
if (!usernameForm.username) {
|
||||
toast.warning('请输入用户名')
|
||||
return
|
||||
}
|
||||
if (!usernameForm.password) {
|
||||
toast.warning('请输入密码')
|
||||
return
|
||||
}
|
||||
if (!captchaEnabled) {
|
||||
await usernameVerifySuccess({})
|
||||
} else {
|
||||
usernameVerifyRef.value.show()
|
||||
}
|
||||
}
|
||||
|
||||
async function usernameVerifySuccess(params: any) {
|
||||
usernameLoading.value = true
|
||||
try {
|
||||
const tokenStore = useTokenStore()
|
||||
usernameForm.captchaVerification = params.captchaVerification
|
||||
await tokenStore.login({ type: 'username', ...usernameForm })
|
||||
redirectAfterLogin(redirectUrl.value)
|
||||
} finally {
|
||||
usernameLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- 导航 ---
|
||||
function goToForgetPassword() {
|
||||
uni.navigateTo({ url: FORGET_PASSWORD_PAGE })
|
||||
}
|
||||
|
||||
function handleWechatLogin() {
|
||||
toast.info('微信登录功能开发中')
|
||||
}
|
||||
|
||||
function goToAgreement() {
|
||||
uni.navigateTo({ url: '/pages-core/user/settings/agreement/index' })
|
||||
}
|
||||
@@ -268,4 +121,18 @@ function goToPrivacy() {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './styles/auth.scss';
|
||||
|
||||
.login-panel--sms,
|
||||
.login-panel--username {
|
||||
:deep(.auth-btn) {
|
||||
background: linear-gradient(to right, #fbbf24, #f97316) !important;
|
||||
border: none !important;
|
||||
border-radius: 24rpx !important;
|
||||
height: 100rpx !important;
|
||||
font-size: 34rpx !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 8rpx !important;
|
||||
box-shadow: 0 12rpx 40rpx rgba(249, 115, 22, 0.25) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -211,6 +211,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 微信手机号授权区域
|
||||
.wx-phone-auth {
|
||||
&__tip {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #9c9b99;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 微信手机号授权按钮(原生 button)
|
||||
.wx-phone-btn {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
background: linear-gradient(to right, #07c160, #06ad56);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 24rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(7, 193, 96, 0.25);
|
||||
}
|
||||
|
||||
// 底部协议区
|
||||
.agreement {
|
||||
display: flex;
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
AuthLoginReqVO,
|
||||
AuthRegisterReqVO,
|
||||
AuthSmsLoginReqVO,
|
||||
AuthSocialLoginReqVO,
|
||||
AuthWeixinMiniAppLoginReqVO,
|
||||
ILoginForm,
|
||||
} from '@/api/login'
|
||||
import type { IAuthLoginRes } from '@/api/types/login'
|
||||
@@ -13,8 +15,8 @@ import {
|
||||
login as _login,
|
||||
logout as _logout,
|
||||
refreshToken as _refreshToken,
|
||||
wxLogin as _wxLogin,
|
||||
getWxCode,
|
||||
socialLogin as _socialLogin,
|
||||
weixinMiniAppLogin as _weixinMiniAppLogin,
|
||||
register,
|
||||
smsLogin,
|
||||
} from '@/api/login'
|
||||
@@ -160,38 +162,20 @@ export const useTokenStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
* 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
|
||||
* (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
|
||||
* @returns 登录结果
|
||||
*/
|
||||
const wxLogin = async () => {
|
||||
try {
|
||||
// 获取微信小程序登录的code
|
||||
const code = await getWxCode()
|
||||
console.log('微信登录-code: ', code)
|
||||
const res = await _wxLogin(code)
|
||||
console.log('微信登录-res: ', res)
|
||||
await _postLogin(res)
|
||||
// 注释 by 芋艿:使用 wd-toast 替代
|
||||
// uni.showToast({
|
||||
// title: '登录成功',
|
||||
// icon: 'success',
|
||||
// })
|
||||
toast.success('登录成功')
|
||||
return res
|
||||
}
|
||||
catch (error) {
|
||||
console.error('微信登录失败:', error)
|
||||
// 注释 by 芋艿:使用 wd-toast 替代
|
||||
// uni.showToast({
|
||||
// title: '微信登录失败,请重试',
|
||||
// icon: 'error',
|
||||
// })
|
||||
toast.error('微信登录失败,请重试')
|
||||
throw error
|
||||
}
|
||||
/** 微信静默登录(已绑定用户) */
|
||||
const socialLogin = async (params: AuthSocialLoginReqVO) => {
|
||||
const res = await _socialLogin(params)
|
||||
await _postLogin(res)
|
||||
toast.success('登录成功')
|
||||
return res
|
||||
}
|
||||
|
||||
/** 微信小程序一键登录(手机号匹配) */
|
||||
const weixinMiniAppLogin = async (params: AuthWeixinMiniAppLoginReqVO) => {
|
||||
const res = await _weixinMiniAppLogin(params)
|
||||
await _postLogin(res)
|
||||
toast.success('登录成功')
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,7 +302,8 @@ export const useTokenStore = defineStore(
|
||||
return {
|
||||
// 核心API方法
|
||||
login,
|
||||
wxLogin,
|
||||
socialLogin,
|
||||
weixinMiniAppLogin,
|
||||
logout,
|
||||
|
||||
// 认证状态判断(最常用的)
|
||||
|
||||
@@ -124,8 +124,8 @@ export function getEnvBaseUrl() {
|
||||
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
|
||||
// TODO @芋艿:这个后续也要调整。
|
||||
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'http://192.168.0.104:48080/admin-api'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'http://192.168.0.104:48080/admin-api'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'http://192.168.0.104:48080/admin-api'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://viewshanghai.com/admin-api'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://viewshanghai.com/admin-api'
|
||||
|
||||
// 微信小程序端环境区分
|
||||
if (isMpWeixin) {
|
||||
|
||||
Reference in New Issue
Block a user