|
|
@@ -2,7 +2,7 @@
|
|
|
<view class="safe-page login">
|
|
|
<view class="hero">
|
|
|
<view class="brand">订餐小程序</view>
|
|
|
- <view class="subtitle">授权本机号码,一键登录订餐服务</view>
|
|
|
+ <view class="subtitle">登录后享受订餐服务</view>
|
|
|
</view>
|
|
|
|
|
|
<view class="card box">
|
|
|
@@ -12,8 +12,28 @@
|
|
|
</button>
|
|
|
<!-- #endif -->
|
|
|
|
|
|
- <!-- #ifndef MP-WEIXIN -->
|
|
|
- <view class="tip-box">一键登录仅支持微信小程序,请在微信中打开本小程序。</view>
|
|
|
+ <!-- #ifdef H5 -->
|
|
|
+ <block v-if="inWechat && !showMobileForm">
|
|
|
+ <button class="phone-btn" :loading="logging" @click="onWechatAuthLogin">微信授权登录</button>
|
|
|
+ <view class="switch-link" @click="showMobileForm = true">使用手机号验证码登录</view>
|
|
|
+ </block>
|
|
|
+ <block v-else>
|
|
|
+ <view class="form-item">
|
|
|
+ <input v-model="mobile" type="number" maxlength="11" placeholder="请输入手机号" class="form-input" />
|
|
|
+ </view>
|
|
|
+ <view class="form-item code-row">
|
|
|
+ <input v-model="smsCode" type="number" maxlength="6" placeholder="请输入验证码" class="form-input" />
|
|
|
+ <view class="code-btn" :class="{ disabled: countdown > 0 }" @click="onSendSmsCode">
|
|
|
+ {{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <button class="phone-btn" :loading="logging" @click="onMobileLogin">登录</button>
|
|
|
+ <view v-if="inWechat" class="switch-link" @click="showMobileForm = false">返回微信授权登录</view>
|
|
|
+ </block>
|
|
|
+ <!-- #endif -->
|
|
|
+
|
|
|
+ <!-- #ifndef MP-WEIXIN || H5 -->
|
|
|
+ <view class="tip-box">当前环境暂不支持登录,请在微信小程序或H5中打开。</view>
|
|
|
<!-- #endif -->
|
|
|
|
|
|
<view class="agreement-section">
|
|
|
@@ -35,25 +55,81 @@
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
|
-import { ref } from 'vue'
|
|
|
-import { loginByWechatMiniPhone } from '@/api/modules/memberAuth'
|
|
|
+import { ref, onUnmounted } from 'vue'
|
|
|
+import { loginByWechatMiniPhone, loginByMobile, loginByWechatMp, sendSmsCode, type MemberLoginResultVO } from '@/api/modules/memberAuth'
|
|
|
import { useAuthStore } from '@/store/modules/auth'
|
|
|
import { isTabBarPage } from '@/utils/auth'
|
|
|
import { removeSsoToken } from '@/utils/tokenStorage'
|
|
|
+import { isWechatBrowser, buildWechatOAuthUrl, getWechatOAuthCode } from '@/utils/wechatOAuth'
|
|
|
|
|
|
const auth = useAuthStore()
|
|
|
const redirect = ref('')
|
|
|
const agreed = ref(false)
|
|
|
const logging = ref(false)
|
|
|
+const mobile = ref('')
|
|
|
+const smsCode = ref('')
|
|
|
+const countdown = ref(0)
|
|
|
+const showMobileForm = ref(false)
|
|
|
+const inWechat = ref(false)
|
|
|
+let countdownTimer: ReturnType<typeof setInterval> | null = null
|
|
|
+
|
|
|
+// #ifdef H5
|
|
|
+inWechat.value = isWechatBrowser()
|
|
|
+// #endif
|
|
|
|
|
|
onLoad(query => {
|
|
|
redirect.value = decodeURIComponent(String(query?.redirect || ''))
|
|
|
+ // #ifdef H5
|
|
|
+ if (inWechat.value) {
|
|
|
+ const code = getWechatOAuthCode()
|
|
|
+ if (code) doWechatMpLogin(code)
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (countdownTimer) {
|
|
|
+ clearInterval(countdownTimer)
|
|
|
+ countdownTimer = null
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
function openAgreement(type: 'service' | 'privacy') {
|
|
|
uni.showToast({ title: type === 'service' ? '用户协议页面待配置' : '隐私政策页面待配置', icon: 'none' })
|
|
|
}
|
|
|
|
|
|
+function applyLoginResult(data: MemberLoginResultVO) {
|
|
|
+ auth.setToken(data.accessToken)
|
|
|
+ if (data.refreshToken) {
|
|
|
+ auth.setSsoToken(data.refreshToken)
|
|
|
+ } else {
|
|
|
+ removeSsoToken()
|
|
|
+ }
|
|
|
+ auth.setUser({
|
|
|
+ id: data.memberId,
|
|
|
+ nickname: (data.nickname || '会员').trim(),
|
|
|
+ loginName: data.memberNo?.trim(),
|
|
|
+ avatar: data.avatar || '',
|
|
|
+ phone: (data.mobile || '').trim()
|
|
|
+ })
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '登录成功', icon: 'none' })
|
|
|
+ setTimeout(goAfterLogin, 300)
|
|
|
+}
|
|
|
+
|
|
|
+/** 清理 H5 地址栏上的微信授权 code,避免刷新重复登录 */
|
|
|
+function cleanOAuthCodeFromUrl() {
|
|
|
+ // #ifdef H5
|
|
|
+ if (window.location.search) {
|
|
|
+ const url = new URL(window.location.href)
|
|
|
+ url.searchParams.delete('code')
|
|
|
+ url.searchParams.delete('state')
|
|
|
+ window.history.replaceState({}, '', url.toString())
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+}
|
|
|
+
|
|
|
+// #ifdef MP-WEIXIN
|
|
|
async function onGetPhoneNumber(e: { detail?: { errMsg?: string; code?: string } }) {
|
|
|
if (logging.value) return
|
|
|
if (!agreed.value) {
|
|
|
@@ -81,24 +157,98 @@ async function onGetPhoneNumber(e: { detail?: { errMsg?: string; code?: string }
|
|
|
if (!jsCode) throw new Error('获取微信登录凭证失败')
|
|
|
|
|
|
const res = await loginByWechatMiniPhone({ code: jsCode, phoneCode })
|
|
|
- const data = res.data
|
|
|
- auth.setToken(data.accessToken)
|
|
|
- if (data.refreshToken) {
|
|
|
- auth.setSsoToken(data.refreshToken)
|
|
|
- } else {
|
|
|
- removeSsoToken()
|
|
|
- }
|
|
|
- auth.setUser({
|
|
|
- id: data.memberId,
|
|
|
- nickname: (data.nickname || '会员').trim(),
|
|
|
- loginName: data.memberNo?.trim(),
|
|
|
- avatar: data.avatar || '',
|
|
|
- phone: (data.mobile || '').trim()
|
|
|
- })
|
|
|
+ applyLoginResult(res.data)
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: (error as Error).message || '登录失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ logging.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+// #endif
|
|
|
|
|
|
+// #ifdef H5
|
|
|
+function onWechatAuthLogin() {
|
|
|
+ if (logging.value) return
|
|
|
+ if (!agreed.value) {
|
|
|
+ uni.showToast({ title: '请先阅读并同意用户协议与隐私政策', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const oauthUrl = buildWechatOAuthUrl('meal_login')
|
|
|
+ window.location.href = oauthUrl
|
|
|
+ } catch (error) {
|
|
|
+ uni.showToast({ title: (error as Error).message || '微信授权登录配置异常', icon: 'none' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function doWechatMpLogin(code: string) {
|
|
|
+ if (logging.value) return
|
|
|
+ logging.value = true
|
|
|
+ uni.showLoading({ title: '登录中...', mask: true })
|
|
|
+ try {
|
|
|
+ const res = await loginByWechatMp({ code })
|
|
|
+ if (res.data.needBindMobile) {
|
|
|
+ uni.hideLoading()
|
|
|
+ showMobileForm.value = true
|
|
|
+ uni.showToast({ title: '该微信账号未绑定手机号,请使用手机号登录', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ applyLoginResult(res.data)
|
|
|
+ } catch (error) {
|
|
|
uni.hideLoading()
|
|
|
- uni.showToast({ title: '登录成功', icon: 'none' })
|
|
|
- setTimeout(goAfterLogin, 300)
|
|
|
+ uni.showToast({ title: (error as Error).message || '微信登录失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ cleanOAuthCodeFromUrl()
|
|
|
+ logging.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function onSendSmsCode() {
|
|
|
+ if (countdown.value > 0) return
|
|
|
+ if (!/^1[3-9]\d{9}$/.test(mobile.value)) {
|
|
|
+ uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await sendSmsCode({ mobile: mobile.value, scene: 'LOGIN' })
|
|
|
+ uni.showToast({ title: '验证码已发送', icon: 'none' })
|
|
|
+ startCountdown()
|
|
|
+ } catch (error) {
|
|
|
+ uni.showToast({ title: (error as Error).message || '验证码发送失败', icon: 'none' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function startCountdown() {
|
|
|
+ countdown.value = 60
|
|
|
+ countdownTimer = setInterval(() => {
|
|
|
+ countdown.value--
|
|
|
+ if (countdown.value <= 0 && countdownTimer) {
|
|
|
+ clearInterval(countdownTimer)
|
|
|
+ countdownTimer = null
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+}
|
|
|
+
|
|
|
+async function onMobileLogin() {
|
|
|
+ if (logging.value) return
|
|
|
+ if (!agreed.value) {
|
|
|
+ uni.showToast({ title: '请先阅读并同意用户协议与隐私政策', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!/^1[3-9]\d{9}$/.test(mobile.value)) {
|
|
|
+ uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!smsCode.value) {
|
|
|
+ uni.showToast({ title: '请输入验证码', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ logging.value = true
|
|
|
+ uni.showLoading({ title: '登录中...', mask: true })
|
|
|
+ try {
|
|
|
+ const res = await loginByMobile({ mobile: mobile.value, smsCode: smsCode.value, platform: 'H5' })
|
|
|
+ applyLoginResult(res.data)
|
|
|
} catch (error) {
|
|
|
uni.hideLoading()
|
|
|
uni.showToast({ title: (error as Error).message || '登录失败', icon: 'none' })
|
|
|
@@ -106,6 +256,7 @@ async function onGetPhoneNumber(e: { detail?: { errMsg?: string; code?: string }
|
|
|
logging.value = false
|
|
|
}
|
|
|
}
|
|
|
+// #endif
|
|
|
|
|
|
function goAfterLogin() {
|
|
|
const fallbackUrl = '/pages/meal/index'
|
|
|
@@ -175,6 +326,48 @@ function goAfterLogin() {
|
|
|
font-size: 26rpx;
|
|
|
line-height: 1.5;
|
|
|
}
|
|
|
+.form-item {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+.form-input {
|
|
|
+ width: 100%;
|
|
|
+ height: 88rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ background: #f3f6f8;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #16343a;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+.code-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+.code-row .form-input {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.code-btn {
|
|
|
+ flex-shrink: 0;
|
|
|
+ height: 88rpx;
|
|
|
+ line-height: 88rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ background: #e0f7fa;
|
|
|
+ color: #00838f;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 600;
|
|
|
+
|
|
|
+ &.disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
+}
|
|
|
+.switch-link {
|
|
|
+ margin-top: 28rpx;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #00a9bf;
|
|
|
+}
|
|
|
.agreement-section {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|