Bladeren bron

实现手机密码登录

Zhangbw 2 maanden geleden
bovenliggende
commit
6b891131b4
3 gewijzigde bestanden met toevoegingen van 224 en 162 verwijderingen
  1. 193 76
      src/pages/login/login.vue
  2. 13 0
      src/utils/api.js
  3. 18 86
      src/utils/auth.js

+ 193 - 76
src/pages/login/login.vue

@@ -18,33 +18,45 @@
       <text class="subtitle">专业的市场量化分析工具</text>
     </view>
 
-    <!-- 登录按钮区域 -->
+    <!-- 登录注册区域 -->
     <view class="login-actions">
-      <!-- 微信授权登录按钮 -->
-      <button
-        v-if="!showPhoneAuth"
-        class="login-btn primary-btn"
-        @click="handleWxAuth"
-      >
-        <text class="btn-text">微信授权登录</text>
-      </button>
-
-      <!-- 手机号输入表单(新用户第二步) -->
-      <view v-else class="phone-form">
+      <!-- 初始状态:显示登录和注册按钮 -->
+      <view v-if="currentStep === 'initial'" class="button-group">
+        <button class="login-btn primary-btn" @click="handleLoginClick">
+          <text class="btn-text">登录</text>
+        </button>
+        <button class="login-btn secondary-btn" @click="handleRegisterClick">
+          <text class="btn-text">注册</text>
+        </button>
+      </view>
+
+      <!-- 输入手机号密码表单 -->
+      <view v-else-if="currentStep === 'input'" class="form-container">
+        <view class="form-title">{{ isLogin ? '登录账号' : '注册账号' }}</view>
         <input
           v-model="phoneNumber"
           type="number"
           placeholder="请输入手机号"
           maxlength="11"
-          class="phone-input"
+          class="form-input"
+        />
+        <input
+          v-model="password"
+          type="text"
+          :password="true"
+          placeholder="请输入密码"
+          class="form-input"
         />
-        <button class="login-btn primary-btn" @click="handlePhoneSubmit">
-          <text class="btn-text">完成注册</text>
+        <button class="login-btn primary-btn" @click="handleSubmit">
+          <text class="btn-text">{{ isLogin ? '登录' : '注册' }}</text>
+        </button>
+        <button class="login-btn cancel-btn" @click="handleCancel">
+          <text class="btn-text">取消</text>
         </button>
       </view>
 
-      <!-- 用户协议 -->
-      <view class="agreement">
+      <!-- 用户协议 - 只在初始状态显示 -->
+      <view v-if="currentStep === 'initial'" class="agreement">
         <checkbox-group @change="handleAgreementChange">
           <label class="agreement-label">
             <checkbox :checked="agreedToTerms" color="#5d55e8" />
@@ -62,19 +74,30 @@
 </template>
 
 <script>
-import { startH5Auth, handleH5AuthCallback } from '@/utils/auth.js'
+import { startH5Auth, handleH5AuthCallback, setToken } from '@/utils/auth.js'
+import { checkUserStatus, h5PhoneLogin } from '@/utils/api.js'
 
 export default {
   data() {
     return {
-      showPhoneAuth: false,
+      currentStep: 'initial', // 'initial' | 'input'
+      isLogin: true, // true=登录, false=注册
       agreedToTerms: false,
-      tempUserData: null,
-      phoneNumber: ''
+      showAgreement: false, // 是否显示协议选择框
+      phoneNumber: '',
+      password: '',
+      tempWxData: null // 临时存储微信授权数据
     }
   },
 
   onLoad(options) {
+    // 恢复登录/注册状态
+    const savedIsLogin = uni.getStorageSync('temp_isLogin')
+    if (savedIsLogin !== null && savedIsLogin !== undefined && savedIsLogin !== '') {
+      this.isLogin = savedIsLogin === 'true'
+      uni.removeStorageSync('temp_isLogin')
+    }
+
     // 检查URL中是否有微信授权回调的code参数
     let code = options && options.code
 
@@ -111,7 +134,7 @@ export default {
   },
 
   methods: {
-    async handleWxAuth() {
+    async handleLoginClick() {
       if (!this.agreedToTerms) {
         uni.showToast({
           title: '请先阅读并同意用户协议',
@@ -121,8 +144,11 @@ export default {
         return
       }
 
+      this.isLogin = true
+      uni.setStorageSync('temp_isLogin', 'true')
+
       try {
-        uni.showLoading({ title: '跳转中...' })
+        uni.showLoading({ title: '授权中...' })
         await startH5Auth()
         uni.hideLoading()
       } catch (error) {
@@ -136,37 +162,98 @@ export default {
       }
     },
 
+    async handleRegisterClick() {
+      if (!this.agreedToTerms) {
+        uni.showToast({
+          title: '请先阅读并同意用户协议',
+          icon: 'none',
+          duration: 2000
+        })
+        return
+      }
+
+      this.isLogin = false
+      uni.setStorageSync('temp_isLogin', 'false')
+
+      try {
+        uni.showLoading({ title: '授权中...' })
+        await startH5Auth()
+        uni.hideLoading()
+      } catch (error) {
+        uni.hideLoading()
+        console.error('[注册] 微信授权失败:', error)
+        uni.showToast({
+          title: error.message || '授权失败,请重试',
+          icon: 'none',
+          duration: 2000
+        })
+      }
+    },
+
     async handleAuthCallback(code) {
       try {
-        uni.showLoading({ title: '登录中...' })
+        uni.showLoading({ title: '处理中...' })
+
+        // 获取微信用户信息
         const result = await handleH5AuthCallback(code)
+
+        // 检查用户是否已注册
+        const statusResult = await checkUserStatus(result.openid)
+        const isRegistered = statusResult.data
+
         uni.hideLoading()
 
-        if (result.isRegistered) {
-          console.log('[登录] 老用户登录成功')
-          this.handleLoginSuccess()
+        // 保存微信数据
+        this.tempWxData = {
+          openid: result.openid,
+          unionid: result.unionid,
+          nickname: result.nickname,
+          avatarUrl: result.avatarUrl
+        }
+
+        // 根据用户状态和操作类型判断
+        if (this.isLogin) {
+          // 登录流程
+          if (isRegistered) {
+            // 已注册,显示输入框
+            this.currentStep = 'input'
+          } else {
+            // 未注册,提示先注册
+            uni.showToast({
+              title: '该微信未注册,请先注册',
+              icon: 'none',
+              duration: 2000
+            })
+            this.currentStep = 'initial'
+          }
         } else {
-          console.log('[登录] 新用户,需要手机号授权')
-          this.tempUserData = {
-            openid: result.openid,
-            unionid: result.unionid,
-            nickname: result.nickname,
-            avatarUrl: result.avatarUrl
+          // 注册流程
+          if (!isRegistered) {
+            // 未注册,显示输入框
+            this.currentStep = 'input'
+          } else {
+            // 已注册,提示直接登录
+            uni.showToast({
+              title: '该微信已注册,请直接登录',
+              icon: 'none',
+              duration: 2000
+            })
+            this.currentStep = 'initial'
           }
-          this.showPhoneAuth = true
         }
       } catch (error) {
         uni.hideLoading()
-        console.error('[登录] 授权回调处理失败:', error)
+        console.error('[授权] 处理失败:', error)
         uni.showToast({
-          title: error.message || '登录失败,请重试',
+          title: error.message || '处理失败,请重试',
           icon: 'none',
           duration: 2000
         })
+        this.currentStep = 'initial'
       }
     },
 
-    async handlePhoneSubmit() {
+    async handleSubmit() {
       if (!this.phoneNumber) {
         uni.showToast({
           title: '请输入手机号',
@@ -185,46 +272,58 @@ export default {
         return
       }
 
+      if (!this.password) {
+        uni.showToast({
+          title: '请输入密码',
+          icon: 'none',
+          duration: 2000
+        })
+        return
+      }
+
       try {
-        uni.showLoading({ title: '注册中...' })
+        uni.showLoading({ title: this.isLogin ? '登录中...' : '注册中...' })
 
-        const { h5PhoneLogin } = await import('@/utils/api.js')
-        const result = await h5PhoneLogin({
-          openid: this.tempUserData.openid,
-          unionid: this.tempUserData.unionid,
+        const params = {
+          openid: this.tempWxData.openid,
+          unionid: this.tempWxData.unionid,
           phone: this.phoneNumber,
-          nickname: this.tempUserData.nickname,
-          avatarUrl: this.tempUserData.avatarUrl
-        })
+          password: this.password
+        }
+
+        // 注册时需要传递昵称和头像
+        if (!this.isLogin) {
+          params.nickname = this.tempWxData.nickname
+          params.avatarUrl = this.tempWxData.avatarUrl
+        }
+
+        const result = await h5PhoneLogin(params)
 
         uni.hideLoading()
 
         if (result.code === 200 && result.data.token) {
-          const { setToken } = await import('@/utils/auth.js')
           setToken(result.data.token)
-          console.log('[登录] 注册成功')
+          console.log('[登录] 成功')
           this.handleLoginSuccess()
         } else {
-          throw new Error(result.message || '注册失败')
+          throw new Error(result.message || (this.isLogin ? '登录失败' : '注册失败'))
         }
       } catch (error) {
         uni.hideLoading()
-        console.error('[登录] 注册失败:', error)
+        console.error('[提交] 失败:', error)
         uni.showToast({
-          title: error.message || '注册失败,请重试',
+          title: error.message || (this.isLogin ? '登录失败,请重试' : '注册失败,请重试'),
           icon: 'none',
           duration: 2000
         })
       }
     },
 
-    handlePhoneAuth() {
-      // 已废弃,保留以防万一
-      uni.showToast({
-        title: 'H5环境暂不支持手机号快速验证,请联系管理员',
-        icon: 'none',
-        duration: 3000
-      })
+    handleCancel() {
+      this.currentStep = 'initial'
+      this.phoneNumber = ''
+      this.password = ''
+      this.tempWxData = null
     },
 
     handleLoginSuccess() {
@@ -374,6 +473,10 @@ export default {
   z-index: 1;
 }
 
+.button-group {
+  width: 100%;
+}
+
 .login-btn {
   width: 100%;
   height: 96rpx;
@@ -383,7 +486,7 @@ export default {
   justify-content: center;
   font-size: 32rpx;
   font-weight: 500;
-  margin-bottom: 40rpx;
+  margin-bottom: 24rpx;
   border: none;
   box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
 }
@@ -393,15 +496,44 @@ export default {
   color: #667eea;
 }
 
-.btn-icon {
-  font-size: 40rpx;
-  margin-right: 16rpx;
+.secondary-btn {
+  background: rgba(255, 255, 255, 0.2);
+  color: #fff;
+  border: 2rpx solid #fff;
+}
+
+.cancel-btn {
+  background: rgba(255, 255, 255, 0.1);
+  color: #fff;
 }
 
 .btn-text {
   font-size: 32rpx;
 }
 
+.form-container {
+  width: 100%;
+}
+
+.form-title {
+  font-size: 36rpx;
+  color: #fff;
+  font-weight: bold;
+  margin-bottom: 40rpx;
+  text-align: center;
+}
+
+.form-input {
+  width: 100%;
+  height: 96rpx;
+  border-radius: 48rpx;
+  background: #fff;
+  padding: 0 40rpx;
+  font-size: 32rpx;
+  margin-bottom: 24rpx;
+  box-sizing: border-box;
+}
+
 .agreement {
   margin-top: 60rpx;
 }
@@ -422,19 +554,4 @@ export default {
   color: #fff;
   text-decoration: underline;
 }
-
-.phone-form {
-  width: 100%;
-}
-
-.phone-input {
-  width: 100%;
-  height: 96rpx;
-  border-radius: 48rpx;
-  background: #fff;
-  padding: 0 40rpx;
-  font-size: 32rpx;
-  margin-bottom: 40rpx;
-  box-sizing: border-box;
-}
 </style>

+ 13 - 0
src/utils/api.js

@@ -169,6 +169,19 @@ export const h5AuthCallback = (code) => {
   })
 }
 
+/**
+ * 检查用户是否已注册
+ * @param {string} openid - 微信openid
+ * @returns {Promise} 返回 { data: boolean }
+ */
+export const checkUserStatus = (openid) => {
+  return request({
+    url: '/v1/auth/h5/check-user',
+    method: 'GET',
+    data: { openid }
+  })
+}
+
 /**
  * H5手机号登录/注册
  * @param {object} params - { openid, code, nickname, avatarUrl, unionid }

+ 18 - 86
src/utils/auth.js

@@ -2,19 +2,20 @@
  * H5公众号认证工具类
  * 用于管理用户登录状态、token存储和登录检查
  *
- * H5公众号登录流程:
- * 1. 获取微信授权URL并重定向
- * 2. 用户授权后微信回调,获取code
- * 3. 通过code获取用户信息(openid、昵称、头像)
- * 4. 如果是老用户,直接登录成功
- * 5. 如果是新用户,调用手机号快速验证组件
- * 6. 完成注册并登录
+ * H5公众号登录注册流程:
+ * 1. 用户选择"登录"或"注册"
+ * 2. 获取微信授权URL并重定向
+ * 3. 用户授权后微信回调,获取code
+ * 4. 通过code获取用户信息(openid、昵称、头像)
+ * 5. 前端检查openid是否已注册
+ * 6. 用户输入手机号和密码
+ * 7. 登录:验证openid+手机号+密码
+ * 8. 注册:保存openid+手机号+密码+用户信息
  */
 
 import {
   getH5AuthUrl,
   h5AuthCallback,
-  h5PhoneLogin,
   getUserInfoApi
 } from './api.js'
 
@@ -121,7 +122,7 @@ export const startH5Auth = async () => {
  * 第二步:处理微信授权回调
  * 用户授权后,微信会重定向回来并携带code参数
  * @param {string} code - 微信授权code
- * @returns {Promise<object>} 返回用户信息和登录状态
+ * @returns {Promise<object>} 返回用户信息
  */
 export const handleH5AuthCallback = async (code) => {
   try {
@@ -134,31 +135,13 @@ export const handleH5AuthCallback = async (code) => {
     if (result.code === 200 && result.data) {
       const userInfo = result.data
 
-      // 如果是老用户,直接登录成功
-      if (userInfo.isRegistered && userInfo.token) {
-        setToken(userInfo.token)
-        console.log('[H5回调] 老用户登录成功')
-
-        // 获取用户信息
-        await fetchAndSaveUserInfo()
-
-        // 启动状态检查
-        startStatusCheck()
-
-        return {
-          isRegistered: true,
-          token: userInfo.token
-        }
-      } else {
-        // 新用户,需要手机号授权
-        console.log('[H5回调] 新用户,需要手机号授权')
-        return {
-          isRegistered: false,
-          openid: userInfo.openid,
-          unionid: userInfo.unionid,
-          nickname: userInfo.nickname,
-          avatarUrl: userInfo.avatarUrl
-        }
+      // 只返回微信用户信息,不自动登录
+      console.log('[H5回调] 获取微信用户信息成功')
+      return {
+        openid: userInfo.openid,
+        unionid: userInfo.unionid,
+        nickname: userInfo.nickname,
+        avatarUrl: userInfo.avatarUrl
       }
     } else {
       throw new Error(result.message || '授权回调处理失败')
@@ -169,56 +152,6 @@ export const handleH5AuthCallback = async (code) => {
   }
 }
 
-/**
- * 第三步:手机号登录/注册
- * 使用微信手机号快速验证组件获取手机号
- * @param {object} params - { openid, code, nickname, avatarUrl, unionid }
- * @returns {Promise<boolean>} 登录是否成功
- */
-export const h5PhoneLoginHandler = async (params) => {
-  try {
-    console.log('[H5手机号登录] 开始登录')
-
-    const result = await h5PhoneLogin(params)
-
-    console.log('[H5手机号登录] 后端响应:', result)
-
-    if (result.code === 200 && result.data.token) {
-      setToken(result.data.token)
-      console.log('[H5手机号登录] 登录成功')
-
-      // 获取用户信息
-      await fetchAndSaveUserInfo()
-
-      // 启动状态检查
-      startStatusCheck()
-
-      return true
-    } else {
-      throw new Error(result.message || '登录失败')
-    }
-  } catch (error) {
-    console.error('[H5手机号登录] 失败:', error)
-    throw error
-  }
-}
-
-/**
- * 获取并保存用户信息
- * 使用 token 调用后端接口获取完整用户信息
- */
-const fetchAndSaveUserInfo = async () => {
-  try {
-    const result = await getUserInfoApi()
-    if (result.code === 200 && result.data) {
-      setUserInfo(result.data)
-      console.log('[用户信息] 获取成功')
-    }
-  } catch (error) {
-    console.error('[用户信息] 获取失败:', error)
-  }
-}
-
 /**
  * 刷新用户状态(从后端获取最新信息)
  * 各页面可调用此方法确保用户状态是最新的
@@ -342,10 +275,9 @@ const checkUserStatus = async () => {
 
 /**
  * 检查登录状态,未登录则提示用户登录
- * @param {function} callback - 登录成功后的回调函数
  * @returns {boolean} 是否已登录
  */
-export const checkLogin = (callback) => {
+export const checkLogin = () => {
   if (isLoggedIn()) {
     return true
   }