| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- <template>
- <div class="password-login">
- <!-- 区号选择和手机号输入(同一行) -->
- <div class="phone-input-group">
- <div class="area-code-field" @click="showAreaCodePicker = true">
- <div class="area-code-label">{{ $t('login.areaCode') }}</div>
- <div class="area-code-value">
- <span>+{{ areaCode }}</span>
- <van-icon name="arrow-down" size="12" />
- </div>
- </div>
- <div class="phone-field">
- <van-field
- v-model="mobile"
- type="tel"
- :label="$t('login.phone')"
- :placeholder="$t('login.enterPhone')"
- clearable
- />
- </div>
- </div>
- <!-- 密码输入 -->
- <div class="input-group">
- <van-field
- v-model="password"
- :type="showPassword ? 'text' : 'password'"
- :label="$t('login.password')"
- :placeholder="$t('login.enterPassword')"
- clearable
- >
- <template #button>
- <van-icon
- :name="showPassword ? 'eye-o' : 'closed-eye'"
- @click="showPassword = !showPassword"
- />
- </template>
- </van-field>
- </div>
- <!-- 忘记密码 -->
- <div class="forgot-password">
- <span @click="handleForgotPassword">{{ $t('login.forgotPassword') }}</span>
- </div>
- <!-- 登录按钮 -->
- <van-button
- type="primary"
- round
- block
- :loading="logging"
- @click="handleLogin"
- class="login-btn"
- >
- {{ $t('login.loginNow') }}
- </van-button>
- <!-- 注册提示 -->
- <div class="register-tip">
- <span>{{ $t('login.noAccount') }}</span>
- <span class="link" @click="handleRegister">{{ $t('login.registerNow') }}</span>
- </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 } from 'vue'
- import { useI18n } from 'vue-i18n'
- import { showToast } from 'vant'
- import { login as apiLogin } from '@/api/auth'
- const { t } = useI18n()
- // Props
- interface Props {
- agreed: boolean
- }
- const props = defineProps<Props>()
- // Emits
- const emit = defineEmits<{
- (e: 'update:agreed', value: boolean): void
- (e: 'login-success', data: any): void
- }>()
- // 区号选项
- const areaCodeOptions = ref([
- { text: '日本 +81', value: '81' },
- { text: '中国 +86', value: '86' },
- { text: '韓国 +82', value: '82' }
- ])
- // 表单数据
- const areaCode = ref('81')
- const areaCodeDisplay = ref('+81')
- const mobile = ref('')
- const password = ref('')
- // UI状态
- const showAreaCodePicker = ref(false)
- const showPassword = ref(false)
- const logging = ref(false)
- // 验证手机号
- const validatePhone = (phone: string) => {
- 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 }: any) => {
- areaCode.value = selectedValues[0]
- areaCodeDisplay.value = `+${selectedValues[0]}`
- showAreaCodePicker.value = false
- }
- // 密码登录
- const handleLogin = async () => {
- if (!mobile.value) {
- showToast(t('login.enterPhone'))
- return
- }
- if (!validatePhone(mobile.value)) {
- showToast(t('login.invalidPhone'))
- return
- }
- if (!password.value) {
- showToast(t('login.enterPassword'))
- return
- }
- if (password.value.length < 6) {
- showToast(t('login.passwordLength'))
- return
- }
- if (!props.agreed) {
- showToast(t('login.checkAgreement'))
- return
- }
- try {
- logging.value = true
- const phoneValue = areaCode.value === '81'
- ? mobile.value.replace(/^0+/, '')
- : mobile.value
- const fullPhone = areaCode.value + phoneValue
- const res = await apiLogin({
- mobile: fullPhone,
- password: password.value,
- from: 'h5'
- })
- if (res) {
- emit('login-success', res)
- }
- } catch (error) {
- console.error('登录失败:', error)
- } finally {
- logging.value = false
- }
- }
- // 忘记密码
- const handleForgotPassword = () => {
- showToast(t('common.featureInDevelopment'))
- }
- // 注册
- const handleRegister = () => {
- showToast(t('common.featureInDevelopment'))
- }
- </script>
- <style scoped lang="scss">
- .password-login {
- .phone-input-group {
- display: flex;
- align-items: flex-end;
- gap: 12px;
- margin-bottom: 20px;
- }
- .area-code-field {
- width: 100px;
- flex-shrink: 0;
- padding: 10px 16px;
- background: #f7f8fa;
- border-radius: 4px;
- cursor: pointer;
- transition: background 0.3s;
- &:active {
- background: #ebedf0;
- }
- .area-code-label {
- font-size: 12px;
- color: #646566;
- margin-bottom: 4px;
- }
- .area-code-value {
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 14px;
- color: #323233;
- font-weight: 500;
- }
- }
- .phone-field {
- flex: 1;
- :deep(.van-cell) {
- padding: 10px 16px;
- }
- }
- .input-group {
- margin-bottom: 20px;
- }
- .forgot-password {
- text-align: right;
- margin-bottom: 30px;
- padding-right: 16px;
- span {
- font-size: 12px;
- color: #09b4f1;
- cursor: pointer;
- }
- }
- .login-btn {
- height: 44px;
- font-size: 16px;
- background-color: #09b4f1;
- border-color: #09b4f1;
- }
- .register-tip {
- margin-top: 20px;
- text-align: center;
- font-size: 12px;
- color: #969799;
- .link {
- color: #09b4f1;
- margin-left: 8px;
- cursor: pointer;
- }
- }
- }
- </style>
|