LoginPage.vue 12.6 KB
<template>
  <div
    class="flex min-h-screen flex-col bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 px-4 py-8 sm:px-6 lg:px-8"
  >
    <div class="text-center sm:mx-auto sm:w-full sm:max-w-md">
      <van-icon
        name="https://cdn.ipadbiz.cn/mlaj/icon/behalo-logo-2.png?imageMogr2/thumbnail/200x/strip/quality/70"
        size="10rem"
        style="margin-bottom: 0.5rem"
      />
      <h1 class="mb-2 text-center text-3xl font-bold text-gray-800">生命力教育联盟教育</h1>
      <!-- <h2 class="text-center text-xl font-medium text-gray-600">欢迎回来</h2> -->
    </div>

    <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
      <FrostedGlass class="rounded-lg px-6 py-8">
        <div
          v-if="error"
          class="mb-4 rounded-md border border-red-400 bg-red-100 px-4 py-3 text-red-700"
        >
          {{ error }}
        </div>

        <form class="space-y-6" @submit.prevent="handleSubmit">
          <div>
            <label for="mobile" class="block text-sm font-medium text-gray-700">
              手机号 <span class="text-red-500">*</span>
            </label>
            <input
              id="mobile"
              v-model="mobile"
              name="mobile"
              type="tel"
              autocomplete="tel"
              required
              pattern="^1[3-9]\d{9}$"
              maxlength="11"
              placeholder="请输入手机号"
              @input="mobile = $event.target.value.replace(/\D/g, '')"
              @focus="handleInputFocus"
              @blur="validatePhone"
              class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
            />
          </div>

          <div v-if="!isVerifyCodeLogin">
            <label for="password" class="block text-sm font-medium text-gray-700">
              密码 <span class="text-red-500">*</span>
            </label>
            <input
              id="password"
              v-model="password"
              name="password"
              type="password"
              autocomplete="current-password"
              required
              placeholder="请输入6位密码"
              class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
            />
          </div>

          <div v-else>
            <label for="verificationCode" class="block text-sm font-medium text-gray-700">
              验证码 <span class="text-red-500">*</span>
            </label>
            <div class="flex space-x-2">
              <input
                id="verificationCode"
                v-model="verificationCode"
                type="text"
                required
                maxlength="6"
                placeholder="请输入验证码"
                class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
              />
              <button
                type="button"
                :disabled="countdown > 0 || !isPhoneValid"
                @click="sendVerificationCode"
                class="mt-1 whitespace-nowrap rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
              >
                {{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
              </button>
            </div>
          </div>

          <!-- <div class="flex justify-end space-x-4">
            <div class="text-sm">
              <router-link
                to="/forgotPwd"
                class="font-medium text-green-600 hover:text-green-500"
              >
                忘记密码?
              </router-link>
            </div>
            <div class="text-sm">
              <button
                type="button"
                class="font-medium text-green-600 hover:text-green-500"
                @click="isVerifyCodeLogin = !isVerifyCodeLogin"
              >
                {{ isVerifyCodeLogin ? '密码登录' : '验证码登录' }}
              </button>
            </div>
          </div> -->

          <div>
            <button
              type="submit"
              :disabled="loading"
              class="flex w-full justify-center rounded-md border border-transparent bg-gradient-to-r from-green-500 to-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-green-600 hover:to-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
              :class="{ 'cursor-not-allowed opacity-70': loading }"
            >
              {{ loading ? '登录中...' : '登录/注册BEHALO宇宙账号' }}
            </button>
          </div>
        </form>

        <!-- <div class="mt-6">
          <div class="relative">
            <div class="absolute inset-0 flex items-center">
              <div class="w-full border-t border-gray-300"></div>
            </div>
            <div class="relative flex justify-center text-sm">
              <span class="px-2 bg-white/30 backdrop-blur-sm text-gray-500">
                或者
              </span>
            </div>
          </div>

          <div class="mt-6 grid grid-cols-2 gap-3">
            <button
              type="button"
              class="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
            >
              <svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
                <path d="M12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-2.917 16.083c-2.258 0-4.083-1.825-4.083-4.083s1.825-4.083 4.083-4.083c1.103 0 2.024.402 2.735 1.067l-1.107 1.068c-.304-.292-.834-.63-1.628-.63-1.394 0-2.531 1.155-2.531 2.579 0 1.424 1.138 2.579 2.531 2.579 1.616 0 2.224-1.162 2.316-1.762h-2.316v-1.4h3.855c.036.204.064.408.064.677.001 2.332-1.563 3.988-3.919 3.988zm9.917-3.5h-1.75v1.75h-1.167v-1.75h-1.75v-1.166h1.75v-1.75h1.167v1.75h1.75v1.166z"></path>
              </svg>
              Google
            </button>
            <button
              type="button"
              class="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
            >
              <svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
                <path d="M12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-2.917 16.083c-2.258 0-4.083-1.825-4.083-4.083s1.825-4.083 4.083-4.083c1.103 0 2.024.402 2.735 1.067l-1.107 1.068c-.304-.292-.834-.63-1.628-.63-1.394 0-2.531 1.155-2.531 2.579 0 1.424 1.138 2.579 2.531 2.579 1.616 0 2.224-1.162 2.316-1.762h-2.316v-1.4h3.855c.036.204.064.408.064.677.001 2.332-1.563 3.988-3.919 3.988zm9.917-3.5h-1.75v1.75h-1.167v-1.75h-1.75v-1.166h1.75v-1.75h1.167v1.75h1.75v1.166z"></path>
              </svg>
              微信
            </button>
          </div>
        </div> -->

        <!-- <div class="text-center mt-6">
          <p class="text-sm text-gray-600">
            还没有账号?
            <router-link
              to="/register"
              class="font-medium text-green-600 hover:text-green-500"
            >
              立即注册
            </router-link>
          </p>
        </div> -->
        <div class="mt-6 text-center">
          <p class="text-sm text-gray-600">
            登录即表示同意
            <span
              class="cursor-pointer font-medium text-green-600 hover:text-green-500"
              @click="userAgreementRef.openAgreement()"
            >
              《生命力教育联盟宇宙用户协议》
            </span>
            <UserAgreement ref="userAgreementRef" />
          </p>
        </div>

        <div class="mt-6 flex items-center justify-center gap-4" style="flex-direction: column">
          <div class="text-sm font-medium text-gray-600">其他登录方式</div>
          <div>
            <img
              :src="weixinLogo"
              alt="微信登录"
              style="width: 40px; height: 40px; border-radius: 8px"
              @click="handleWxLoginClick"
            />
          </div>
        </div>
      </FrostedGlass>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import FrostedGlass from '@/components/effects/FrostedGlass.vue'
import { useAuth } from '@/contexts/auth'
import { loginAPI, getUserInfoAPI } from '@/api/users'
import { useTitle } from '@vueuse/core'
import { smsAPI } from '@/api/common'
import { showToast } from 'vant'
import UserAgreement from '@/components/common/UserAgreement.vue'
import { setAuthHeaders } from '@/utils/axios'
import { applyUserInfoAuth } from '@/utils/auth_user_info'
import weixinLogo from '@/assets/images/weixin_logo_lg.jpeg'
import { startWxAuth } from '@/router/guards'
import { wxInfo } from '@/utils/tools'

const userAgreementRef = ref(null)

const handleInputFocus = () => {
  setTimeout(() => {
    // 使用平滑滚动将页面向上移动200px(根据实际按钮区域高度调整)
    window.scrollTo({
      top: 150,
      behavior: 'smooth',
    })
  }, 100)
}

const $route = useRoute()
useTitle($route.meta.title)

const router = useRouter()
const { login } = useAuth()

const mobile = ref('')
const password = ref('')
const verificationCode = ref('')
const error = ref('')
const loading = ref(false)
const isVerifyCodeLogin = ref(true)
const countdown = ref(0)
const isPhoneValid = ref(false)

const validatePhone = () => {
  if (!mobile.value) {
    error.value = '请输入手机号'
    isPhoneValid.value = false
    return
  }

  if (!/^1[3-9]\d{9}$/.test(mobile.value)) {
    error.value = '请输入正确的手机号'
    isPhoneValid.value = false
    return
  }

  error.value = ''
  isPhoneValid.value = true
}

const startCountdown = () => {
  countdown.value = 60
  const timer = setInterval(() => {
    countdown.value--
    if (countdown.value <= 0) {
      clearInterval(timer)
    }
  }, 1000)
}

/**
 * @description 点击微信图标触发微信授权登录
 * @returns {void}
 */
const handleWxLoginClick = async () => {
  // 非微信环境提示并不触发授权
  if (!import.meta.env.DEV && !wxInfo().isWeiXin) {
    showToast('请在微信内打开以完成微信授权登录')
    return
  }

  try {
    // 手动发起微信授权流程(guards.js中实现)
    await startWxAuth()
  } catch (e) {
    console.error('微信授权触发失败:', e)
    showToast('微信授权失败,请稍后重试')
  }
}

const sendVerificationCode = async () => {
  if (!isPhoneValid.value) {
    return
  }

  try {
    const { code } = await smsAPI({ mobile: mobile.value })
    if (code === 1) {
      showToast('验证码已发送')
      startCountdown()
      return
    }
  } catch (err) {
    console.error('Send verification code error:', err)
    error.value = '发送验证码失败,请稍后重试'
  }
}

const handleSubmit = async () => {
  if (
    !mobile.value ||
    (!isVerifyCodeLogin.value && !password.value) ||
    (isVerifyCodeLogin.value && !verificationCode.value)
  ) {
    error.value = '请填写所有字段'
    return
  }

  try {
    error.value = ''
    loading.value = true

    // 调用登录接口
    const response = await loginAPI({
      mobile: mobile.value,
      ...(isVerifyCodeLogin.value
        ? { sms_code: verificationCode.value }
        : { password: password.value }),
    })

    if (response.code !== 1) {
      error.value = response.msg || '登录失败,请检查您的输入项'
      return
    }
    applyUserInfoAuth(response, { set_auth_headers: setAuthHeaders, storage: localStorage })

    const { code, data } = await getUserInfoAPI()
    if (code === 1) {
      // 登录成功,更新auth状态,传递完整的用户信息包括打卡信息
      const success = login({ ...data.user, ...data.checkin })

      if (success) {
        // 如果有重定向参数,登录成功后跳转到对应页面
        // 说明:redirect 是经过 URL 编码的,需要先解码再跳转
        const redirect_raw = $route.query.redirect
        const redirect = redirect_raw ? decodeURIComponent(redirect_raw) : '/'
        router.push(redirect)
      } else {
        error.value = '登录失败,请检查您的输入项'
      }
    }
  } catch (err) {
    console.error('Login error:', err)
    error.value = '登录时发生错误'
  } finally {
    loading.value = false
  }
}

// 说明:授权回跳的重定向处理已移动到路由层
</script>