login.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <script setup lang="ts">
  2. import {
  3. GoogleSignInButton,
  4. type CredentialResponse,
  5. } from 'vue3-google-signin'
  6. definePageMeta({
  7. layout: 'login',
  8. auth: false,
  9. })
  10. const router = useRouter()
  11. const route = useRoute()
  12. const { login, loginWithEmail, isAuthenticated } = useAuth()
  13. const config = useRuntimeConfig()
  14. const { locale } = useI18n()
  15. const isTestEnv = computed(() => config.public.env === 'development')
  16. const redirectPath = computed(() => {
  17. const redirect = route.query.redirect
  18. if (!redirect || Array.isArray(redirect)) {
  19. return '/'
  20. }
  21. // 避免跳转回登录页本身
  22. if (typeof redirect === 'string' && redirect.startsWith('/login')) {
  23. return '/'
  24. }
  25. return redirect as string
  26. })
  27. const showTestLogin = ref(false)
  28. const testEmail = ref('')
  29. const testLoginLoading = ref(false)
  30. const testLoginError = ref('')
  31. onMounted(() => {
  32. watch(isAuthenticated, (newIsAuthenticated) => {
  33. if (newIsAuthenticated) {
  34. router.replace(redirectPath.value)
  35. }
  36. }, {
  37. immediate: true,
  38. })
  39. })
  40. // Google SignIn
  41. const handleLoginSuccess = async (response: CredentialResponse) => {
  42. await login('google', response.credential || '')
  43. }
  44. const handleLoginError = () => {
  45. console.error('Login failed')
  46. }
  47. const openTestLogin = () => {
  48. testEmail.value = ''
  49. testLoginError.value = ''
  50. showTestLogin.value = true
  51. }
  52. const closeTestLogin = () => {
  53. if (testLoginLoading.value)
  54. return
  55. showTestLogin.value = false
  56. }
  57. const handleTestLogin = async () => {
  58. if (!testEmail.value) {
  59. testLoginError.value = 'Email is required'
  60. return
  61. }
  62. testLoginError.value = ''
  63. testLoginLoading.value = true
  64. const result = await loginWithEmail(testEmail.value.trim())
  65. testLoginLoading.value = false
  66. if (!result) {
  67. testLoginError.value = 'Login failed, please check email'
  68. return
  69. }
  70. showTestLogin.value = false
  71. }
  72. const handleTermsOfService = () => {
  73. router.push('/about/termsOfService?header=true')
  74. }
  75. const handlePrivacyPolicy = () => {
  76. router.push('/about/privacyPolicy?header=true')
  77. }
  78. </script>
  79. <template>
  80. <div class="login-container min-h-screen flex flex-col justify-end items-center px-8">
  81. <img
  82. src="~/assets/images/auth/logo.png"
  83. alt="logo"
  84. class="login-container__logo"
  85. >
  86. <div class="login-container__btn flex flex-col items-center">
  87. <GoogleSignInButton
  88. size="large"
  89. shape="pill"
  90. :locale="locale"
  91. theme="filled_black"
  92. @success="handleLoginSuccess"
  93. @error="handleLoginError"
  94. />
  95. <div
  96. v-if="isTestEnv"
  97. class="login-container__btn-test"
  98. @click="openTestLogin"
  99. >
  100. !!! TEST ENV LOGIN !!!
  101. </div>
  102. </div>
  103. <p class="login-footer text-[11px] font-normal text-center capitalize mb-15 mt-20">
  104. By continuing to use, you agree to our
  105. <a @click="handleTermsOfService">Terms of Service</a>
  106. and
  107. <a @click="handlePrivacyPolicy">Privacy Policy</a>
  108. </p>
  109. <div
  110. v-if="showTestLogin"
  111. class="login-test-modal"
  112. >
  113. <div
  114. class="login-test-modal__mask"
  115. @click="closeTestLogin"
  116. />
  117. <div class="login-test-modal__content">
  118. <h3 class="login-test-modal__title">
  119. Test Email Login
  120. </h3>
  121. <input
  122. v-model="testEmail"
  123. type="email"
  124. class="login-test-modal__input"
  125. placeholder="Enter test email"
  126. >
  127. <p
  128. v-if="testLoginError"
  129. class="login-test-modal__error"
  130. >
  131. {{ testLoginError }}
  132. </p>
  133. <div class="login-test-modal__actions">
  134. <button
  135. type="button"
  136. class="login-test-modal__btn login-test-modal__btn--cancel"
  137. @click="closeTestLogin"
  138. >
  139. Cancel
  140. </button>
  141. <button
  142. type="button"
  143. class="login-test-modal__btn login-test-modal__btn--confirm"
  144. :disabled="testLoginLoading"
  145. @click="handleTestLogin"
  146. >
  147. <span v-if="!testLoginLoading">Login</span>
  148. <span v-else>Logging in...</span>
  149. </button>
  150. </div>
  151. </div>
  152. </div>
  153. </div>
  154. </template>
  155. <style lang="scss" scoped>
  156. .login-container {
  157. @include bg('~/assets/images/auth/bg.png', 100vw auto);
  158. position: relative;
  159. &__logo {
  160. width: 113px;
  161. position: absolute;
  162. top: 20vh;
  163. }
  164. &__btn {
  165. gap: 16px;
  166. &-test {
  167. display: flex;
  168. cursor: pointer;
  169. height: 40px;
  170. padding: 8px 24px;
  171. justify-content: center;
  172. align-items: center;
  173. background: #1D2129;
  174. border-radius: 100px;
  175. color: #FFF;
  176. }
  177. }
  178. }
  179. .login-footer {
  180. color: #4E5969;
  181. a {
  182. color: #1D2129;
  183. text-decoration-line: underline;
  184. }
  185. }
  186. .login-test-modal {
  187. position: fixed;
  188. inset: 0;
  189. display: flex;
  190. align-items: center;
  191. justify-content: center;
  192. z-index: 1000;
  193. &__mask {
  194. position: absolute;
  195. inset: 0;
  196. background: rgba(0, 0, 0, 0.5);
  197. }
  198. &__content {
  199. position: relative;
  200. width: 80%;
  201. max-width: 360px;
  202. background: #111827;
  203. border-radius: 16px;
  204. padding: 20px 16px 16px;
  205. box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
  206. color: #fff;
  207. }
  208. &__title {
  209. margin-bottom: 12px;
  210. font-size: 16px;
  211. font-weight: 600;
  212. text-align: center;
  213. }
  214. &__input {
  215. width: 100%;
  216. padding: 10px 12px;
  217. border-radius: 8px;
  218. border: 1px solid #4B5563;
  219. background: #1F2937;
  220. color: #F9FAFB;
  221. font-size: 14px;
  222. outline: none;
  223. &::placeholder {
  224. color: #6B7280;
  225. }
  226. &:focus {
  227. border-color: #6366F1;
  228. }
  229. }
  230. &__error {
  231. margin-top: 8px;
  232. font-size: 12px;
  233. color: #FCA5A5;
  234. text-align: left;
  235. }
  236. &__actions {
  237. margin-top: 16px;
  238. display: flex;
  239. justify-content: flex-end;
  240. gap: 8px;
  241. }
  242. &__btn {
  243. min-width: 80px;
  244. height: 32px;
  245. border-radius: 999px;
  246. font-size: 14px;
  247. border: none;
  248. cursor: pointer;
  249. &--cancel {
  250. background: #374151;
  251. color: #E5E7EB;
  252. }
  253. &--confirm {
  254. background: #6366F1;
  255. color: #F9FAFB;
  256. &:disabled {
  257. opacity: 0.7;
  258. cursor: default;
  259. }
  260. }
  261. }
  262. }
  263. </style>