|
|
@@ -1,248 +1,626 @@
|
|
|
<template>
|
|
|
<div class="login-page">
|
|
|
- <div class="login-container">
|
|
|
- <!-- 标题 -->
|
|
|
- <div class="title">
|
|
|
- {{ $t('login.welcome') }}
|
|
|
+ <!-- Top Bar -->
|
|
|
+ <div class="login-top-bar">
|
|
|
+ <div class="employee-entry" @click="handleEmployeeLogin">
|
|
|
+ <van-icon name="friends-o" size="24" color="#181511" />
|
|
|
+ <span class="employee-text">{{ $t('login.employee') || 'Staff' }}</span>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 登录方式切换 -->
|
|
|
- <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>
|
|
|
-
|
|
|
- <!-- 注册提示 -->
|
|
|
- <div class="register-hint">
|
|
|
- {{ $t('login.noAccount') }}
|
|
|
- <span class="link" @click="loginType = 'phone'">{{ $t('login.registerNow') }}</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- LINE登录 (仅在LINE环境中显示) -->
|
|
|
- <div v-if="shouldShowLineFeatures" class="third-party-login">
|
|
|
- <van-divider>{{ $t('login.orLoginWith') }}</van-divider>
|
|
|
- <van-button
|
|
|
- type="success"
|
|
|
- round
|
|
|
- block
|
|
|
- @click="handleLineLogin"
|
|
|
- class="line-login-btn"
|
|
|
- >
|
|
|
- <van-icon name="chat-o" />
|
|
|
- LINE {{ $t('login.quickLogin') }}
|
|
|
- </van-button>
|
|
|
+ <div class="lang-selector" @click="showLangAction = true">
|
|
|
+ <van-icon name="globe-o" size="18" class="mr-1" />
|
|
|
+ <span class="lang-text">{{ currentLangText }}</span>
|
|
|
+ <van-icon name="arrow-down" size="14" />
|
|
|
</div>
|
|
|
+ </div>
|
|
|
|
|
|
- <!-- 环境提示 (仅在开发模式显示) -->
|
|
|
- <div v-if="isDev" class="env-tip">
|
|
|
- <van-tag :type="isLineEnvironment ? 'success' : 'primary'" size="medium">
|
|
|
- {{ $t('login.currentEnvironment') }}: {{ environmentName }}
|
|
|
- </van-tag>
|
|
|
+ <div class="login-card">
|
|
|
+ <!-- Header Area with Pattern -->
|
|
|
+ <div class="card-header">
|
|
|
+ <div class="pattern-bg"></div>
|
|
|
+ <div class="header-icon-wrapper">
|
|
|
+ <div class="header-icon">
|
|
|
+ <van-icon name="shop-o" size="40" color="#f2930d" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 用户协议 -->
|
|
|
- <div class="agreement">
|
|
|
- <van-checkbox v-model="agreedToTerms" icon-size="14px">
|
|
|
- {{ $t('login.agreementPrefix') }}
|
|
|
- <span class="link" @click.stop="showAgreement('user')">
|
|
|
- 《{{ $t('login.userAgreement') }}》
|
|
|
- </span>
|
|
|
- {{ $t('login.and') }}
|
|
|
- <span class="link" @click.stop="showAgreement('privacy')">
|
|
|
- 《{{ $t('login.privacyPolicy') }}》
|
|
|
- </span>
|
|
|
- </van-checkbox>
|
|
|
+ <!-- Content Area -->
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="text-center mb-8">
|
|
|
+ <h2 class="welcome-title">{{ $t('login.welcome') || 'Welcome to LINE Order' }}</h2>
|
|
|
+ <p class="welcome-subtitle">Order your favorites with ease.</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Phone Login Mode -->
|
|
|
+ <div v-if="loginMode === 'phone'" class="form-section">
|
|
|
+ <div class="form-group">
|
|
|
+ <label class="form-label">{{ $t('login.phone') || 'Mobile Number' }}</label>
|
|
|
+ <div class="phone-input-pill">
|
|
|
+ <div class="flag-prefix">
|
|
|
+ <span class="flag">🇯🇵</span>
|
|
|
+ <span class="prefix">+81</span>
|
|
|
+ </div>
|
|
|
+ <van-field
|
|
|
+ v-model="mobile"
|
|
|
+ type="tel"
|
|
|
+ class="pill-field"
|
|
|
+ :placeholder="$t('login.enterPhone') || '(555) 000-0000'"
|
|
|
+ :border="false"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="showCaptchaInput" class="form-group mt-4">
|
|
|
+ <label class="form-label">{{ $t('login.captcha') }}</label>
|
|
|
+ <div class="input-pill">
|
|
|
+ <van-field
|
|
|
+ v-model="captcha"
|
|
|
+ type="digit"
|
|
|
+ class="pill-field"
|
|
|
+ :placeholder="$t('login.enterCaptcha')"
|
|
|
+ :border="false"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <van-button
|
|
|
+ type="primary"
|
|
|
+ round
|
|
|
+ block
|
|
|
+ class="action-btn"
|
|
|
+ :loading="loading"
|
|
|
+ @click="handlePhoneAction"
|
|
|
+ >
|
|
|
+ {{ showCaptchaInput ? $t('login.loginNow') : $t('login.getCaptcha') }}
|
|
|
+ <template #icon v-if="!loading">
|
|
|
+ <van-icon name="arrow" />
|
|
|
+ </template>
|
|
|
+ </van-button>
|
|
|
+
|
|
|
+ <div class="text-center pt-4">
|
|
|
+ <div class="text-link" @click="loginMode = 'password'">
|
|
|
+ {{ $t('login.passwordLogin') || 'Log in with Password instead' }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Password Login Mode -->
|
|
|
+ <div v-else class="form-section">
|
|
|
+ <div class="form-group">
|
|
|
+ <label class="form-label">{{ $t('login.phone') }}</label>
|
|
|
+ <div class="phone-input-pill">
|
|
|
+ <div class="flag-prefix">
|
|
|
+ <span class="flag">🇯🇵</span>
|
|
|
+ <span class="prefix">+81</span>
|
|
|
+ </div>
|
|
|
+ <van-field
|
|
|
+ v-model="mobile"
|
|
|
+ type="tel"
|
|
|
+ class="pill-field"
|
|
|
+ :placeholder="$t('login.enterPhone')"
|
|
|
+ :border="false"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group mt-4">
|
|
|
+ <label class="form-label">{{ $t('login.password') }}</label>
|
|
|
+ <div class="input-pill">
|
|
|
+ <van-field
|
|
|
+ v-model="password"
|
|
|
+ :type="showPassword ? 'text' : 'password'"
|
|
|
+ class="pill-field"
|
|
|
+ :placeholder="$t('login.enterPassword')"
|
|
|
+ :border="false"
|
|
|
+ :right-icon="showPassword ? 'eye-o' : 'closed-eye'"
|
|
|
+ @click-right-icon="showPassword = !showPassword"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <van-button
|
|
|
+ type="primary"
|
|
|
+ round
|
|
|
+ block
|
|
|
+ class="action-btn"
|
|
|
+ :loading="loading"
|
|
|
+ @click="handlePasswordLogin"
|
|
|
+ >
|
|
|
+ {{ $t('login.loginNow') }}
|
|
|
+ <template #icon v-if="!loading">
|
|
|
+ <van-icon name="arrow" />
|
|
|
+ </template>
|
|
|
+ </van-button>
|
|
|
+
|
|
|
+ <div class="text-center pt-4">
|
|
|
+ <div class="text-link" @click="loginMode = 'phone'">
|
|
|
+ {{ $t('login.phoneLogin') || 'Log in with Phone instead' }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Divider -->
|
|
|
+ <div class="divider-section">
|
|
|
+ <div class="divider-line"></div>
|
|
|
+ <div class="divider-text">{{ $t('login.orLoginWith') || 'Or continue with' }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Social Buttons -->
|
|
|
+ <div class="social-grid">
|
|
|
+ <van-button round class="social-btn line" @click="handleLineLogin">
|
|
|
+ <svg class="w-6 h-6 fill-current" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2.5C6.6 2.5 2.5 6.2 2.5 10.8c0 2.6 1.3 4.9 3.4 6.4-.2.7-.8 2.5-.9 2.9-.1.3 0 .6.3.6.1 0 .2 0 .3-.1 3.5-2 3.9-2.2 4.2-2.3.4.1.8.1 1.2.1 5.4 0 9.5-3.7 9.5-8.3S17.4 2.5 12 2.5z" fill="white"></path></svg>
|
|
|
+ </van-button>
|
|
|
+ <van-button round class="social-btn" @click="showToast('Google Login')">
|
|
|
+ <svg class="w-6 h-6" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"></path><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"></path><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"></path><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"></path></svg>
|
|
|
+ </van-button>
|
|
|
+ <van-button round class="social-btn" @click="showToast('Apple Login')">
|
|
|
+ <svg class="w-6 h-6" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.74 1.18 0 2.45-1.02 3.93-.84 1.16.14 2.29.58 3.09 1.54-.1.06-2.09 1.23-2.03 3.77.06 2.65 2.44 3.59 2.5 3.63-.03.11-.38 1.34-1.27 2.68-.82 1.23-1.68 2.45-3.03 2.45h-.26zM13.03 4.98c.67-.84 1.15-2.02.97-3.18-1.02.04-2.29.69-3.02 1.56-.63.74-1.19 1.95-.94 3.08 1.13.08 2.29-.61 2.99-1.46z"></path></svg>
|
|
|
+ </van-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="mt-auto text-center pt-8">
|
|
|
+ <p class="footer-text">
|
|
|
+ {{ $t('login.newToApp') || 'New to LINE Order?' }}
|
|
|
+ <a class="register-link" @click="router.push('/register')">
|
|
|
+ {{ $t('login.registerNow') || 'Register Now' }}
|
|
|
+ </a>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- Language Action Sheet -->
|
|
|
+ <van-action-sheet
|
|
|
+ v-model:show="showLangAction"
|
|
|
+ :actions="langActions"
|
|
|
+ @select="onSelectLang"
|
|
|
+ :cancel-text="$t('common.cancel') || 'Cancel'"
|
|
|
+ close-on-click-action
|
|
|
+ />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, onMounted } from 'vue'
|
|
|
+import { ref, onMounted, computed } from 'vue'
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
import { showToast } from 'vant'
|
|
|
import { useUserStore } from '@/store/modules/user'
|
|
|
-import { lineLogin as apiLineLogin } from '@/api/auth'
|
|
|
+import { login as apiLogin, sendSmsCode, 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()
|
|
|
+// TypeScript Interfaces
|
|
|
+interface LangAction {
|
|
|
+ name: string
|
|
|
+ value: string
|
|
|
+}
|
|
|
+
|
|
|
+interface LoginResponse {
|
|
|
+ userInfo: any
|
|
|
+ accessToken: string
|
|
|
+}
|
|
|
+
|
|
|
+const { t, locale } = useI18n()
|
|
|
const router = useRouter()
|
|
|
const route = useRoute()
|
|
|
const userStore = useUserStore()
|
|
|
const { init: initLiff, login: liffLogin, getAccessToken, isLoggedIn } = useLiff()
|
|
|
|
|
|
-// 环境检测
|
|
|
-const {
|
|
|
- shouldShowLineFeatures,
|
|
|
- isLineEnvironment,
|
|
|
- environmentName
|
|
|
-} = useEnv()
|
|
|
-
|
|
|
-// 是否为开发模式
|
|
|
-const isDev = ref(import.meta.env.DEV)
|
|
|
+// Logic State
|
|
|
+const loginMode = ref<'phone' | 'password'>('phone')
|
|
|
+const mobile = ref('')
|
|
|
+const captcha = ref('')
|
|
|
+const password = ref('')
|
|
|
+const showPassword = ref(false)
|
|
|
+const showCaptchaInput = ref(false) // Whether code has been sent
|
|
|
+const loading = ref(false)
|
|
|
+const showLangAction = ref(false)
|
|
|
+
|
|
|
+// Language Actions
|
|
|
+const langActions: LangAction[] = [
|
|
|
+ { name: '日本語', value: 'ja' },
|
|
|
+ { name: 'English', value: 'en' },
|
|
|
+ { name: '简体中文', value: 'zh-Hans' },
|
|
|
+ { name: '繁體中文', value: 'zh-Hant' }
|
|
|
+]
|
|
|
+
|
|
|
+const currentLangText = computed(() => {
|
|
|
+ const current = langActions.find(item => item.value === locale.value)
|
|
|
+ return current ? current.name : 'Language'
|
|
|
+})
|
|
|
|
|
|
-// 登录方式
|
|
|
-const loginType = ref<'phone' | 'password'>('phone')
|
|
|
+const onSelectLang = (item: LangAction) => {
|
|
|
+ locale.value = item.value
|
|
|
+ showLangAction.value = false
|
|
|
+ localStorage.setItem('locale', item.value)
|
|
|
+}
|
|
|
|
|
|
-// 用户协议
|
|
|
-const agreedToTerms = ref(false)
|
|
|
+const handleEmployeeLogin = () => {
|
|
|
+ // Navigate to employee/merchant login if exists, else show toast
|
|
|
+ showToast(t('common.featureInDevelopment'))
|
|
|
+}
|
|
|
|
|
|
-// 初始化LIFF
|
|
|
onMounted(async () => {
|
|
|
await initLiff()
|
|
|
-
|
|
|
- // 如果已经登录,直接跳转
|
|
|
if (userStore.isLogin) {
|
|
|
router.replace((route.query.redirect as string) || '/index')
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-// 登录成功处理
|
|
|
-const handleLoginSuccess = (data: any) => {
|
|
|
- userStore.setMember(data.userInfo)
|
|
|
- userStore.setToken(data.accessToken)
|
|
|
-
|
|
|
- showToast(t('login.success'))
|
|
|
+// Phone Flow
|
|
|
+const handlePhoneAction = async () => {
|
|
|
+ if (!mobile.value) {
|
|
|
+ showToast(t('login.enterPhone'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Step 1: Send Code
|
|
|
+ if (!showCaptchaInput.value) {
|
|
|
+ await handleSendCode()
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- setTimeout(() => {
|
|
|
- const redirect = (route.query.redirect as string) || '/index'
|
|
|
- router.replace(redirect)
|
|
|
- }, 1000)
|
|
|
+ // Step 2: Login with Code
|
|
|
+ await handleLogin()
|
|
|
}
|
|
|
|
|
|
-// LINE登录
|
|
|
-const handleLineLogin = async () => {
|
|
|
- if (!agreedToTerms.value) {
|
|
|
- showToast(t('login.checkAgreement'))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- if (!isLoggedIn.value) {
|
|
|
- await liffLogin()
|
|
|
+const handleSendCode = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const fullPhone = '81' + mobile.value.replace(/^0+/, '') // Mock JP code
|
|
|
+ await sendSmsCode({ mobile: fullPhone, scene: 1 })
|
|
|
+ showToast(t('login.captchaSent'))
|
|
|
+ showCaptchaInput.value = true
|
|
|
+ // Start countdown (simplified for design view)
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error)
|
|
|
+ showToast(t('login.sendFailed'))
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- const accessToken = await getAccessToken()
|
|
|
- if (!accessToken) {
|
|
|
- showToast('获取LINE认证信息失败')
|
|
|
- return
|
|
|
+const handleLogin = async () => {
|
|
|
+ if (!captcha.value) {
|
|
|
+ showToast(t('login.enterCaptcha'))
|
|
|
+ return
|
|
|
}
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const fullPhone = '81' + mobile.value.replace(/^0+/, '')
|
|
|
+ const res = await apiLogin({ mobile: fullPhone, code: captcha.value })
|
|
|
+ if (res) handleLoginSuccess(res)
|
|
|
+ } catch(err) {
|
|
|
+ console.error(err)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- const res = await apiLineLogin({ token: accessToken })
|
|
|
-
|
|
|
- if (res) {
|
|
|
- handleLoginSuccess(res)
|
|
|
+const handlePasswordLogin = async () => {
|
|
|
+ if (!mobile.value || !password.value) return showToast(t('login.error'))
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const fullPhone = '81' + mobile.value.replace(/^0+/, '')
|
|
|
+ const res = await apiLogin({ mobile: fullPhone, password: password.value })
|
|
|
+ if (res) handleLoginSuccess(res)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+const handleLineLogin = async () => {
|
|
|
+ try {
|
|
|
+ if (!isLoggedIn.value) await liffLogin()
|
|
|
+ const accessToken = await getAccessToken()
|
|
|
+ if (!accessToken) return showToast('LINE Login Error')
|
|
|
+ const res = await apiLineLogin(accessToken)
|
|
|
+ if (res) handleLoginSuccess(res)
|
|
|
} catch (error) {
|
|
|
- console.error('LINE登录失败:', error)
|
|
|
- showToast('LINE登录失败')
|
|
|
+ console.error(error)
|
|
|
+ showToast('LINE Login Failed')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 显示协议
|
|
|
-const showAgreement = (type: string) => {
|
|
|
- showToast(t('common.featureInDevelopment'))
|
|
|
+const handleLoginSuccess = (data: LoginResponse) => {
|
|
|
+ userStore.setMember(data.userInfo)
|
|
|
+ userStore.setToken(data.accessToken)
|
|
|
+ showToast(t('login.success'))
|
|
|
+
|
|
|
+ const REDIRECT_DELAY = 1000
|
|
|
+ setTimeout(() => {
|
|
|
+ router.replace((route.query.redirect as string) || '/index')
|
|
|
+ }, REDIRECT_DELAY)
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.login-page {
|
|
|
min-height: 100vh;
|
|
|
- background: #fff;
|
|
|
- padding: 20px;
|
|
|
+ background-color: #fff;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ font-family: var(--font-body);
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
-.login-container {
|
|
|
- max-width: 500px;
|
|
|
- margin: 0 auto;
|
|
|
- padding-top: 40px;
|
|
|
+/* Top Bar Styles */
|
|
|
+.login-top-bar {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ padding: 16px 20px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 100;
|
|
|
}
|
|
|
|
|
|
-.title {
|
|
|
- font-size: 32px;
|
|
|
- font-weight: 500;
|
|
|
- color: #323233;
|
|
|
- margin-bottom: 40px;
|
|
|
+.lang-selector {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background: rgba(255, 255, 255, 0.8);
|
|
|
+ border-radius: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
|
+
|
|
|
+ .lang-text {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #181511;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.login-tabs {
|
|
|
- margin-bottom: 20px;
|
|
|
+.employee-entry {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 2px;
|
|
|
+ cursor: pointer;
|
|
|
|
|
|
- :deep(.van-tab) {
|
|
|
- font-size: 15px;
|
|
|
+ .employee-text {
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #181511;
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- :deep(.van-tabs__content) {
|
|
|
- padding-top: 20px;
|
|
|
- }
|
|
|
+.login-card {
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ background: #fff;
|
|
|
+ overflow-y: auto;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-.register-hint {
|
|
|
- text-align: center;
|
|
|
- font-size: 14px;
|
|
|
- color: #666;
|
|
|
- margin-bottom: 20px;
|
|
|
-
|
|
|
- .link {
|
|
|
- color: #1989fa;
|
|
|
- cursor: pointer;
|
|
|
+ /* Header Pattern */
|
|
|
+ .card-header {
|
|
|
+ height: 240px;
|
|
|
+ position: relative;
|
|
|
+ background-color: rgba(242, 147, 13, 0.08); /* Primary tint */
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pattern-bg {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ opacity: 0.2;
|
|
|
+ background-image: radial-gradient(#f2930d 1px, transparent 1px);
|
|
|
+ background-size: 20px 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header-icon-wrapper {
|
|
|
+ position: relative;
|
|
|
+ z-index: 50;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header-icon {
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Content */
|
|
|
+ .card-content {
|
|
|
+ padding: 32px 24px;
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .welcome-title {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #181511;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ font-family: var(--font-heading);
|
|
|
+ }
|
|
|
+
|
|
|
+ .welcome-subtitle {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #8a7960;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #181511;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ padding-left: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Custom Input Pills */
|
|
|
+ .phone-input-pill {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #f9fafb; /* gray-50 equivalent */
|
|
|
+ border-radius: 999px;
|
|
|
+ padding: 2px;
|
|
|
+ transition: all 0.3s;
|
|
|
+ border: 1px solid transparent;
|
|
|
+
|
|
|
+ &:focus-within {
|
|
|
+ border-color: rgba(242, 147, 13, 0.5);
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 0 0 2px rgba(242, 147, 13, 0.2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-pill {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #f9fafb;
|
|
|
+ border-radius: 999px;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ transition: all 0.3s;
|
|
|
+
|
|
|
+ &:focus-within {
|
|
|
+ border-color: rgba(242, 147, 13, 0.5);
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 0 0 2px rgba(242, 147, 13, 0.2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Vant Field Overrides for Pill */
|
|
|
+ .pill-field {
|
|
|
+ flex: 1;
|
|
|
+ background: transparent;
|
|
|
+ padding: 10px 16px;
|
|
|
+ /* Ensure radius matches container if needed, but transparent bg makes it okay */
|
|
|
+ --van-field-input-text-color: #181511;
|
|
|
+ }
|
|
|
+
|
|
|
+ .flag-prefix {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 14px 16px;
|
|
|
+ gap: 6px;
|
|
|
+ border-right: 1px solid #e5e7eb;
|
|
|
+ height: 24px;
|
|
|
+ margin: 12px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .flag { font-size: 18px; }
|
|
|
+ .prefix {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #181511;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Action Button */
|
|
|
+ .action-btn {
|
|
|
+ width: 100%;
|
|
|
+ margin-top: 24px;
|
|
|
+ height: 52px; /* Taller button */
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 16px;
|
|
|
+ box-shadow: 0 4px 12px rgba(242, 147, 13, 0.3);
|
|
|
+
|
|
|
+ :deep(.van-button__text) {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+.text-link {
|
|
|
+ font-size: 14px;
|
|
|
font-weight: 500;
|
|
|
- }
|
|
|
+ color: #8a7960;
|
|
|
+ display: inline-block;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: var(--van-primary-color);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.third-party-login {
|
|
|
- margin-top: 30px;
|
|
|
- margin-bottom: 20px;
|
|
|
+/* Divider & Social */
|
|
|
+.divider-section {
|
|
|
+ position: relative;
|
|
|
+ margin: 32px 0;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
|
|
|
- .van-divider {
|
|
|
- font-size: 12px;
|
|
|
- color: #969799;
|
|
|
- margin: 20px 0;
|
|
|
- }
|
|
|
+.divider-line {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 1px;
|
|
|
+ background: #f1f1f1;
|
|
|
}
|
|
|
|
|
|
-.line-login-btn {
|
|
|
- height: 44px;
|
|
|
- font-size: 16px;
|
|
|
- background-color: #06c755;
|
|
|
- border-color: #06c755;
|
|
|
+.divider-text {
|
|
|
+ position: relative;
|
|
|
+ background: #fff;
|
|
|
+ padding: 0 16px;
|
|
|
+ color: #8a7960;
|
|
|
+ font-size: 14px;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
|
|
|
- :deep(.van-icon) {
|
|
|
- margin-right: 8px;
|
|
|
- }
|
|
|
+.social-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 24px;
|
|
|
}
|
|
|
|
|
|
-.env-tip {
|
|
|
- margin-bottom: 30px;
|
|
|
- text-align: center;
|
|
|
+.social-btn {
|
|
|
+ height: 56px;
|
|
|
+ width: 100%;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ padding: 0;
|
|
|
+
|
|
|
+ :deep(.van-button__content) {
|
|
|
+ width: 100%;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.line {
|
|
|
+ background: #06c755;
|
|
|
+ border-color: #06c755;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.agreement {
|
|
|
- font-size: 12px;
|
|
|
- color: #969799;
|
|
|
- text-align: center;
|
|
|
- margin-top: 20px;
|
|
|
+.footer-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #8a7960;
|
|
|
+}
|
|
|
|
|
|
- .link {
|
|
|
- color: #f9ae3d;
|
|
|
+.register-link {
|
|
|
+ color: var(--van-primary-color);
|
|
|
+ font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
- }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+/* Utilities */
|
|
|
+.text-center { text-align: center; }
|
|
|
+.mb-8 { margin-bottom: 32px; }
|
|
|
+.mt-4 { margin-top: 16px; }
|
|
|
+.pl-4 { padding-left: 16px; }
|
|
|
+.pt-4 { padding-top: 16px; }
|
|
|
+.pt-8 { padding-top: 32px; }
|
|
|
+.mr-1 { margin-right: 4px; }
|
|
|
</style>
|