login.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. <template>
  2. <div class="login-container">
  3. <!-- 系统头部 -->
  4. <s-header
  5. :name="
  6. sys
  7. ? sys.title
  8. : sysTitle == 'AETI GLOBAL'
  9. ? sysTitle
  10. : $t('public.sysName')
  11. "
  12. :noback="true"
  13. />
  14. <!-- 主要内容区域 -->
  15. <div class="login-content">
  16. <!-- Logo区域 -->
  17. <div class="logo-section">
  18. <div class="logo-container">
  19. <img :src="logoName" alt="System Logo" class="system-logo" />
  20. </div>
  21. </div>
  22. <!-- 登录表单 -->
  23. <div class="form-container">
  24. <!-- 登录方式切换 -->
  25. <div class="login-type-tabs">
  26. <div
  27. class="tab-item"
  28. :class="{ active: loginType === 'password' }"
  29. @click="switchLoginType('password')"
  30. >
  31. {{ $t('login.passwordLogin') }}
  32. </div>
  33. <div
  34. class="tab-item"
  35. :class="{ active: loginType === 'sms' }"
  36. @click="switchLoginType('sms')"
  37. >
  38. {{ $t('login.smsLogin') }}
  39. </div>
  40. </div>
  41. <van-form @submit="onSubmit" class="login-form">
  42. <!-- 密码登录 -->
  43. <div v-if="loginType === 'password'">
  44. <!-- 用户名输入 -->
  45. <van-field
  46. v-model="userName"
  47. name="userName"
  48. :placeholder="$t('login.userNameInput')"
  49. :rules="[{ required: true, message: $t('login.userNameInput') }]"
  50. class="form-field"
  51. >
  52. <template #left-icon>
  53. <van-icon name="user-o" size="22" class="field-icon" />
  54. </template>
  55. </van-field>
  56. <!-- 密码输入 -->
  57. <van-field
  58. v-model="userPwd"
  59. name="userPwd"
  60. type="password"
  61. :placeholder="$t('login.passWordInput')"
  62. :rules="[{ required: true, message: $t('login.passWordInput') }]"
  63. class="form-field"
  64. >
  65. <template #left-icon>
  66. <van-icon name="lock" size="22" class="field-icon" />
  67. </template>
  68. </van-field>
  69. </div>
  70. <!-- 验证码登录 -->
  71. <div v-if="loginType === 'sms'">
  72. <!-- 手机号/邮箱输入 -->
  73. <van-field
  74. v-model="phoneOrEmail"
  75. name="phoneOrEmail"
  76. :placeholder="$t('login.phoneOrEmailInput')"
  77. :rules="phoneOrEmailRules"
  78. class="form-field"
  79. >
  80. <template #left-icon>
  81. <van-icon name="phone-o" size="22" class="field-icon" />
  82. </template>
  83. </van-field>
  84. <!-- 验证码输入 -->
  85. <van-field
  86. v-model="smsCode"
  87. name="smsCode"
  88. :placeholder="$t('login.smsCodeInput')"
  89. :rules="[{ required: true, message: $t('login.smsCodeInput') }]"
  90. class="form-field sms-field"
  91. >
  92. <template #left-icon>
  93. <van-icon name="shield-o" size="22" class="field-icon" />
  94. </template>
  95. <template #button>
  96. <van-button
  97. size="small"
  98. type="primary"
  99. :disabled="smsCountdown > 0 || !phoneOrEmail"
  100. @click="sendSmsCode"
  101. class="sms-button"
  102. >
  103. {{ smsCountdown > 0 ? `${smsCountdown}s` : $t('login.sendSmsCode') }}
  104. </van-button>
  105. </template>
  106. </van-field>
  107. </div>
  108. <!-- 记住密码和忘记密码 (仅密码登录时显示) -->
  109. <div v-if="loginType === 'password'" class="password-options">
  110. <div class="remember-me">
  111. <van-checkbox v-model="checked" shape="square" icon-size="20">
  112. {{ $t("login.checkedPassWord") }}
  113. </van-checkbox>
  114. </div>
  115. <div class="forgot-password" @click="forgetPassword">
  116. <span>{{ $t("login.forgetPassWord") }}</span>
  117. <van-icon name="arrow" size="16" />
  118. </div>
  119. </div>
  120. <!-- 登录按钮 -->
  121. <van-button
  122. round
  123. block
  124. type="primary"
  125. native-type="submit"
  126. class="login-button"
  127. >
  128. {{ $t("login.loginButton") }}
  129. </van-button>
  130. <!-- 微信登录 -->
  131. <div v-if="isInWeChat" class="wechat-login" @click="wxLoginHandler">
  132. <van-button round block class="wechat-button">
  133. <div class="wechat-container">
  134. <van-icon name="wechat" size="28" class="wechat-icon" />
  135. <span>{{ $t("login.loginWithWechat") }}</span>
  136. </div>
  137. </van-button>
  138. </div>
  139. <!-- 注册选项 -->
  140. <div class="register-option">
  141. <span class="register-link" @click="registerClick">{{
  142. $t("login.regusterButton")
  143. }}</span>
  144. </div>
  145. </van-form>
  146. </div>
  147. </div>
  148. </div>
  149. </template>
  150. <script>
  151. import md5 from "js-md5";
  152. import { onMounted, ref, computed, reactive } from "vue";
  153. import {
  154. showSuccessToast,
  155. showFailToast,
  156. showDialog,
  157. Dialog,
  158. Button,
  159. } from "vant";
  160. import { loginSys, getSys, getOpenid, sendSmsCode as sendSmsCodeApi, loginWithSms } from "../service/login";
  161. import {
  162. setLocal,
  163. getLocal,
  164. navigatorLanguage,
  165. styleUrl,
  166. } from "../common/js/utils";
  167. import sHeader from "../components/SimpleHeader";
  168. import { useRoute, useRouter } from "vue-router";
  169. import { useI18n } from "vue-i18n";
  170. import defaultLogo from "../assets/login/logo.png";
  171. import aetiLogo from "../assets/login/aetiLogo.png";
  172. export default {
  173. setup() {
  174. let languageName = ref(getLocal("curLang"));
  175. const { t } = useI18n();
  176. const checked = ref(false); // 是否记住密码状态
  177. const userName = ref("");
  178. const userPwd = ref("");
  179. const router = useRouter();
  180. const route = useRoute();
  181. const sys = ref(null);
  182. const currentLan = ref(""); // 当前语言
  183. const logoName = ref(defaultLogo); // Logo图片名称
  184. const sysTitle = ref(""); // 页头标题
  185. // 验证码登录相关
  186. const loginType = ref("password"); // 登录方式:password | sms
  187. const phoneOrEmail = ref(""); // 手机号或邮箱
  188. const smsCode = ref(""); // 验证码
  189. const smsCountdown = ref(0); // 验证码倒计时
  190. const countdownTimer = ref(null); // 倒计时定时器
  191. // 手机号/邮箱验证规则
  192. const phoneOrEmailRules = [
  193. { required: true, message: t('login.phoneOrEmailInput') },
  194. {
  195. validator: (value) => {
  196. const phoneRegex = /^1[3-9]\d{9}$/;
  197. const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  198. return phoneRegex.test(value) || emailRegex.test(value);
  199. },
  200. message: t('login.phoneOrEmailFormatError')
  201. }
  202. ];
  203. // 切换登录方式
  204. const switchLoginType = (type) => {
  205. loginType.value = type;
  206. // 清空表单数据
  207. if (type === 'password') {
  208. phoneOrEmail.value = '';
  209. smsCode.value = '';
  210. clearCountdown();
  211. } else {
  212. userName.value = '';
  213. userPwd.value = '';
  214. }
  215. };
  216. // 清除倒计时
  217. const clearCountdown = () => {
  218. if (countdownTimer.value) {
  219. clearInterval(countdownTimer.value);
  220. countdownTimer.value = null;
  221. }
  222. smsCountdown.value = 0;
  223. };
  224. // 页面初始化
  225. onMounted(() => {
  226. // 加载样式
  227. styleUrl("login");
  228. // 如果没有语言缓存
  229. if (!getLocal("curLang")) {
  230. // 根据浏览器语言重新缓存到localstorage
  231. setLocal("curLang", navigatorLanguage());
  232. languageName.value = getLocal("curLang");
  233. }
  234. if (route.query.relation_admin_id) {
  235. getSysFun();
  236. }
  237. const savedCredentials = localStorage.getItem("savedCredentials");
  238. if (savedCredentials) {
  239. checked.value = true;
  240. const { savedUsername, savedPassword } = JSON.parse(savedCredentials);
  241. userName.value = savedUsername;
  242. userPwd.value = savedPassword;
  243. }
  244. getDomainFunc();
  245. });
  246. const getDomainFunc = async () => {
  247. const currentDomain = window.location.href;
  248. // console.log("href >>>", currentDomain);
  249. // console.log("hostname >>>", window.location.hostname);
  250. switch (true) {
  251. case currentDomain.includes("/aeti/"): // aeti是美国孙总portalmcc.com.cn
  252. logoName.value = aetiLogo;
  253. sysTitle.value = "AETI GLOBAL";
  254. break;
  255. default:
  256. logoName.value = defaultLogo;
  257. sysTitle.value = t("public.sysName");
  258. }
  259. };
  260. const getSysFun = async () => {
  261. const { data } = await getSys({
  262. relationAdminId: route.query.relation_admin_id,
  263. });
  264. if (data.code === "00000") {
  265. data.data.relationAdminId = route.query.relation_admin_id;
  266. setLocal("loginSys", JSON.stringify(data.data));
  267. sys.value = data.data;
  268. console.log("sys.value >>>", sys.value);
  269. }
  270. };
  271. // 发送验证码
  272. const sendSmsCode = async () => {
  273. if (!phoneOrEmail.value) {
  274. showFailToast(t('login.phoneOrEmailInput'));
  275. return;
  276. }
  277. try {
  278. const { data } = await sendSmsCodeApi({
  279. phoneOrEmail: phoneOrEmail.value,
  280. hostName: "Sunzee",
  281. });
  282. if (data.code === "00000") {
  283. showSuccessToast(t('login.smsCodeSent'));
  284. startCountdown();
  285. } else {
  286. showFailToast(data.message || t('login.smsCodeSendFailed'));
  287. }
  288. } catch (error) {
  289. showFailToast(t('login.smsCodeSendFailed'));
  290. }
  291. };
  292. // 开始倒计时
  293. const startCountdown = () => {
  294. smsCountdown.value = 60;
  295. countdownTimer.value = setInterval(() => {
  296. smsCountdown.value--;
  297. if (smsCountdown.value <= 0) {
  298. clearCountdown();
  299. }
  300. }, 1000);
  301. };
  302. // 登录
  303. const onSubmit = async (values) => {
  304. if (loginType.value === 'password') {
  305. // 密码登录
  306. const { data } = await loginSys({
  307. username: values.userName,
  308. password: md5(values.userPwd),
  309. hostName: "Sunzee",
  310. });
  311. console.log(checked.value);
  312. if (data.code === "00000") {
  313. setLocal("loginUser", JSON.stringify(data.data));
  314. if (checked.value) {
  315. const savedCredentials = JSON.stringify({
  316. savedUsername: values.userName,
  317. savedPassword: values.userPwd,
  318. });
  319. localStorage.setItem("savedCredentials", savedCredentials);
  320. } else {
  321. const savedCredentials = localStorage.getItem("savedCredentials");
  322. if (savedCredentials) {
  323. localStorage.removeItem("savedCredentials", savedCredentials);
  324. }
  325. }
  326. showSuccessToast(t("login.loginSucess"));
  327. localStorage.setItem("firstLogin", true);
  328. // 需要刷新页面,否则 axios.js 文件里的 token 不会被重置
  329. // window.location.href = '/shenze/';
  330. setTimeout(() => {
  331. router.push("/home");
  332. }, 200);
  333. } else {
  334. showFailToast(data.message);
  335. }
  336. } else {
  337. // 验证码登录
  338. const { data } = await loginWithSms({
  339. phoneOrEmail: values.phoneOrEmail,
  340. code: values.smsCode,
  341. hostName: "Sunzee",
  342. });
  343. if (data.code === "00000") {
  344. setLocal("loginUser", JSON.stringify(data.data));
  345. showSuccessToast(t("login.loginSucess"));
  346. localStorage.setItem("firstLogin", true);
  347. clearCountdown();
  348. setTimeout(() => {
  349. router.push("/home");
  350. }, 200);
  351. } else {
  352. showFailToast(data.message);
  353. }
  354. }
  355. };
  356. // 跳转注册页面
  357. const registerClick = async () => {
  358. await router.push("/register");
  359. };
  360. // 跳转忘记密码页面
  361. const forgetPassword = async () => {
  362. await router.push("/forgetpassword");
  363. };
  364. const state = reactive({
  365. isLoading: false,
  366. });
  367. // 微信登录
  368. const wxLoginHandler = async () => {
  369. state.isLoading = true;
  370. try {
  371. // 用户静默授权,获取 用户信息
  372. const { data } = await getOpenid({ hostName: "Sunzee" });
  373. console.log("微信登录:", data);
  374. if (data.code === "00000") {
  375. window.location.href = data.data;
  376. } else {
  377. showFailToast("微信登录失败:" + data.message);
  378. }
  379. } catch (error) {
  380. handleError(error.message || "微信登录失败,请重试");
  381. } finally {
  382. state.isLoading = false;
  383. }
  384. };
  385. const isInWeChat = computed(() => {
  386. const ua = window.navigator.userAgent.toLowerCase();
  387. return new RegExp("micromessenger").test(ua);
  388. });
  389. const handleError = (errMsg) => {
  390. showDialog({
  391. title: "错误提示",
  392. message: errMsg,
  393. });
  394. };
  395. return {
  396. checked,
  397. userName,
  398. userPwd,
  399. onSubmit,
  400. registerClick,
  401. forgetPassword,
  402. sys,
  403. isInWeChat,
  404. state,
  405. wxLoginHandler,
  406. getOpenid,
  407. currentLan,
  408. logoName,
  409. sysTitle,
  410. // 验证码登录相关
  411. loginType,
  412. phoneOrEmail,
  413. smsCode,
  414. smsCountdown,
  415. phoneOrEmailRules,
  416. switchLoginType,
  417. sendSmsCode,
  418. };
  419. },
  420. components: {
  421. sHeader,
  422. [Button.name]: Button,
  423. [Dialog.name]: Dialog,
  424. },
  425. };
  426. </script>
  427. <style lang="less" scoped>
  428. @theme-color: #4d6add;
  429. .login-container {
  430. display: flex;
  431. flex-direction: column;
  432. height: 100vh;
  433. overflow: hidden;
  434. }
  435. .login-content {
  436. flex: 1;
  437. display: flex;
  438. flex-direction: column;
  439. align-items: center;
  440. padding-top: 30px;
  441. // justify-content: center;
  442. background: #f5f6fa;
  443. height: calc(100% - 45px);
  444. overflow: auto;
  445. overflow-x: hidden;
  446. }
  447. .logo-section {
  448. margin-bottom: 20px;
  449. .logo-container {
  450. width: 120px;
  451. height: 120px;
  452. border-radius: 50%;
  453. display: flex;
  454. align-items: center;
  455. justify-content: center;
  456. background-color: #fff;
  457. box-shadow: 0 4px 20px rgba(2, 77, 163, 0.25);
  458. border: 2px solid rgba(2, 77, 163, 0.1);
  459. padding: 10px;
  460. .system-logo {
  461. width: 90%;
  462. }
  463. }
  464. }
  465. .form-container {
  466. width: 100%;
  467. max-width: 400px;
  468. .login-type-tabs {
  469. display: flex;
  470. margin: 15px 15px 0 15px;
  471. background-color: #f5f9ff;
  472. border-radius: 12px;
  473. padding: 4px;
  474. margin-bottom: 0;
  475. .tab-item {
  476. flex: 1;
  477. text-align: center;
  478. padding: 12px 0;
  479. border-radius: 8px;
  480. font-size: 16px;
  481. font-weight: 500;
  482. color: #666;
  483. cursor: pointer;
  484. transition: all 0.3s ease;
  485. &.active {
  486. background-color: white;
  487. color: @theme-color;
  488. box-shadow: 0 2px 8px rgba(2, 77, 163, 0.15);
  489. }
  490. &:hover:not(.active) {
  491. color: @theme-color;
  492. }
  493. }
  494. }
  495. .login-form {
  496. margin: 15px;
  497. background-color: white;
  498. border-radius: 20px;
  499. padding: 20px;
  500. box-shadow: 0 8px 30px rgba(2, 77, 163, 0.15);
  501. }
  502. .form-field {
  503. margin-bottom: 22px;
  504. padding: 16px 15px;
  505. background-color: #f5f9ff;
  506. border-radius: 12px;
  507. :deep(.van-field__control) {
  508. padding-left: 10px;
  509. font-size: 16px;
  510. }
  511. .field-icon {
  512. color: @theme-color;
  513. margin-right: 8px;
  514. }
  515. :deep(.van-field__error-message) {
  516. margin-left: 10px;
  517. }
  518. &.sms-field {
  519. :deep(.van-field__button) {
  520. padding-left: 10px;
  521. }
  522. :deep(.van-field__left-icon) {
  523. display: flex;
  524. align-items: center;
  525. justify-content: center;
  526. }
  527. .sms-button {
  528. background: linear-gradient(135deg, @theme-color, #0f6fee);
  529. border: none;
  530. border-radius: 8px;
  531. color: white;
  532. font-size: 14px;
  533. font-weight: 500;
  534. min-width: 100px;
  535. height: 36px;
  536. box-shadow: 0 2px 8px rgba(2, 77, 163, 0.3);
  537. &:disabled {
  538. background: #ccc;
  539. color: #999;
  540. box-shadow: none;
  541. }
  542. &:active:not(:disabled) {
  543. opacity: 0.9;
  544. transform: translateY(1px);
  545. }
  546. }
  547. }
  548. }
  549. }
  550. .password-options {
  551. display: flex;
  552. justify-content: space-between;
  553. align-items: center;
  554. margin-top: -8px;
  555. margin-bottom: 20px;
  556. .remember-me {
  557. :deep(.van-checkbox__label) {
  558. color: #666;
  559. font-size: 14px;
  560. }
  561. :deep(.van-checkbox__icon--checked .van-icon) {
  562. background-color: @theme-color;
  563. border-color: @theme-color;
  564. }
  565. }
  566. .forgot-password {
  567. display: flex;
  568. align-items: center;
  569. color: @theme-color;
  570. font-size: 14px;
  571. cursor: pointer;
  572. span {
  573. margin-right: 4px;
  574. }
  575. &:hover {
  576. text-decoration: underline;
  577. }
  578. }
  579. }
  580. .login-button {
  581. height: 50px;
  582. font-size: 18px;
  583. font-weight: 500;
  584. color: white;
  585. background: linear-gradient(135deg, @theme-color, #0f6fee);
  586. border: none;
  587. margin-top: 15px;
  588. margin-bottom: 20px;
  589. box-shadow: 0 4px 15px rgba(2, 77, 163, 0.3);
  590. &:active {
  591. opacity: 0.95;
  592. transform: translateY(1px);
  593. }
  594. }
  595. .wechat-login {
  596. margin: 20px 0;
  597. .wechat-button {
  598. height: 50px;
  599. background-color: #07c160;
  600. border: none;
  601. color: white;
  602. font-weight: 500;
  603. &:active {
  604. background-color: #06ae56;
  605. }
  606. }
  607. .wechat-container {
  608. display: flex;
  609. align-items: center;
  610. justify-content: center;
  611. .wechat-icon {
  612. margin-right: 10px;
  613. color: white;
  614. }
  615. }
  616. }
  617. .register-option {
  618. text-align: center;
  619. margin-top: 25px;
  620. color: #666;
  621. font-size: 15px;
  622. .register-link {
  623. color: @theme-color;
  624. font-weight: 500;
  625. margin-left: 8px;
  626. cursor: pointer;
  627. &:hover {
  628. text-decoration: underline;
  629. }
  630. }
  631. }
  632. .footer-container {
  633. text-align: center;
  634. padding: 15px 0;
  635. .app-version {
  636. font-size: 13px;
  637. color: #888;
  638. margin-bottom: 5px;
  639. }
  640. .copyright {
  641. font-size: 12px;
  642. color: #999;
  643. }
  644. }
  645. @media (max-width: 480px) {
  646. .logo-section .logo-container {
  647. width: 150px;
  648. height: 150px;
  649. }
  650. .login-button,
  651. .wechat-button {
  652. height: 46px;
  653. font-size: 16px;
  654. }
  655. .form-field {
  656. padding: 12px 15px;
  657. }
  658. }
  659. </style>