|
|
@@ -6,73 +6,28 @@
|
|
|
{{ $t('login.welcome') }}
|
|
|
</div>
|
|
|
|
|
|
- <!-- 区号选择和手机号输入 -->
|
|
|
- <div class="input-group">
|
|
|
- <van-field
|
|
|
- v-model="areaCodeDisplay"
|
|
|
- readonly
|
|
|
- label="区号"
|
|
|
- :placeholder="$t('login.selectAreaCode')"
|
|
|
- @click="showAreaCodePicker = true"
|
|
|
- >
|
|
|
- <template #input>
|
|
|
- <div class="area-code-display">
|
|
|
- <span>+{{ areaCode }}</span>
|
|
|
- <van-icon name="arrow-down" />
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- </van-field>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="input-group">
|
|
|
- <van-field
|
|
|
- v-model="mobile"
|
|
|
- type="tel"
|
|
|
- label="手机号"
|
|
|
- :placeholder="$t('login.enterPhone')"
|
|
|
- clearable
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="tips">
|
|
|
- <van-icon name="info-o" color="#09b4f1" size="14" />
|
|
|
- <span>{{ $t('login.autoCreateAccount') }}</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 验证码输入 -->
|
|
|
- <div class="captcha-group">
|
|
|
- <van-field
|
|
|
- v-model="captcha"
|
|
|
- type="digit"
|
|
|
- label="验证码"
|
|
|
- :placeholder="$t('login.enterCaptcha')"
|
|
|
- clearable
|
|
|
- />
|
|
|
- <van-button
|
|
|
- :disabled="!canSendCode"
|
|
|
- :loading="sendingCode"
|
|
|
- size="small"
|
|
|
- type="primary"
|
|
|
- @click="handleSendCode"
|
|
|
- >
|
|
|
- {{ codeButtonText }}
|
|
|
- </van-button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 登录按钮 -->
|
|
|
- <van-button
|
|
|
- type="primary"
|
|
|
- round
|
|
|
- block
|
|
|
- :loading="logging"
|
|
|
- @click="handleLogin"
|
|
|
- class="login-btn"
|
|
|
- >
|
|
|
- {{ $t('login.loginNow') }}
|
|
|
- </van-button>
|
|
|
+ <!-- 登录方式切换 -->
|
|
|
+ <van-tabs v-model:active="loginType" class="login-tabs" animated>
|
|
|
+ <!-- 手机验证码登录 -->
|
|
|
+ <van-tab :title="$t('login.phoneLogin')" name="phone">
|
|
|
+ <PhoneCodeLogin
|
|
|
+ :agreed="agreedToTerms"
|
|
|
+ @login-success="handleLoginSuccess"
|
|
|
+ />
|
|
|
+ </van-tab>
|
|
|
+
|
|
|
+ <!-- 密码登录 -->
|
|
|
+ <van-tab :title="$t('login.passwordLogin')" name="password">
|
|
|
+ <PasswordLogin
|
|
|
+ :agreed="agreedToTerms"
|
|
|
+ @login-success="handleLoginSuccess"
|
|
|
+ />
|
|
|
+ </van-tab>
|
|
|
+ </van-tabs>
|
|
|
|
|
|
<!-- LINE登录 (仅在LINE环境中显示) -->
|
|
|
<div v-if="shouldShowLineFeatures" class="third-party-login">
|
|
|
+ <van-divider>{{ $t('login.orLoginWith') }}</van-divider>
|
|
|
<van-button
|
|
|
type="success"
|
|
|
round
|
|
|
@@ -88,7 +43,7 @@
|
|
|
<!-- 环境提示 (仅在开发模式显示) -->
|
|
|
<div v-if="isDev" class="env-tip">
|
|
|
<van-tag :type="isLineEnvironment ? 'success' : 'primary'" size="medium">
|
|
|
- {{ $t('login.currentEnvironment') || '当前环境' }}: {{ environmentName }}
|
|
|
+ {{ $t('login.currentEnvironment') }}: {{ environmentName }}
|
|
|
</van-tag>
|
|
|
</div>
|
|
|
|
|
|
@@ -106,28 +61,20 @@
|
|
|
</van-checkbox>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 区号选择弹窗 -->
|
|
|
- <van-popup v-model:show="showAreaCodePicker" position="bottom" round>
|
|
|
- <van-picker
|
|
|
- :title="$t('login.selectAreaCode')"
|
|
|
- :columns="areaCodeOptions"
|
|
|
- @confirm="onAreaCodeConfirm"
|
|
|
- @cancel="showAreaCodePicker = false"
|
|
|
- />
|
|
|
- </van-popup>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
import { showToast } from 'vant'
|
|
|
import { useUserStore } from '@/store/modules/user'
|
|
|
-import { login as apiLogin, sendSmsCode, lineLogin as apiLineLogin } from '@/api/auth'
|
|
|
+import { lineLogin as apiLineLogin } from '@/api/auth'
|
|
|
import { useLiff } from '@/composables/useLiff'
|
|
|
import { useEnv } from '@/composables/useEnv'
|
|
|
+import PhoneCodeLogin from './components/PhoneCodeLogin.vue'
|
|
|
+import PasswordLogin from './components/PasswordLogin.vue'
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
const router = useRouter()
|
|
|
@@ -145,40 +92,11 @@ const {
|
|
|
// 是否为开发模式
|
|
|
const isDev = ref(import.meta.env.DEV)
|
|
|
|
|
|
-// 区号选项
|
|
|
-const areaCodeOptions = ref([
|
|
|
- { text: '日本 +81', value: '81' },
|
|
|
- { text: '中国 +86', value: '86' },
|
|
|
- { text: '韓国 +82', value: '82' },
|
|
|
- { text: 'アメリカ +1', value: '1' },
|
|
|
- { text: 'イギリス +44', value: '44' }
|
|
|
-])
|
|
|
-
|
|
|
-// 表单数据
|
|
|
-const areaCode = ref('81') // 默认日本
|
|
|
-const areaCodeDisplay = ref('+81')
|
|
|
-const mobile = ref('')
|
|
|
-const captcha = ref('')
|
|
|
-const agreedToTerms = ref(false)
|
|
|
-
|
|
|
-// UI状态
|
|
|
-const showAreaCodePicker = ref(false)
|
|
|
-const sendingCode = ref(false)
|
|
|
-const logging = ref(false)
|
|
|
-const countdown = ref(0)
|
|
|
-let timer = null
|
|
|
-
|
|
|
-// 计算属性
|
|
|
-const canSendCode = computed(() => {
|
|
|
- return mobile.value && countdown.value === 0 && validatePhone(mobile.value)
|
|
|
-})
|
|
|
+// 登录方式
|
|
|
+const loginType = ref<'phone' | 'password'>('phone')
|
|
|
|
|
|
-const codeButtonText = computed(() => {
|
|
|
- if (countdown.value > 0) {
|
|
|
- return `${countdown.value}s`
|
|
|
- }
|
|
|
- return t('login.getCaptcha')
|
|
|
-})
|
|
|
+// 用户协议
|
|
|
+const agreedToTerms = ref(false)
|
|
|
|
|
|
// 初始化LIFF
|
|
|
onMounted(async () => {
|
|
|
@@ -186,133 +104,21 @@ onMounted(async () => {
|
|
|
|
|
|
// 如果已经登录,直接跳转
|
|
|
if (userStore.isLogin) {
|
|
|
- router.replace(route.query.redirect || '/index')
|
|
|
+ router.replace((route.query.redirect as string) || '/index')
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-// 验证手机号
|
|
|
-const validatePhone = (phone) => {
|
|
|
- const code = areaCode.value
|
|
|
- if (code === '81') {
|
|
|
- // 日本手机号
|
|
|
- const cleanPhone = phone.replace(/^0+/, '')
|
|
|
- return /^[1-9]\d{8,9}$/.test(cleanPhone)
|
|
|
- } else if (code === '86') {
|
|
|
- // 中国手机号
|
|
|
- return /^1[3-9]\d{9}$/.test(phone)
|
|
|
- }
|
|
|
- return phone.length >= 5
|
|
|
-}
|
|
|
-
|
|
|
-// 区号选择确认
|
|
|
-const onAreaCodeConfirm = ({ selectedValues }) => {
|
|
|
- areaCode.value = selectedValues[0]
|
|
|
- areaCodeDisplay.value = `+${selectedValues[0]}`
|
|
|
- showAreaCodePicker.value = false
|
|
|
-}
|
|
|
-
|
|
|
-// 发送验证码
|
|
|
-const handleSendCode = async () => {
|
|
|
- if (!mobile.value) {
|
|
|
- showToast(t('login.enterPhone'))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (!validatePhone(mobile.value)) {
|
|
|
- showToast(t('login.invalidPhone'))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- sendingCode.value = true
|
|
|
-
|
|
|
- // 处理手机号(去除日本手机号的前导0)
|
|
|
- const phoneValue = areaCode.value === '81'
|
|
|
- ? mobile.value.replace(/^0+/, '')
|
|
|
- : mobile.value
|
|
|
-
|
|
|
- const fullPhone = areaCode.value + phoneValue
|
|
|
-
|
|
|
- await sendSmsCode({
|
|
|
- mobile: fullPhone,
|
|
|
- scene: 1
|
|
|
- })
|
|
|
-
|
|
|
- showToast(t('login.captchaSent'))
|
|
|
-
|
|
|
- // 开始倒计时
|
|
|
- countdown.value = 60
|
|
|
- timer = setInterval(() => {
|
|
|
- countdown.value--
|
|
|
- if (countdown.value <= 0) {
|
|
|
- clearInterval(timer)
|
|
|
- timer = null
|
|
|
- }
|
|
|
- }, 1000)
|
|
|
- } catch (error) {
|
|
|
- console.error('发送验证码失败:', error)
|
|
|
- showToast(t('login.sendFailed'))
|
|
|
- } finally {
|
|
|
- sendingCode.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 手机号登录
|
|
|
-const handleLogin = async () => {
|
|
|
- if (!mobile.value) {
|
|
|
- showToast(t('login.enterPhone'))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (!validatePhone(mobile.value)) {
|
|
|
- showToast(t('login.invalidPhone'))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (!captcha.value) {
|
|
|
- showToast(t('login.enterCaptcha'))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (!agreedToTerms.value) {
|
|
|
- showToast(t('login.checkAgreement'))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- logging.value = true
|
|
|
-
|
|
|
- // 处理手机号
|
|
|
- const phoneValue = areaCode.value === '81'
|
|
|
- ? mobile.value.replace(/^0+/, '')
|
|
|
- : mobile.value
|
|
|
+// 登录成功处理
|
|
|
+const handleLoginSuccess = (data: any) => {
|
|
|
+ userStore.setMember(data.userInfo)
|
|
|
+ userStore.setToken(data.accessToken)
|
|
|
|
|
|
- const fullPhone = areaCode.value + phoneValue
|
|
|
+ showToast(t('login.success'))
|
|
|
|
|
|
- const res = await apiLogin({
|
|
|
- mobile: fullPhone,
|
|
|
- code: captcha.value,
|
|
|
- from: 'h5'
|
|
|
- })
|
|
|
-
|
|
|
- if (res) {
|
|
|
- // 保存用户信息和token
|
|
|
- userStore.setMember(res.userInfo)
|
|
|
- userStore.setToken(res.accessToken)
|
|
|
-
|
|
|
- showToast(t('login.loginSuccess'))
|
|
|
-
|
|
|
- // 跳转
|
|
|
- setTimeout(() => {
|
|
|
- const redirect = route.query.redirect || '/index'
|
|
|
- router.replace(redirect)
|
|
|
- }, 1000)
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('登录失败:', error)
|
|
|
- } finally {
|
|
|
- logging.value = false
|
|
|
- }
|
|
|
+ setTimeout(() => {
|
|
|
+ const redirect = (route.query.redirect as string) || '/index'
|
|
|
+ router.replace(redirect)
|
|
|
+ }, 1000)
|
|
|
}
|
|
|
|
|
|
// LINE登录
|
|
|
@@ -323,31 +129,20 @@ const handleLineLogin = async () => {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- // 如果未登录LINE,先登录
|
|
|
if (!isLoggedIn.value) {
|
|
|
await liffLogin()
|
|
|
}
|
|
|
|
|
|
- // 获取LINE AccessToken
|
|
|
const accessToken = await getAccessToken()
|
|
|
if (!accessToken) {
|
|
|
showToast('获取LINE认证信息失败')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- // 调用后端LINE登录接口
|
|
|
const res = await apiLineLogin({ token: accessToken })
|
|
|
|
|
|
if (res) {
|
|
|
- userStore.setMember(res.userInfo)
|
|
|
- userStore.setToken(res.accessToken)
|
|
|
-
|
|
|
- showToast(t('login.loginSuccess'))
|
|
|
-
|
|
|
- setTimeout(() => {
|
|
|
- const redirect = route.query.redirect || '/index'
|
|
|
- router.replace(redirect)
|
|
|
- }, 1000)
|
|
|
+ handleLoginSuccess(res)
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('LINE登录失败:', error)
|
|
|
@@ -356,17 +151,9 @@ const handleLineLogin = async () => {
|
|
|
}
|
|
|
|
|
|
// 显示协议
|
|
|
-const showAgreement = (type) => {
|
|
|
+const showAgreement = (type: string) => {
|
|
|
showToast(t('common.featureInDevelopment'))
|
|
|
}
|
|
|
-
|
|
|
-// 组件销毁时清除定时器
|
|
|
-onUnmounted(() => {
|
|
|
- if (timer) {
|
|
|
- clearInterval(timer)
|
|
|
- timer = null
|
|
|
- }
|
|
|
-})
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
@@ -379,62 +166,37 @@ onUnmounted(() => {
|
|
|
.login-container {
|
|
|
max-width: 500px;
|
|
|
margin: 0 auto;
|
|
|
- padding-top: 60px;
|
|
|
+ padding-top: 40px;
|
|
|
}
|
|
|
|
|
|
.title {
|
|
|
font-size: 32px;
|
|
|
font-weight: 500;
|
|
|
color: #323233;
|
|
|
- margin-bottom: 50px;
|
|
|
+ margin-bottom: 40px;
|
|
|
}
|
|
|
|
|
|
-.input-group {
|
|
|
+.login-tabs {
|
|
|
margin-bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.area-code-display {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 4px;
|
|
|
- color: #323233;
|
|
|
-}
|
|
|
-
|
|
|
-.tips {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 6px;
|
|
|
- font-size: 12px;
|
|
|
- color: #969799;
|
|
|
- margin-bottom: 30px;
|
|
|
- padding: 0 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.captcha-group {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 30px;
|
|
|
|
|
|
- :deep(.van-field) {
|
|
|
- flex: 1;
|
|
|
+ :deep(.van-tab) {
|
|
|
+ font-size: 15px;
|
|
|
}
|
|
|
|
|
|
- .van-button {
|
|
|
- flex-shrink: 0;
|
|
|
+ :deep(.van-tabs__content) {
|
|
|
+ padding-top: 20px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.login-btn {
|
|
|
- margin-bottom: 20px;
|
|
|
- height: 44px;
|
|
|
- font-size: 16px;
|
|
|
- background-color: #09b4f1;
|
|
|
- border-color: #09b4f1;
|
|
|
-}
|
|
|
-
|
|
|
.third-party-login {
|
|
|
+ margin-top: 30px;
|
|
|
margin-bottom: 20px;
|
|
|
+
|
|
|
+ .van-divider {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #969799;
|
|
|
+ margin: 20px 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.line-login-btn {
|
|
|
@@ -457,6 +219,7 @@ onUnmounted(() => {
|
|
|
font-size: 12px;
|
|
|
color: #969799;
|
|
|
text-align: center;
|
|
|
+ margin-top: 20px;
|
|
|
|
|
|
.link {
|
|
|
color: #f9ae3d;
|