login.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. <template>
  2. <view class="login-container">
  3. <!-- 语言切换按钮 -->
  4. <view class="language-switcher" @click="switchLanguage">
  5. <text class="language-icon">🌐</text>
  6. <text class="language-text">{{ currentLanguageName }}</text>
  7. </view>
  8. <view class="login-box">
  9. <view class="logo-section">
  10. <text class="app-title">{{ $t('login.appTitle') }}</text>
  11. <text class="app-subtitle">{{ $t('login.welcome') }}</text>
  12. </view>
  13. <view class="form-section">
  14. <view class="form-item">
  15. <view class="input-wrapper">
  16. <text class="input-icon">📱</text>
  17. <input
  18. v-model="phone"
  19. type="text"
  20. maxlength="11"
  21. :placeholder="$t('login.phonePlaceholder')"
  22. class="input-field"
  23. @input="onPhoneInput"
  24. />
  25. </view>
  26. </view>
  27. <view class="form-item">
  28. <view class="input-wrapper">
  29. <text class="input-icon">🔒</text>
  30. <input
  31. v-model="password"
  32. :password="!showPassword"
  33. :placeholder="$t('login.passwordPlaceholder')"
  34. class="input-field"
  35. />
  36. <text
  37. class="toggle-password"
  38. @click="togglePassword"
  39. >
  40. {{ showPassword ? '👁' : '👁‍🗨' }}
  41. </text>
  42. </view>
  43. </view>
  44. <button
  45. class="login-btn"
  46. @click="handleLogin"
  47. >
  48. {{ $t('login.loginButton') }}
  49. </button>
  50. </view>
  51. </view>
  52. </view>
  53. </template>
  54. <script setup>
  55. import { ref, computed, watch } from 'vue'
  56. import { onShow } from '@dcloudio/uni-app'
  57. import { useI18n } from 'vue-i18n'
  58. import { useUserStore } from '@/store/index'
  59. import { useLocaleStore } from '@/store/locale'
  60. import { login } from '@/apis/auth'
  61. const { t, locale } = useI18n()
  62. const userStore = useUserStore()
  63. const localeStore = useLocaleStore()
  64. const phone = ref('')
  65. const password = ref('')
  66. const showPassword = ref(false)
  67. // 当前语言名称
  68. const currentLanguageName = computed(() => {
  69. return localeStore.getCurrentLocaleName()
  70. })
  71. // 设置页面标题
  72. const setPageTitle = () => {
  73. uni.setNavigationBarTitle({
  74. title: t('login.title')
  75. })
  76. }
  77. // 页面显示时设置标题
  78. onShow(() => {
  79. setPageTitle()
  80. })
  81. // 监听语言变化,更新标题
  82. watch(locale, () => {
  83. setPageTitle()
  84. })
  85. // 切换语言
  86. const switchLanguage = () => {
  87. localeStore.toggleLocale()
  88. uni.showToast({
  89. title: t('common.language.switchSuccess'),
  90. icon: 'success',
  91. duration: 1500
  92. })
  93. }
  94. // 验证手机号格式
  95. const isValidPhone = computed(() => {
  96. return /^1[3-9]\d{9}$/.test(phone.value)
  97. })
  98. // 是否可以提交
  99. const canSubmit = computed(() => {
  100. return isValidPhone.value && password.value.length >= 6
  101. })
  102. // 手机号输入处理
  103. const onPhoneInput = (e) => {
  104. // 限制只能输入数字
  105. phone.value = e.detail.value.replace(/[^\d]/g, '')
  106. }
  107. // 切换密码显示
  108. const togglePassword = () => {
  109. showPassword.value = !showPassword.value
  110. }
  111. // 登录处理
  112. const handleLogin = async () => {
  113. // 验证手机号
  114. if (!isValidPhone.value) {
  115. uni.showToast({
  116. title: t('login.phoneError'),
  117. icon: 'none',
  118. duration: 2000
  119. })
  120. return
  121. }
  122. // 验证密码
  123. if (password.value.length < 6) {
  124. uni.showToast({
  125. title: t('login.passwordError'),
  126. icon: 'none',
  127. duration: 2000
  128. })
  129. return
  130. }
  131. try {
  132. uni.showLoading({
  133. title: t('login.loggingIn'),
  134. mask: true
  135. })
  136. const res = await login({
  137. phoneNumber: phone.value,
  138. password: password.value
  139. })
  140. // 保存 token
  141. userStore.setToken(res.data.token)
  142. uni.hideLoading()
  143. uni.showToast({
  144. title: t('login.loginSuccess'),
  145. icon: 'success',
  146. duration: 1500
  147. })
  148. // 延迟跳转到首页
  149. setTimeout(() => {
  150. uni.switchTab({
  151. url: '/pages/index/index'
  152. })
  153. }, 1500)
  154. } catch (error) {
  155. uni.hideLoading()
  156. console.error('登录失败:', error)
  157. }
  158. }
  159. </script>
  160. <style lang="scss" scoped>
  161. .login-container {
  162. min-height: 100vh;
  163. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  164. display: flex;
  165. flex-direction: column;
  166. align-items: center;
  167. justify-content: flex-start;
  168. padding: 40rpx;
  169. padding-top: 270rpx;
  170. position: relative;
  171. }
  172. .language-switcher {
  173. align-self: flex-end;
  174. margin-bottom: 32rpx;
  175. margin-right: 40rpx;
  176. display: flex;
  177. align-items: center;
  178. background: rgba(255, 255, 255, 0.25);
  179. backdrop-filter: blur(10rpx);
  180. border-radius: 40rpx;
  181. padding: 16rpx 28rpx;
  182. cursor: pointer;
  183. transition: all 0.3s;
  184. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
  185. &:active {
  186. transform: scale(0.95);
  187. background: rgba(255, 255, 255, 0.35);
  188. }
  189. }
  190. .login-box {
  191. width: 100%;
  192. max-width: 600rpx;
  193. background: #ffffff;
  194. border-radius: 32rpx;
  195. padding: 60rpx 40rpx;
  196. box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
  197. }
  198. .language-icon {
  199. font-size: 36rpx;
  200. margin-right: 8rpx;
  201. }
  202. .language-text {
  203. font-size: 28rpx;
  204. color: #ffffff;
  205. font-weight: 500;
  206. }
  207. .logo-section {
  208. text-align: center;
  209. margin-bottom: 80rpx;
  210. }
  211. .app-title {
  212. display: block;
  213. font-size: 48rpx;
  214. font-weight: bold;
  215. color: #333;
  216. margin-bottom: 16rpx;
  217. }
  218. .app-subtitle {
  219. display: block;
  220. font-size: 28rpx;
  221. color: #999;
  222. }
  223. .form-section {
  224. width: 100%;
  225. }
  226. .form-item {
  227. margin-bottom: 32rpx;
  228. }
  229. .input-wrapper {
  230. display: flex;
  231. align-items: center;
  232. background: #f5f5f5;
  233. border-radius: 16rpx;
  234. padding: 0 24rpx;
  235. height: 96rpx;
  236. transition: all 0.3s;
  237. border: 2rpx solid transparent;
  238. &:focus-within {
  239. background: #fff;
  240. border-color: #667eea;
  241. box-shadow: 0 0 0 4rpx rgba(102, 126, 234, 0.1);
  242. }
  243. }
  244. .input-icon {
  245. font-size: 40rpx;
  246. margin-right: 16rpx;
  247. }
  248. .input-field {
  249. flex: 1;
  250. font-size: 32rpx;
  251. color: #333;
  252. &::placeholder {
  253. color: #999;
  254. }
  255. }
  256. .toggle-password {
  257. font-size: 40rpx;
  258. cursor: pointer;
  259. padding: 8rpx;
  260. }
  261. .login-btn {
  262. width: 100%;
  263. height: 96rpx;
  264. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  265. color: #ffffff;
  266. font-size: 32rpx;
  267. font-weight: bold;
  268. border-radius: 16rpx;
  269. border: none;
  270. margin-top: 48rpx;
  271. transition: all 0.3s;
  272. &:active {
  273. opacity: 0.8;
  274. transform: scale(0.98);
  275. }
  276. }
  277. .login-btn-disabled {
  278. background: #ccc;
  279. opacity: 0.6;
  280. &:active {
  281. transform: none;
  282. }
  283. }
  284. </style>