login.vue 13 KB

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