login.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <script setup lang="ts">
  2. import type { VbenFormSchema } from '@vben/common-ui';
  3. import { computed, reactive, ref, watch } from 'vue';
  4. import { useVbenForm, useVbenModal, VbenButton, z } from '@vben/common-ui';
  5. import { useAccessStore, useUserStore } from '@vben/stores';
  6. import { $t } from '@/locales';
  7. import { message } from 'antdv-next';
  8. import MD5 from 'crypto-js/md5';
  9. import {
  10. createAccountApi,
  11. getUserInfoApi,
  12. loginApi,
  13. sendSmsCodeApi,
  14. } from '#/api';
  15. import CaptchaInput from './CaptchaInput.vue';
  16. defineOptions({
  17. name: 'LoginComponent',
  18. });
  19. const loading = ref(false);
  20. const [Modal, modalApi] = useVbenModal();
  21. const isRegister = ref(false);
  22. const accessStore = useAccessStore();
  23. const userStore = useUserStore();
  24. const countdown = ref(0);
  25. const isSending = ref(false);
  26. const REMEMBER_ME_KEY = `REMEMBER_ME_USERNAME_${location.hostname}`;
  27. const localUsername = localStorage.getItem(REMEMBER_ME_KEY) || '';
  28. const rememberMe = ref(!!localUsername);
  29. const open = defineModel<boolean>('open', { default: false });
  30. const captchaRandomKey = ref('');
  31. const loginFormSchema = computed((): VbenFormSchema[] => {
  32. return [
  33. {
  34. component: 'VbenInput',
  35. componentProps: {
  36. placeholder: '',
  37. },
  38. fieldName: 'username',
  39. label: $t('auth.loginName'),
  40. rules: z
  41. .string()
  42. .min(1, { message: $t('auth.validation.enterUsername') }),
  43. defaultValue: localUsername,
  44. },
  45. {
  46. component: 'VbenInputPassword',
  47. componentProps: {
  48. placeholder: '',
  49. },
  50. fieldName: 'password',
  51. label: $t('auth.password'),
  52. rules: z
  53. .string()
  54. .min(1, { message: $t('auth.validation.enterPassword') }),
  55. },
  56. {
  57. component: 'VbenInput',
  58. componentProps: {
  59. placeholder: '',
  60. },
  61. fieldName: 'captcha',
  62. label: $t('auth.verificationCode'),
  63. rules: z
  64. .string()
  65. .min(1, { message: $t('auth.validation.enterVerificationCode') }),
  66. formItemClass: 'captcha-form-item',
  67. },
  68. ];
  69. });
  70. const registerFormSchema = computed((): VbenFormSchema[] => {
  71. return [
  72. {
  73. component: 'VbenInput',
  74. componentProps: {
  75. placeholder: '',
  76. },
  77. fieldName: 'mobile',
  78. label: $t('auth.mobileNumber'),
  79. rules: z
  80. .string()
  81. .min(1, { message: $t('auth.validation.enterMobileNumber') })
  82. .regex(/^1[3-9]\d{9}$/, {
  83. message: $t('auth.validation.enterCorrectMobileNumber'),
  84. }),
  85. },
  86. {
  87. component: 'VbenInput',
  88. componentProps: {
  89. placeholder: '',
  90. },
  91. fieldName: 'code',
  92. label: $t('auth.verificationCode'),
  93. rules: z
  94. .string()
  95. .min(1, { message: $t('auth.validation.enterVerificationCode') }),
  96. formItemClass: 'verification-form-item',
  97. },
  98. {
  99. component: 'VbenInput',
  100. componentProps: {
  101. placeholder: '',
  102. },
  103. fieldName: 'accountName',
  104. label: $t('auth.accountName'),
  105. rules: z
  106. .string()
  107. .min(1, { message: $t('auth.validation.enterAccountName') }),
  108. },
  109. {
  110. component: 'VbenInputPassword',
  111. componentProps: {
  112. placeholder: '',
  113. },
  114. fieldName: 'password',
  115. label: $t('auth.password'),
  116. rules: z
  117. .string()
  118. .min(1, { message: $t('auth.validation.enterPassword') }),
  119. },
  120. {
  121. component: 'VbenCheckbox',
  122. componentProps: {
  123. placeholder: '',
  124. },
  125. fieldName: 'agreeTerms',
  126. label: '',
  127. rules: z.boolean().refine((val) => val === true, {
  128. message: $t('auth.validation.agreeTerms'),
  129. }),
  130. formItemClass: 'agree-terms-form-item',
  131. },
  132. ];
  133. });
  134. const [LoginForm, loginFormApi] = useVbenForm(
  135. reactive({
  136. commonConfig: {
  137. hideLabel: false,
  138. hideRequiredMark: true,
  139. formItemClass: 'pb-[20px]',
  140. },
  141. layout: 'vertical',
  142. schema: loginFormSchema,
  143. showDefaultActions: false,
  144. wrapperClass: 'text-[12px]',
  145. }),
  146. );
  147. const [RegisterForm, registerFormApi] = useVbenForm(
  148. reactive({
  149. commonConfig: {
  150. hideLabel: false,
  151. hideRequiredMark: true,
  152. formItemClass: 'pb-[15px]',
  153. },
  154. layout: 'vertical',
  155. schema: registerFormSchema,
  156. showDefaultActions: false,
  157. wrapperClass: 'text-[12px]',
  158. }),
  159. );
  160. watch(
  161. () => open.value,
  162. (val) => {
  163. modalApi.setState({ isOpen: val });
  164. isRegister.value = false;
  165. },
  166. { immediate: true },
  167. );
  168. function handleClose() {
  169. open.value = false;
  170. }
  171. async function handleLoginSubmit() {
  172. const { valid } = await loginFormApi.validate();
  173. const values = await loginFormApi.getValues();
  174. if (valid) {
  175. localStorage.setItem(
  176. REMEMBER_ME_KEY,
  177. rememberMe.value ? values?.username : '',
  178. );
  179. try {
  180. loading.value = true;
  181. const encryptedPassword = MD5(values?.password || '').toString();
  182. const loginRes = await loginApi({
  183. account: values?.username,
  184. pwd: encryptedPassword,
  185. check_code: values?.captcha,
  186. check_key: captchaRandomKey.value,
  187. });
  188. if (loginRes && loginRes.isSuccess) {
  189. if (loginRes.token) {
  190. accessStore.setAccessToken(loginRes.token);
  191. }
  192. open.value = false;
  193. message.success($t('auth.message.loginSuccess'));
  194. const userInfoRes = await getUserInfoApi();
  195. if (userInfoRes && userInfoRes.isSuccess) {
  196. const userInfo = {
  197. account:
  198. userInfoRes.result?.account ||
  199. userInfoRes.result?.englishName ||
  200. '',
  201. avatar: userInfoRes.result?.avatarFileId || '',
  202. cellPhone: userInfoRes.result?.cellPhone || '',
  203. realName:
  204. userInfoRes.result?.chineseName || userInfoRes.result?.name || '',
  205. email: userInfoRes.result?.emailAddress || '',
  206. roles: [],
  207. userId: userInfoRes.result?.id || '',
  208. username: userInfoRes.result?.name || '',
  209. };
  210. userStore.setUserInfo(userInfo);
  211. }
  212. } else {
  213. message.error(loginRes?.error || $t('auth.message.loginFailed'));
  214. }
  215. } finally {
  216. loading.value = false;
  217. }
  218. }
  219. }
  220. async function handleSendCode() {
  221. const values = await registerFormApi.getValues();
  222. const mobile = values?.mobile;
  223. if (!mobile) {
  224. message.error($t('auth.message.enterMobileNumberFirst'));
  225. return;
  226. }
  227. const mobileRegex = /^1[3-9]\d{9}$/;
  228. if (!mobileRegex.test(mobile)) {
  229. message.error($t('auth.validation.enterCorrectMobileNumber'));
  230. return;
  231. }
  232. try {
  233. isSending.value = true;
  234. await sendSmsCodeApi({
  235. sms_mobile: mobile,
  236. sms_scene: 'sms_reg',
  237. });
  238. countdown.value = 60;
  239. const timer = setInterval(() => {
  240. countdown.value--;
  241. if (countdown.value <= 0) {
  242. clearInterval(timer);
  243. isSending.value = false;
  244. }
  245. }, 1000);
  246. message.success($t('auth.message.verificationCodeSent'));
  247. } catch {
  248. isSending.value = false;
  249. message.error($t('auth.message.sendVerificationCodeFailed'));
  250. }
  251. }
  252. async function handleRegisterSubmit() {
  253. const { valid } = await registerFormApi.validate();
  254. const values = await registerFormApi.getValues();
  255. if (valid) {
  256. try {
  257. loading.value = true;
  258. const encryptedPassword = MD5(values?.password || '').toString();
  259. const registerRes = await createAccountApi({
  260. sms_scene: 'sms_reg',
  261. sms_mobile: values?.mobile,
  262. sms_check_code: values?.code,
  263. user: {
  264. langNameList: [
  265. {
  266. name: 'zh-CN',
  267. value: values?.accountName || '',
  268. },
  269. {
  270. name: 'en',
  271. value: '',
  272. },
  273. ],
  274. password: encryptedPassword,
  275. },
  276. });
  277. if (registerRes && registerRes.isSuccess) {
  278. message.success($t('auth.message.registerSuccess'));
  279. isRegister.value = false;
  280. } else {
  281. message.error(registerRes?.error || $t('auth.message.registerFailed'));
  282. }
  283. } finally {
  284. loading.value = false;
  285. }
  286. }
  287. }
  288. function handleGoToRegister() {
  289. isRegister.value = true;
  290. }
  291. function handleGoToLogin() {
  292. isRegister.value = false;
  293. }
  294. defineExpose({
  295. getFormApi: () => loginFormApi,
  296. });
  297. </script>
  298. <template>
  299. <div>
  300. <Modal
  301. id="loginModal"
  302. :bordered="false"
  303. :closable="false"
  304. :close-on-click-modal="false"
  305. :close-on-press-escape="false"
  306. :footer="false"
  307. :fullscreen-button="false"
  308. :header="false"
  309. class="relative sm:w-[940px]"
  310. content-class="p-0"
  311. @update:open="handleClose"
  312. >
  313. <div class="relative flex">
  314. <div class="hidden w-[470px] sm:flex">
  315. <img
  316. alt="login"
  317. class="w-[470px] object-contain"
  318. src="@/assets/image/login-banner.png"
  319. />
  320. </div>
  321. <div class="w-full px-[86px] py-[30px] sm:w-1/2">
  322. <div v-if="!isRegister" class="mb-6">
  323. <h2 class="text-[21px] font-bold">{{ $t('auth.login') }}</h2>
  324. <p class="text-muted-foreground mt-3 text-sm">
  325. {{ $t('auth.newUser') }}
  326. <span
  327. class="cursor-pointer text-[#8B0046]"
  328. @click="handleGoToRegister"
  329. >
  330. {{ $t('auth.createAccount') }}
  331. </span>
  332. </p>
  333. </div>
  334. <div v-if="isRegister" class="mb-6">
  335. <h2 class="text-[21px] font-bold">
  336. {{ $t('auth.createAccount') }}
  337. </h2>
  338. <p class="text-muted-foreground mt-3 text-sm">
  339. {{ $t('auth.alreadyHaveAccount') }}
  340. <span
  341. class="cursor-pointer text-[#8B0046]"
  342. @click="handleGoToLogin"
  343. >
  344. {{ $t('auth.login') }}
  345. </span>
  346. </p>
  347. </div>
  348. <div class="my-[20px] h-[1px] w-auto bg-[#E5E5E5]"></div>
  349. <LoginForm v-if="!isRegister">
  350. <template #captcha="{ field }">
  351. <CaptchaInput
  352. :model-value="field.value"
  353. @update:model-value="
  354. (val) => {
  355. loginFormApi.setFieldValue('captcha', val);
  356. }
  357. "
  358. @update:random-key="
  359. (val) => {
  360. captchaRandomKey = val;
  361. }
  362. "
  363. />
  364. </template>
  365. </LoginForm>
  366. <RegisterForm v-if="isRegister">
  367. <template #code="{ field }">
  368. <div class="flex w-full items-center justify-around gap-2">
  369. <input
  370. v-model="field.value"
  371. class="h-[38px] flex-1 rounded-md border border-gray-300 px-3 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#8B0046]"
  372. placeholder=""
  373. type="text"
  374. @input="
  375. () => {
  376. registerFormApi.setFieldValue('code', field.value);
  377. }
  378. "
  379. />
  380. <VbenButton
  381. :disabled="countdown > 0"
  382. :loading="isSending"
  383. class="h-[38px] whitespace-nowrap rounded-md bg-gradient-to-b from-[#8B0046] to-[#460023] px-4 text-white"
  384. @click="handleSendCode"
  385. >
  386. {{ countdown > 0 ? `${countdown}s` : $t('auth.sendCode') }}
  387. </VbenButton>
  388. </div>
  389. </template>
  390. <template #agreeTerms="{ field }">
  391. <label class="flex cursor-pointer items-center gap-2">
  392. <input
  393. v-model="field.value"
  394. class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600"
  395. type="checkbox"
  396. @change="
  397. () => {
  398. registerFormApi.setFieldValue('agreeTerms', field.value);
  399. }
  400. "
  401. />
  402. <span class="text-sm">{{ $t('auth.agreeTerms') }}</span>
  403. </label>
  404. </template>
  405. </RegisterForm>
  406. <VbenButton
  407. v-if="!isRegister"
  408. :class="{ 'cursor-wait': loading }"
  409. :loading="loading"
  410. aria-label="login"
  411. class="m-auto mt-[20px] flex h-[50px] w-full cursor-pointer items-center justify-center rounded-[25px] bg-gradient-to-b from-[#8B0046] to-[#460023] text-[16px] text-white"
  412. @click="handleLoginSubmit"
  413. >
  414. {{ $t('auth.loginButton') }}
  415. </VbenButton>
  416. <VbenButton
  417. v-if="isRegister"
  418. :class="{ 'cursor-wait': loading }"
  419. :loading="loading"
  420. aria-label="register"
  421. class="m-auto mt-[20px] flex h-[50px] w-full cursor-pointer items-center justify-center rounded-[25px] bg-gradient-to-b from-[#8B0046] to-[#460023] text-[16px] text-white"
  422. @click="handleRegisterSubmit"
  423. >
  424. {{ $t('auth.createAccountButton') }}
  425. </VbenButton>
  426. </div>
  427. <div class="absolute right-[20px] top-[24px] h-[44px] w-[59.12px]">
  428. <img alt="" class="" src="@/assets/image/system-logo.png" />
  429. </div>
  430. </div>
  431. <div
  432. class="fixed right-[-80px] top-[0] flex h-[34px] w-[34px] cursor-pointer items-center justify-center rounded-[50%] bg-[#fff] text-[#000] transition-opacity hover:opacity-80"
  433. @click="handleClose"
  434. >
  435. X
  436. </div>
  437. </Modal>
  438. </div>
  439. </template>