PasswordLogin.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <template>
  2. <div class="password-login">
  3. <!-- 区号选择和手机号输入(同一行) -->
  4. <div class="phone-input-group">
  5. <div class="area-code-field" @click="showAreaCodePicker = true">
  6. <div class="area-code-label">{{ $t('login.areaCode') }}</div>
  7. <div class="area-code-value">
  8. <span>+{{ areaCode }}</span>
  9. <van-icon name="arrow-down" size="12" />
  10. </div>
  11. </div>
  12. <div class="phone-field">
  13. <van-field
  14. v-model="mobile"
  15. type="tel"
  16. :label="$t('login.phone')"
  17. :placeholder="$t('login.enterPhone')"
  18. clearable
  19. />
  20. </div>
  21. </div>
  22. <!-- 密码输入 -->
  23. <div class="input-group">
  24. <van-field
  25. v-model="password"
  26. :type="showPassword ? 'text' : 'password'"
  27. :label="$t('login.password')"
  28. :placeholder="$t('login.enterPassword')"
  29. clearable
  30. >
  31. <template #button>
  32. <van-icon
  33. :name="showPassword ? 'eye-o' : 'closed-eye'"
  34. @click="showPassword = !showPassword"
  35. />
  36. </template>
  37. </van-field>
  38. </div>
  39. <!-- 忘记密码 -->
  40. <div class="forgot-password">
  41. <span @click="handleForgotPassword">{{ $t('login.forgotPassword') }}</span>
  42. </div>
  43. <!-- 登录按钮 -->
  44. <van-button
  45. type="primary"
  46. round
  47. block
  48. :loading="logging"
  49. @click="handleLogin"
  50. class="login-btn"
  51. >
  52. {{ $t('login.loginNow') }}
  53. </van-button>
  54. <!-- 注册提示 -->
  55. <div class="register-tip">
  56. <span>{{ $t('login.noAccount') }}</span>
  57. <span class="link" @click="handleRegister">{{ $t('login.registerNow') }}</span>
  58. </div>
  59. <!-- 区号选择弹窗 -->
  60. <van-popup v-model:show="showAreaCodePicker" position="bottom" round>
  61. <van-picker
  62. :title="$t('login.selectAreaCode')"
  63. :columns="areaCodeOptions"
  64. @confirm="onAreaCodeConfirm"
  65. @cancel="showAreaCodePicker = false"
  66. />
  67. </van-popup>
  68. </div>
  69. </template>
  70. <script setup lang="ts">
  71. import { ref, computed } from 'vue'
  72. import { useI18n } from 'vue-i18n'
  73. import { showToast } from 'vant'
  74. import { login as apiLogin } from '@/api/auth'
  75. const { t } = useI18n()
  76. // Props
  77. interface Props {
  78. agreed: boolean
  79. }
  80. const props = defineProps<Props>()
  81. // Emits
  82. const emit = defineEmits<{
  83. (e: 'update:agreed', value: boolean): void
  84. (e: 'login-success', data: any): void
  85. }>()
  86. // 区号选项
  87. const areaCodeOptions = ref([
  88. { text: '日本 +81', value: '81' },
  89. { text: '中国 +86', value: '86' },
  90. { text: '韓国 +82', value: '82' }
  91. ])
  92. // 表单数据
  93. const areaCode = ref('81')
  94. const areaCodeDisplay = ref('+81')
  95. const mobile = ref('')
  96. const password = ref('')
  97. // UI状态
  98. const showAreaCodePicker = ref(false)
  99. const showPassword = ref(false)
  100. const logging = ref(false)
  101. // 验证手机号
  102. const validatePhone = (phone: string) => {
  103. const code = areaCode.value
  104. if (code === '81') {
  105. const cleanPhone = phone.replace(/^0+/, '')
  106. return /^[1-9]\d{8,9}$/.test(cleanPhone)
  107. } else if (code === '86') {
  108. return /^1[3-9]\d{9}$/.test(phone)
  109. }
  110. return phone.length >= 5
  111. }
  112. // 区号选择确认
  113. const onAreaCodeConfirm = ({ selectedValues }: any) => {
  114. areaCode.value = selectedValues[0]
  115. areaCodeDisplay.value = `+${selectedValues[0]}`
  116. showAreaCodePicker.value = false
  117. }
  118. // 密码登录
  119. const handleLogin = async () => {
  120. if (!mobile.value) {
  121. showToast(t('login.enterPhone'))
  122. return
  123. }
  124. if (!validatePhone(mobile.value)) {
  125. showToast(t('login.invalidPhone'))
  126. return
  127. }
  128. if (!password.value) {
  129. showToast(t('login.enterPassword'))
  130. return
  131. }
  132. if (password.value.length < 6) {
  133. showToast(t('login.passwordLength'))
  134. return
  135. }
  136. if (!props.agreed) {
  137. showToast(t('login.checkAgreement'))
  138. return
  139. }
  140. try {
  141. logging.value = true
  142. const phoneValue = areaCode.value === '81'
  143. ? mobile.value.replace(/^0+/, '')
  144. : mobile.value
  145. const fullPhone = areaCode.value + phoneValue
  146. const res = await apiLogin({
  147. mobile: fullPhone,
  148. password: password.value,
  149. from: 'h5'
  150. })
  151. if (res) {
  152. emit('login-success', res)
  153. }
  154. } catch (error) {
  155. console.error('登录失败:', error)
  156. } finally {
  157. logging.value = false
  158. }
  159. }
  160. // 忘记密码
  161. const handleForgotPassword = () => {
  162. showToast(t('common.featureInDevelopment'))
  163. }
  164. // 注册
  165. const handleRegister = () => {
  166. showToast(t('common.featureInDevelopment'))
  167. }
  168. </script>
  169. <style scoped lang="scss">
  170. .password-login {
  171. .phone-input-group {
  172. display: flex;
  173. align-items: flex-end;
  174. gap: 12px;
  175. margin-bottom: 20px;
  176. }
  177. .area-code-field {
  178. width: 100px;
  179. flex-shrink: 0;
  180. padding: 10px 16px;
  181. background: #f7f8fa;
  182. border-radius: 4px;
  183. cursor: pointer;
  184. transition: background 0.3s;
  185. &:active {
  186. background: #ebedf0;
  187. }
  188. .area-code-label {
  189. font-size: 12px;
  190. color: #646566;
  191. margin-bottom: 4px;
  192. }
  193. .area-code-value {
  194. display: flex;
  195. align-items: center;
  196. justify-content: space-between;
  197. font-size: 14px;
  198. color: #323233;
  199. font-weight: 500;
  200. }
  201. }
  202. .phone-field {
  203. flex: 1;
  204. :deep(.van-cell) {
  205. padding: 10px 16px;
  206. }
  207. }
  208. .input-group {
  209. margin-bottom: 20px;
  210. }
  211. .forgot-password {
  212. text-align: right;
  213. margin-bottom: 30px;
  214. padding-right: 16px;
  215. span {
  216. font-size: 12px;
  217. color: #09b4f1;
  218. cursor: pointer;
  219. }
  220. }
  221. .login-btn {
  222. height: 44px;
  223. font-size: 16px;
  224. background-color: #09b4f1;
  225. border-color: #09b4f1;
  226. }
  227. .register-tip {
  228. margin-top: 20px;
  229. text-align: center;
  230. font-size: 12px;
  231. color: #969799;
  232. .link {
  233. color: #09b4f1;
  234. margin-left: 8px;
  235. cursor: pointer;
  236. }
  237. }
  238. }
  239. </style>